From 8aa4f053bbeaf7efe635a2178d501d190dcedf44 Mon Sep 17 00:00:00 2001 From: Waleed Hassan Date: Fri, 6 Mar 2020 22:44:32 +0300 Subject: [PATCH] Use optional chaining and linter improvements (#6422) This is mostly just a follow up to #6342. `prefer-optional-chaining` was turned on and fixed in every location it complained in. The transformed function [0] looks expensive from a glance but from skimming through the replaced sites it doesn't appear to be ran in any important place, so it should be OK. The linter improvements are: - Increase linter performance - Make `full-lint` and `lint` write to different caches so we avoid overwriting their caches since they're different configs - Change husky's hook to `npm run lint` so as to write to the same cache - Remove `@typescript-eslint/eslint-plugin-tslint` which is essentially a wrapper to TSLint because the rules aren't worth running another linter - Convert `.eslintrc.json` and `.eslintrc-syntax.json` to two spaces rather than four tabs to respect PS' `.editorconfig` - Rename `fulllint` to `full-lint` to ease spelling it [0] - https://pastie.io/mmtxpf.js (prettified) --- .eslintrc | 61 ------ .eslintrc-no-types.json | 285 ++++++++++++++++++++++++ .eslintrc-syntax.json | 301 -------------------------- .eslintrc-types.json | 61 ++++++ .gitignore | 1 + .travis.yml | 4 +- lib/streams.ts | 4 +- package.json | 9 +- server/chat-commands/admin.ts | 8 +- server/chat-commands/room-settings.ts | 4 +- server/chat-plugins/chat-monitor.ts | 3 + server/chat-plugins/datasearch.ts | 12 +- server/chat-plugins/helptickets.ts | 10 +- server/chat-plugins/lottery.ts | 10 +- server/chat-plugins/mafia.ts | 20 +- server/chat-plugins/modlog.ts | 2 +- server/chat-plugins/poll.ts | 2 +- server/chat-plugins/room-events.ts | 2 +- server/chat.ts | 17 +- server/ladders-local.ts | 2 +- server/ladders-remote.ts | 12 +- server/ladders.ts | 2 +- server/monitor.ts | 2 +- server/punishments.ts | 14 +- server/room-battle.ts | 34 +-- server/rooms.ts | 4 +- server/team-validator-async.ts | 2 +- server/tournaments/index.ts | 8 +- server/users.ts | 10 +- sim/battle.ts | 12 +- sim/dex-data.ts | 3 + sim/dex.ts | 8 +- sim/field.ts | 4 +- sim/pokemon.ts | 10 +- sim/side.ts | 2 +- sim/team-validator.ts | 2 +- 36 files changed, 471 insertions(+), 476 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc-no-types.json delete mode 100644 .eslintrc-syntax.json create mode 100644 .eslintrc-types.json diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 6554d839beae..000000000000 --- a/.eslintrc +++ /dev/null @@ -1,61 +0,0 @@ -{ - "root": true, - "parserOptions": { - "ecmaVersion": 9, - "sourceType": "script", - "ecmaFeatures": { - "globalReturn": true - } - }, - "ignorePatterns": [ - "logs/", - "node_modules/", - ".*-dist/", - "tools/set-import/importer.js", - "tools/set-import/sets", - "server/global-variables.d.ts", - "sim/global-variables.d.ts" - ], - "env": { - "es6": true, - "node": true - }, - "globals": { - "Config": false, "Monitor": false, "toID": false, "Dex": false, "LoginServer": false, - "Users": false, "Punishments": false, "Rooms": false, "Verifier": false, "Chat": false, - "Tournaments": false, "IPTools": false, "Sockets": false, "TeamValidator": false, - "TeamValidatorAsync": false, "Ladders": false - }, - "extends": ["eslint:recommended", "./.eslintrc-syntax.json"], - "overrides": [ - { - "files": ["./lib/*.ts", "./server/**/*.ts", "./sim/**/*.ts", "./tools/set-import/*.ts"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 9, - "sourceType": "module", - "tsconfigRootDir": ".", - "project": ["./tsconfig.json"] - }, - "extends": [ - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "./.eslintrc-syntax.json" - ], - "rules": { - "@typescript-eslint/no-floating-promises": ["warn", {"ignoreVoid": true}], - "@typescript-eslint/no-misused-promises": "off", - "@typescript-eslint/no-for-in-array": "error" - } - }, - { - "env": { - "mocha": true - }, - "files": ["test/**/*.js"], - "globals": { - "BattleEngine": false - } - } - ] -} diff --git a/.eslintrc-no-types.json b/.eslintrc-no-types.json new file mode 100644 index 000000000000..10a0f0c17862 --- /dev/null +++ b/.eslintrc-no-types.json @@ -0,0 +1,285 @@ +{ + "root": true, + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "script", + "ecmaFeatures": { + "globalReturn": true + } + }, + "env": { + "es6": true, + "node": true + }, + "globals": { + "Config": false, "Monitor": false, "toID": false, "Dex": false, "LoginServer": false, + "Users": false, "Punishments": false, "Rooms": false, "Verifier": false, "Chat": false, + "Tournaments": false, "IPTools": false, "Sockets": false, "TeamValidator": false, + "TeamValidatorAsync": false, "Ladders": false + }, + "ignorePatterns": [ + "logs/", + "node_modules/", + ".*-dist/", + "tools/set-import/importer.js", + "tools/set-import/sets", + "server/global-variables.d.ts", + "sim/global-variables.d.ts" + ], + "extends": "eslint:recommended", + "rules": { + "no-cond-assign": ["error", "except-parens"], + "no-console": "off", + "no-control-regex": "off", + "no-empty": ["error", {"allowEmptyCatch": true}], + "no-inner-declarations": ["error", "functions"], + "require-atomic-updates": "off", + "valid-jsdoc": "off", + + "eol-last": ["error", "always"], + "array-callback-return": "error", + "block-scoped-var": "error", + "comma-dangle": ["error", {"arrays": "always-multiline", "objects": "always-multiline", "imports": "always-multiline", "exports": "always-multiline", "functions": "ignore"}], + "complexity": "off", + "consistent-return": "off", + "curly": ["error", "multi-line", "consistent"], + "default-case": "off", + "dot-location": ["error", "property"], + "dot-notation": "off", + "eqeqeq": "error", + "guard-for-in": "off", + "no-caller": "error", + "no-case-declarations": "off", + "no-div-regex": "error", + "no-else-return": "off", + "no-labels": ["error", {"allowLoop": true, "allowSwitch": true}], + "no-eval": "off", + "no-implied-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "warn", + "no-extra-label": "error", + "no-extra-parens": "off", + "no-floating-decimal": "error", + "no-implicit-coercion": "off", + "no-invalid-this": "off", + "no-iterator": "error", + "no-lone-blocks": "off", + "no-loop-func": "off", + "no-magic-numbers": "off", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-param-reassign": "off", + "no-proto": "error", + "no-return-assign": ["error", "except-parens"], + "no-self-compare": "error", + "no-sequences": "error", + "no-throw-literal": "error", + "no-unmodified-loop-condition": "error", + "no-unused-expressions": "error", + "no-useless-call": "error", + "no-useless-concat": "off", + "no-void": "off", + "no-warning-comments": "off", + "no-with": "error", + "radix": ["warn", "as-needed"], + "vars-on-top": "off", + "wrap-iife": ["error", "inside"], + "yoda": ["error", "never", { "exceptRange": true }], + + "strict": ["error", "global"], + "init-declarations": "off", + "no-shadow": "off", + "no-label-var": "error", + "no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet"], + "no-shadow-restricted-names": "error", + "no-undef": ["error", {"typeof": true}], + "no-undefined": "off", + "no-unused-vars": ["warn", {"args": "none"}], + "no-use-before-define": ["error", {"functions": false, "classes": false, "variables": false}], + + "callback-return": [2, ["callback", "cb", "done"]], + "no-mixed-requires": "error", + "no-new-require": "error", + "no-path-concat": "off", + "no-process-env": "off", + "no-process-exit": "off", + "no-restricted-modules": ["error", "moment", "request", "sugar"], + "no-sync": "off", + + "array-bracket-spacing": ["error", "never"], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", {"allowSingleLine": true}], + "camelcase": "off", + "comma-spacing": ["error", {"before": false, "after": true}], + "comma-style": ["error", "last"], + "computed-property-spacing": ["error", "never"], + "consistent-this": "off", + "func-names": "off", + "func-style": "off", + "id-length": "off", + "id-match": "off", + "indent": ["error", "tab"], + "key-spacing": "error", + "lines-around-comment": "off", + "max-nested-callbacks": "off", + "max-statements-per-line": "off", + "new-cap": ["error", {"newIsCap": true, "capIsNew": false}], + "new-parens": "error", + "padding-line-between-statements": "off", + "no-array-constructor": "error", + "no-continue": "off", + "no-inline-comments": "off", + "no-lonely-if": "off", + "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], + "no-multiple-empty-lines": ["error", {"max": 2, "maxEOF": 1}], + "no-negated-condition": "off", + "no-nested-ternary": "off", + "no-new-object": "error", + "func-call-spacing": "error", + "no-ternary": "off", + "no-trailing-spaces": ["error", {"ignoreComments": false}], + "no-underscore-dangle": "off", + "no-unneeded-ternary": "error", + "object-curly-spacing": ["error", "never"], + "object-shorthand": ["error", "methods"], + "one-var": "off", + "operator-assignment": "off", + "operator-linebreak": ["error", "after"], + "padded-blocks": ["error", "never"], + "quote-props": "off", + "quotes": "off", + "require-jsdoc": "off", + "semi-spacing": ["error", {"before": false, "after": true}], + "semi": ["error", "always"], + "sort-vars": "off", + "keyword-spacing": ["error", {"before": true, "after": true}], + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}], + "space-in-parens": ["error", "never"], + "space-infix-ops": "error", + "space-unary-ops": ["error", {"words": true, "nonwords": false}], + "wrap-regex": "off", + + "arrow-parens": "off", + "arrow-spacing": ["error", {"before": true, "after": true}], + "no-confusing-arrow": "off", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "prefer-arrow-callback": "off", + "rest-spread-spacing": ["error", "never"], + "template-curly-spacing": ["error", "never"], + "no-restricted-syntax": ["error", "WithStatement"], + + "no-constant-condition": ["error", {"checkLoops": false}] + }, + "overrides": [ + { + "files": ["./lib/*.ts", "./server/**/*.ts", "./sim/**/*.ts", "./tools/set-import/*.ts"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "module", + "tsconfigRootDir": ".", + "project": ["./tsconfig.json"] + }, + "extends": [ + "plugin:@typescript-eslint/recommended" + ], + "plugins": [ + "import", + "jsdoc" + ], + "rules": { + "@typescript-eslint/indent": ["error", "tab", {"SwitchCase": 0, "flatTernaryExpressions": true}], + "@typescript-eslint/consistent-type-assertions": ["error", {"assertionStyle": "as"}], + "@typescript-eslint/no-dupe-class-members": "error", + "@typescript-eslint/prefer-regexp-exec": "off", + "@typescript-eslint/prefer-optional-chain": "error", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/ban-ts-ignore": "off", + "@typescript-eslint/consistent-type-definitions": ["error", "interface"], + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/unbound-method": "off", + "@typescript-eslint/camelcase": "off", + "@typescript-eslint/prefer-string-starts-ends-with": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/adjacent-overload-signatures": "error", + "@typescript-eslint/array-type": "error", + "@typescript-eslint/ban-types": "error", + "@typescript-eslint/class-name-casing": "error", + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-ordering": "off", + "@typescript-eslint/no-empty-interface": "error", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-this-alias": "error", + "@typescript-eslint/no-var-requires": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/prefer-function-type": "error", + "@typescript-eslint/prefer-namespace-keyword": "error", + "@typescript-eslint/quotes": "off", + "@typescript-eslint/semi": [ + "error", + "always" + ], + "@typescript-eslint/triple-slash-reference": "error", + "@typescript-eslint/unified-signatures": "error", + "import/no-extraneous-dependencies": "error", + "jsdoc/no-types": "error", + "new-parens": "off", + "no-dupe-class-members": "off", + "operator-linebreak": "off", + "indent": "off", + "prefer-const": ["error", { + "destructuring": "all" + }], + "comma-dangle": ["error", "only-multiline", { + "objects": "always", + "arrays": "always", + "functions": "never" + }], + "no-unused-expressions": ["error", {"allowTernary": true}], + "no-prototype-builtins":"off", + "max-len": ["warn", {"code": 120, "tabWidth": 0, "ignorePattern": "^\\s*(// \\s*)?((let |const )?[a-zA-Z0-9$.]+ \\+?=>? (\\$\\()?|(return |throw )?(new )?([a-zA-Z0-9$.]+\\()?|Chat.html)?['\"`]"}], + "constructor-super": "error", + "max-classes-per-file": "off", + "no-bitwise": "off", + "no-debugger": "error", + "no-duplicate-case": "error", + "no-duplicate-imports": "error", + "no-fallthrough": "off", + "no-redeclare": "error", + "no-return-await": "error", + "no-sparse-arrays": "error", + "no-template-curly-in-string": "error", + "no-undef-init": "off", + "no-unsafe-finally": "error", + "no-unused-labels": "error", + "prefer-object-spread": "off", + "spaced-comment": ["error", "always", {"exceptions": ["*"]}], + "use-isnan": "error", + "valid-typeof": "off" + } + }, + { + "env": { + "mocha": true + }, + "files": ["test/**/*.js"], + "globals": { + "BattleEngine": false + } + } + ] +} diff --git a/.eslintrc-syntax.json b/.eslintrc-syntax.json deleted file mode 100644 index ffe69e7995ae..000000000000 --- a/.eslintrc-syntax.json +++ /dev/null @@ -1,301 +0,0 @@ -{ - "root": true, - "parserOptions": { - "ecmaVersion": 9, - "sourceType": "script", - "ecmaFeatures": { - "globalReturn": true - } - }, - "env": { - "es6": true, - "node": true - }, - "globals": { - "Config": false, "Monitor": false, "toID": false, "Dex": false, "LoginServer": false, - "Users": false, "Punishments": false, "Rooms": false, "Verifier": false, "Chat": false, - "Tournaments": false, "IPTools": false, "Sockets": false, "TeamValidator": false, - "TeamValidatorAsync": false, "Ladders": false - }, - "extends": "eslint:recommended", - "rules": { - "no-cond-assign": ["error", "except-parens"], - "no-console": "off", - "no-control-regex": "off", - "no-empty": ["error", {"allowEmptyCatch": true}], - "no-inner-declarations": ["error", "functions"], - "require-atomic-updates": "off", - "valid-jsdoc": "off", - - "eol-last": ["error", "always"], - "array-callback-return": "error", - "block-scoped-var": "error", - "comma-dangle": ["error", {"arrays": "always-multiline", "objects": "always-multiline", "imports": "always-multiline", "exports": "always-multiline", "functions": "ignore"}], - "complexity": "off", - "consistent-return": "off", - "curly": ["error", "multi-line", "consistent"], - "default-case": "off", - "dot-location": ["error", "property"], - "dot-notation": "off", - "eqeqeq": "error", - "guard-for-in": "off", - "no-caller": "error", - "no-case-declarations": "off", - "no-div-regex": "error", - "no-else-return": "off", - "no-labels": ["error", {"allowLoop": true, "allowSwitch": true}], - "no-eval": "off", - "no-implied-eval": "error", - "no-extend-native": "error", - "no-extra-bind": "warn", - "no-extra-label": "error", - "no-extra-parens": "off", - "no-floating-decimal": "error", - "no-implicit-coercion": "off", - "no-invalid-this": "off", - "no-iterator": "error", - "no-lone-blocks": "off", - "no-loop-func": "off", - "no-magic-numbers": "off", - "no-multi-spaces": "error", - "no-multi-str": "error", - "no-new-func": "error", - "no-new-wrappers": "error", - "no-new": "error", - "no-octal-escape": "error", - "no-param-reassign": "off", - "no-proto": "error", - "no-return-assign": ["error", "except-parens"], - "no-self-compare": "error", - "no-sequences": "error", - "no-throw-literal": "error", - "no-unmodified-loop-condition": "error", - "no-unused-expressions": "error", - "no-useless-call": "error", - "no-useless-concat": "off", - "no-void": "off", - "no-warning-comments": "off", - "no-with": "error", - "radix": ["warn", "as-needed"], - "vars-on-top": "off", - "wrap-iife": ["error", "inside"], - "yoda": ["error", "never", { "exceptRange": true }], - - "strict": ["error", "global"], - - "init-declarations": "off", - "no-shadow": "off", - "no-label-var": "error", - "no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet"], - "no-shadow-restricted-names": "error", - "no-undef": ["error", {"typeof": true}], - "no-undefined": "off", - "no-unused-vars": ["warn", {"args": "none"}], - "no-use-before-define": ["error", {"functions": false, "classes": false, "variables": false}], - - "callback-return": [2, ["callback", "cb", "done"]], - "no-mixed-requires": "error", - "no-new-require": "error", - "no-path-concat": "off", - "no-process-env": "off", - "no-process-exit": "off", - "no-restricted-modules": ["error", "moment", "request", "sugar"], - "no-sync": "off", - - "array-bracket-spacing": ["error", "never"], - "block-spacing": ["error", "always"], - "brace-style": ["error", "1tbs", {"allowSingleLine": true}], - "camelcase": "off", - "comma-spacing": ["error", {"before": false, "after": true}], - "comma-style": ["error", "last"], - "computed-property-spacing": ["error", "never"], - "consistent-this": "off", - "func-names": "off", - "func-style": "off", - "id-length": "off", - "id-match": "off", - "indent": ["error", "tab"], - "key-spacing": "error", - "lines-around-comment": "off", - "max-nested-callbacks": "off", - "max-statements-per-line": "off", - "new-cap": ["error", {"newIsCap": true, "capIsNew": false}], - "new-parens": "error", - "padding-line-between-statements": "off", - "no-array-constructor": "error", - "no-continue": "off", - "no-inline-comments": "off", - "no-lonely-if": "off", - "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], - "no-multiple-empty-lines": ["error", {"max": 2, "maxEOF": 1}], - "no-negated-condition": "off", - "no-nested-ternary": "off", - "no-new-object": "error", - "func-call-spacing": "error", - "no-ternary": "off", - "no-trailing-spaces": ["error", {"ignoreComments": false}], - "no-underscore-dangle": "off", - "no-unneeded-ternary": "error", - "object-curly-spacing": ["error", "never"], - "object-shorthand": ["error", "methods"], - "one-var": "off", - "operator-assignment": "off", - "operator-linebreak": ["error", "after"], - "padded-blocks": ["error", "never"], - "quote-props": "off", - "quotes": "off", - "require-jsdoc": "off", - "semi-spacing": ["error", {"before": false, "after": true}], - "semi": ["error", "always"], - "sort-vars": "off", - "keyword-spacing": ["error", {"before": true, "after": true}], - "space-before-blocks": ["error", "always"], - "space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}], - "space-in-parens": ["error", "never"], - "space-infix-ops": "error", - "space-unary-ops": ["error", {"words": true, "nonwords": false}], - "wrap-regex": "off", - - "arrow-parens": "off", - "arrow-spacing": ["error", {"before": true, "after": true}], - "no-confusing-arrow": "off", - "no-useless-computed-key": "error", - "no-useless-rename": "error", - "no-var": "error", - "prefer-arrow-callback": "off", - "rest-spread-spacing": ["error", "never"], - "template-curly-spacing": ["error", "never"], - "no-restricted-syntax": ["error", "WithStatement"], - - "no-constant-condition": ["error", {"checkLoops": false}] - }, - "overrides": [ - { - "files": ["./lib/*.ts", "./server/**/*.ts", "./sim/**/*.ts", "./tools/set-import/*.ts"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 9, - "sourceType": "module", - "tsconfigRootDir": ".", - "project": ["./tsconfig.json"] - }, - "extends": [ - "plugin:@typescript-eslint/recommended" - ], - "plugins": [ - "@typescript-eslint/tslint", - "import", - "jsdoc" - ], - "rules": { - "@typescript-eslint/indent": ["error", "tab", {"SwitchCase": 0, "flatTernaryExpressions": true}], - "@typescript-eslint/consistent-type-assertions": ["error", {"assertionStyle": "as"}], - "@typescript-eslint/no-dupe-class-members": "error", - "@typescript-eslint/prefer-regexp-exec": "off", - "@typescript-eslint/no-namespace": "off", - "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/ban-ts-ignore": "off", - "@typescript-eslint/consistent-type-definitions": ["error", "interface"], - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/unbound-method": "off", - "@typescript-eslint/camelcase": "off", - "@typescript-eslint/prefer-string-starts-ends-with": "off", - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/member-delimiter-style": "off", - "@typescript-eslint/adjacent-overload-signatures": "error", - "@typescript-eslint/array-type": "error", - "@typescript-eslint/ban-types": "error", - "@typescript-eslint/class-name-casing": "error", - "@typescript-eslint/interface-name-prefix": "off", - "@typescript-eslint/member-ordering": "off", - "@typescript-eslint/no-empty-interface": "error", - "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-parameter-properties": "off", - "@typescript-eslint/no-this-alias": "error", - "@typescript-eslint/no-var-requires": "error", - "@typescript-eslint/prefer-for-of": "error", - "@typescript-eslint/prefer-function-type": "error", - "@typescript-eslint/prefer-namespace-keyword": "error", - "@typescript-eslint/quotes": "off", - "@typescript-eslint/semi": [ - "error", - "always" - ], - "@typescript-eslint/triple-slash-reference": "error", - "@typescript-eslint/unified-signatures": "error", - "@typescript-eslint/tslint/config": [ - "error", - { - "rules": { - "jsdoc-format": [ - true, - "check-multiline-start" - ], - "no-reference-import": true, - "static-this": true, - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type", - "check-typecast", - "check-type-operator", - "check-rest-spread" - ] - } - } - ], - "import/no-extraneous-dependencies": "error", - "jsdoc/no-types": "error", - "new-parens": "off", - "no-dupe-class-members": "off", - "operator-linebreak": "off", - "indent": "off", - "prefer-const": ["error", { - "destructuring": "all" - }], - "comma-dangle": ["error", "only-multiline", { - "objects": "always", - "arrays": "always", - "functions": "never" - }], - "no-unused-expressions": ["error", {"allowTernary": true}], - "no-prototype-builtins":"off", - "max-len": ["warn", {"code": 120, "tabWidth": 0, "ignorePattern": "^\\s*(// \\s*)?((let |const )?[a-zA-Z0-9$.]+ \\+?=>? (\\$\\()?|(return |throw )?(new )?([a-zA-Z0-9$.]+\\()?|Chat.html)?['\"`]"}], - "constructor-super": "error", - "max-classes-per-file": "off", - "no-bitwise": "off", - "no-debugger": "error", - "no-duplicate-case": "error", - "no-duplicate-imports": "error", - "no-fallthrough": "off", - "no-redeclare": "error", - "no-return-await": "error", - "no-sparse-arrays": "error", - "no-template-curly-in-string": "error", - "no-undef-init": "off", - "no-unsafe-finally": "error", - "no-unused-labels": "error", - "prefer-object-spread": "off", - "spaced-comment": ["error", "always", {"exceptions": ["*"]}], - "use-isnan": "error", - "valid-typeof": "off" - } - }, - { - "env": { - "mocha": true - }, - "files": ["test/**/*.js"], - "globals": { - "BattleEngine": false - } - } - ] -} diff --git a/.eslintrc-types.json b/.eslintrc-types.json new file mode 100644 index 000000000000..ac345c9bdd59 --- /dev/null +++ b/.eslintrc-types.json @@ -0,0 +1,61 @@ +{ + "root": true, + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "script", + "ecmaFeatures": { + "globalReturn": true + } + }, + "ignorePatterns": [ + "logs/", + "node_modules/", + ".*-dist/", + "tools/set-import/importer.js", + "tools/set-import/sets", + "server/global-variables.d.ts", + "sim/global-variables.d.ts" + ], + "env": { + "es6": true, + "node": true + }, + "globals": { + "Config": false, "Monitor": false, "toID": false, "Dex": false, "LoginServer": false, + "Users": false, "Punishments": false, "Rooms": false, "Verifier": false, "Chat": false, + "Tournaments": false, "IPTools": false, "Sockets": false, "TeamValidator": false, + "TeamValidatorAsync": false, "Ladders": false + }, + "extends": ["eslint:recommended", "./.eslintrc-no-types.json"], + "overrides": [ + { + "files": ["./lib/*.ts", "./server/**/*.ts", "./sim/**/*.ts", "./tools/set-import/*.ts"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "module", + "tsconfigRootDir": ".", + "project": ["./tsconfig.json"] + }, + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "./.eslintrc-no-types.json" + ], + "rules": { + "@typescript-eslint/no-floating-promises": ["warn", {"ignoreVoid": true}], + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-for-in-array": "error" + } + }, + { + "env": { + "mocha": true + }, + "files": ["test/**/*.js"], + "globals": { + "BattleEngine": false + } + } + ] +} diff --git a/.gitignore b/.gitignore index 91d51c716d0b..2ad0a42100be 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /server/chat-plugins/*-private.js npm-debug.log .eslintcache +.eslintfullcache package-lock.json /tools/set-import/sets databases/*.db* diff --git a/.travis.yml b/.travis.yml index d9b9f08b278b..624d995964f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,5 @@ node_js: cache: directories: - node_modules - - .eslintcache -script: npm run fulltest + - .eslintfullcache +script: npm run full-test diff --git a/lib/streams.ts b/lib/streams.ts index c149710c66d3..63c6ec1cbae4 100644 --- a/lib/streams.ts +++ b/lib/streams.ts @@ -301,7 +301,7 @@ export class ReadStream { async readLine(encoding: BufferEncoding = this.encoding) { if (!encoding) throw new Error(`readLine must have an encoding`); let line = await this.readDelimitedBy('\n', encoding); - if (line && line.endsWith('\r')) line = line.slice(0, -1); + if (line?.endsWith('\r')) line = line.slice(0, -1); return line; } @@ -572,7 +572,7 @@ export class ObjectReadStream { this.readSize = Math.max(count, this.readSize); while (!this.errorBuf && !this.atEOF && this.buf.length < this.readSize) { const readResult = this._read(); - if (readResult && readResult.then) { + if (readResult) { await readResult; } else { await this.nextPush; diff --git a/package.json b/package.json index e927d04b1426..1eef8f4d96b4 100644 --- a/package.json +++ b/package.json @@ -24,16 +24,16 @@ "start": "node pokemon-showdown start", "build": "node build", "tsc": "tsc", - "lint": "eslint . --config .eslintrc-syntax.json --cache --ext .js,.ts", - "fulllint": "eslint . --config .eslintrc --ext .js,.ts", + "lint": "eslint . --config .eslintrc-no-types.json --cache --ext .js,.ts", + "full-lint": "eslint . --config .eslintrc-types.json --cache --cache-location .eslintfullcache --ext .js,.ts", "pretest": "npm run lint && npm run build", "test": "mocha", "posttest": "npm run tsc", - "fulltest": "npm run fulllint && npm run build && npm run tsc && mocha --forbid-only -R spec -g \".*\"" + "full-test": "npm run full-lint && npm run build && npm run tsc && mocha --forbid-only -R spec -g \".*\"" }, "husky": { "hooks": { - "pre-push": "eslint --cache . --ext .js,.ts" + "pre-push": "npm run lint" } }, "bin": "./pokemon-showdown", @@ -68,7 +68,6 @@ "@types/nodemailer": "^6.4.0", "@types/sockjs": "^0.3.31", "@typescript-eslint/eslint-plugin": "^2.19.0", - "@typescript-eslint/eslint-plugin-tslint": "^2.20.0", "@typescript-eslint/parser": "^2.19.0", "eslint": "^6.8.0", "eslint-plugin-import": "^2.20.1", diff --git a/server/chat-commands/admin.ts b/server/chat-commands/admin.ts index 838adb248988..ebb019401060 100644 --- a/server/chat-commands/admin.ts +++ b/server/chat-commands/admin.ts @@ -237,7 +237,7 @@ export const commands: ChatCommands = { (Monitor.hotpatchVersions[patch] ? Monitor.hotpatchVersions[patch] === version : (global.__version && version === global.__version.tree)); - const requiresForceMessage = `The git work tree has not changed since the last time ${target} was hotpatched (${version && version.slice(0, 8)}), use /forcehotpatch ${target} if you wish to hotpatch anyway.`; + const requiresForceMessage = `The git work tree has not changed since the last time ${target} was hotpatched (${version?.slice(0, 8)}), use /forcehotpatch ${target} if you wish to hotpatch anyway.`; let patch = target; try { @@ -798,7 +798,7 @@ export const commands: ChatCommands = { let log = `[o] ${stdout}[e] ${stderr}`; if (error) log = `[c] ${error.code}\n${log}`; logRoom.roomlog(log); - resolve([error && error.code || 0, stdout, stderr]); + resolve([error?.code || 0, stdout, stderr]); }); }); } @@ -882,7 +882,7 @@ export const commands: ChatCommands = { let log = `[o] ${stdout}[e] ${stderr}`; if (error) log = `[c] ${error.code}\n${log}`; logRoom.roomlog(log); - resolve([error && error.code || 0, stdout, stderr]); + resolve([error?.code || 0, stdout, stderr]); }); }); } @@ -926,7 +926,7 @@ export const commands: ChatCommands = { const me = user; // tslint:disable-next-line: no-eval let result = eval(target); - if (result && result.then) { + if (result?.then) { result = `Promise -> ${Chat.stringify(await result)}`; } else { result = Chat.stringify(result); diff --git a/server/chat-commands/room-settings.ts b/server/chat-commands/room-settings.ts index 023a41171ef6..117b5a351772 100644 --- a/server/chat-commands/room-settings.ts +++ b/server/chat-commands/room-settings.ts @@ -135,7 +135,7 @@ export const commands: ChatCommands = { inviteonlynext: 'ionext', ionext(target, room, user) { const groupConfig = Config.groups[Users.PLAYER_SYMBOL]; - if (!(groupConfig && groupConfig.editprivacy)) return this.errorReply(`/ionext - Access denied.`); + if (!groupConfig?.editprivacy) return this.errorReply(`/ionext - Access denied.`); if (this.meansNo(target)) { user.inviteOnlyNextBattle = false; user.update('inviteOnlyNextBattle'); @@ -1043,7 +1043,7 @@ export const commands: ChatCommands = { } const parent = room.parent; - if (parent && parent.subRooms) { + if (parent?.subRooms) { parent.subRooms.delete(room.roomid); if (!parent.subRooms.size) parent.subRooms = null; } diff --git a/server/chat-plugins/chat-monitor.ts b/server/chat-plugins/chat-monitor.ts index 73de59834d86..9b95e0d8c9c2 100644 --- a/server/chat-plugins/chat-monitor.ts +++ b/server/chat-plugins/chat-monitor.ts @@ -291,6 +291,8 @@ void FS(MONITOR_FILE).readIfExists().then(data => { } }); +/* The sucrase transformation of optional chaining is too expensive to be used in a hot function like this. */ +/* eslint-disable @typescript-eslint/prefer-optional-chain */ export const chatfilter: ChatFilter = function (message, user, room) { let lcMessage = message .replace(/\u039d/g, 'N').toLowerCase() @@ -339,6 +341,7 @@ export const chatfilter: ChatFilter = function (message, user, room) { return message; }; +/* eslint-enable @typescript-eslint/prefer-optional-chain */ export const namefilter: NameFilter = (name, user) => { const id = toID(name); diff --git a/server/chat-plugins/datasearch.ts b/server/chat-plugins/datasearch.ts index 2c4eeb430fcd..692b7cecb2bc 100644 --- a/server/chat-plugins/datasearch.ts +++ b/server/chat-plugins/datasearch.ts @@ -82,7 +82,7 @@ export const commands: ChatCommands = { return runSearch({ tar: target, cmd: 'dexsearch', - canAll: (!this.broadcastMessage || (room && room.isPersonal)), + canAll: (!this.broadcastMessage || room?.isPersonal), message: (this.broadcastMessage ? "" : message), }).then(response => { if (!this.runBroadcast()) return; @@ -146,7 +146,7 @@ export const commands: ChatCommands = { return runSearch({ tar: targetsBuffer.join(","), cmd: 'randmove', - canAll: (!this.broadcastMessage || (room && room.isPersonal)), + canAll: (!this.broadcastMessage || room?.isPersonal), message: (this.broadcastMessage ? "" : message), }).then(response => { if (!this.runBroadcast(true)) return; @@ -192,7 +192,7 @@ export const commands: ChatCommands = { return runSearch({ tar: targetsBuffer.join(","), cmd: 'randpoke', - canAll: (!this.broadcastMessage || (room && room.isPersonal)), + canAll: (!this.broadcastMessage || room?.isPersonal), message: (this.broadcastMessage ? "" : message), }).then(response => { if (!this.runBroadcast(true)) return; @@ -235,7 +235,7 @@ export const commands: ChatCommands = { return runSearch({ tar: target, cmd: 'movesearch', - canAll: (!this.broadcastMessage || (room && room.isPersonal)), + canAll: (!this.broadcastMessage || room?.isPersonal), message: (this.broadcastMessage ? "" : message), }).then(response => { if (!this.runBroadcast()) return; @@ -282,7 +282,7 @@ export const commands: ChatCommands = { return runSearch({ tar: target, cmd: 'itemsearch', - canAll: (!this.broadcastMessage || (room && room.isPersonal)), + canAll: (!this.broadcastMessage || room?.isPersonal), message: (this.broadcastMessage ? "" : message), }).then(response => { if (!this.runBroadcast()) return; @@ -323,7 +323,7 @@ export const commands: ChatCommands = { return runSearch({ tar: target, cmd: 'learn', - canAll: (!this.broadcastMessage || (room && room.isPersonal)), + canAll: (!this.broadcastMessage || room?.isPersonal), message: cmd, }).then(response => { if (!this.runBroadcast()) return; diff --git a/server/chat-plugins/helptickets.ts b/server/chat-plugins/helptickets.ts index 6728dcc65397..40213ec64634 100644 --- a/server/chat-plugins/helptickets.ts +++ b/server/chat-plugins/helptickets.ts @@ -569,7 +569,7 @@ export const pages: PageTable = { if (banMsg) return connection.popup(banMsg); let ticket = tickets[user.id]; const ipTicket = checkIp(user.latestIp); - if ((ticket && ticket.open) || ipTicket) { + if (ticket?.open || ipTicket) { if (!ticket && ipTicket) ticket = ipTicket; const helpRoom = Rooms.get(`help-${ticket.userid}`); if (!helpRoom) { @@ -1033,7 +1033,7 @@ export const commands: ChatCommands = { if (!this.runBroadcast()) return; const meta = this.pmTarget ? `-user-${this.pmTarget.id}` : this.room ? `-room-${this.room.roomid}` : ''; if (this.broadcasting) { - if (room && room.battle) return this.errorReply(`This command cannot be broadcast in battles.`); + if (room?.battle) return this.errorReply(`This command cannot be broadcast in battles.`); return this.sendReplyBox(``); } @@ -1045,7 +1045,7 @@ export const commands: ChatCommands = { if (!this.runBroadcast()) return; const meta = this.pmTarget ? `-user-${this.pmTarget.id}` : this.room ? `-room-${this.room.roomid}` : ''; if (this.broadcasting) { - if (room && room.battle) return this.errorReply(`This command cannot be broadcast in battles.`); + if (room?.battle) return this.errorReply(`This command cannot be broadcast in battles.`); return this.sendReplyBox(``); } @@ -1082,7 +1082,7 @@ export const commands: ChatCommands = { if (banMsg) return this.popupReply(banMsg); let ticket = tickets[user.id]; const ipTicket = checkIp(user.latestIp); - if ((ticket && ticket.open) || ipTicket) { + if (ticket?.open || ipTicket) { if (!ticket && ipTicket) ticket = ipTicket; const helpRoom = Rooms.get(`help-${ticket.userid}`); if (!helpRoom) { @@ -1342,7 +1342,7 @@ export const commands: ChatCommands = { for (const userObj of affected) { const userObjID = (typeof userObj !== 'string' ? userObj.getLastId() : toID(userObj)); const targetTicket = tickets[userObjID]; - if (targetTicket && targetTicket.open) targetTicket.open = false; + if (targetTicket?.open) targetTicket.open = false; const helpRoom = Rooms.get(`help-${userObjID}`); if (helpRoom) { const ticketGame = helpRoom.getGame(HelpTicket)!; diff --git a/server/chat-plugins/lottery.ts b/server/chat-plugins/lottery.ts index 2280964f25fe..3e8be73a281c 100644 --- a/server/chat-plugins/lottery.ts +++ b/server/chat-plugins/lottery.ts @@ -20,8 +20,8 @@ function createLottery(roomid: RoomID, maxWinners: number, name: string, markup: } const lottery = lotteries[roomid]; lotteries[roomid] = { - maxWinners, name, markup, participants: (lottery && lottery.participants) || Object.create(null), - winners: (lottery && lottery.winners) || [], running: true, + maxWinners, name, markup, participants: lottery?.participants || Object.create(null), + winners: lottery?.winners || [], running: true, }; writeLotteries(); } @@ -100,7 +100,7 @@ export const commands: ChatCommands = { return this.errorReply('This room does not support the creation of lotteries.'); } const lottery = lotteries[room.roomid]; - const edited = lottery && lottery.running; + const edited = lottery?.running; if (cmd === 'edit' && !target && lottery) { this.sendReply('Source:'); const markup = Chat.html`${lottery.markup}`.replace(/\n/g, '
'); @@ -178,7 +178,7 @@ export const commands: ChatCommands = { join(target, room, user) { // This hack is used for the HTML room to be able to // join lotteries in other rooms from the global room - const roomid = target || (room && room.roomid); + const roomid = target || room?.roomid; if (!roomid) { return this.errorReply(`This is not a valid room.`); } @@ -209,7 +209,7 @@ export const commands: ChatCommands = { leave(target, room, user) { // This hack is used for the HTML room to be able to // join lotteries in other rooms from the global room - const roomid = target || (room && room.roomid); + const roomid = target || room?.roomid; if (!roomid) { return this.errorReply('This can only be used in rooms.'); } diff --git a/server/chat-plugins/mafia.ts b/server/chat-plugins/mafia.ts index 31ab730025a1..908e6515a508 100644 --- a/server/chat-plugins/mafia.ts +++ b/server/chat-plugins/mafia.ts @@ -606,7 +606,7 @@ class MafiaTracker extends Rooms.RoomGame { const role = roles.shift()!; this.playerTable[p].role = role; const u = Users.get(p); - if (u && u.connected) { + if (u?.connected) { u.send(`>${this.room.roomid}\n|notify|Your role is ${role.safeName}. For more details of your role, check your Role PM.`); } } @@ -657,7 +657,7 @@ class MafiaTracker extends Rooms.RoomGame { this.phase = 'night'; for (const hostid of [...this.cohosts, this.hostid]) { const host = Users.get(hostid); - if (host && host.connected) host.send(`>${this.room.roomid}\n|notify|It's night in your game of Mafia!`); + if (host?.connected) host.send(`>${this.room.roomid}\n|notify|It's night in your game of Mafia!`); } this.sendDeclare(`Night ${this.dayNum}. PM the host your action, or idle.`); const hasPlurality = this.getPlurality(); @@ -1157,7 +1157,7 @@ class MafiaTracker extends Rooms.RoomGame { if (this.dead[p].restless && this.dead[p].lynching === oldPlayer.id) this.dead[p].lynching = newPlayer.id; } } - if (newUser && newUser.connected) { + if (newUser?.connected) { for (const conn of newUser.connections) { void Chat.resolvePage(`view-mafia-${this.room.roomid}`, newUser, conn); } @@ -1273,7 +1273,7 @@ class MafiaTracker extends Rooms.RoomGame { this.IDEA.waitingPick.push(p); } const u = Users.get(p); - if (u && u.connected) u.send(`>${this.room.roomid}\n|notify|Pick your role in the IDEA module.`); + if (u?.connected) u.send(`>${this.room.roomid}\n|notify|Pick your role in the IDEA module.`); } this.phase = 'IDEApicking'; @@ -1689,7 +1689,7 @@ export const pages: PageTable = { let roomid = query.shift(); if (roomid === 'groupchat') roomid += `-${query.shift()}-${query.shift()}`; const room = Rooms.get(roomid); - const game = room && room.getGame(MafiaTracker); + const game = room?.getGame(MafiaTracker); if (!room || !room.users[user.id] || !game || game.ended) { return this.close(); } @@ -2622,7 +2622,7 @@ export const commands: ChatCommands = { uninsomniac: 'nighttalk', unnighttalk: 'nighttalk', nighttalk(target, room, user, connection, cmd) { - const game = room && room.getGame(MafiaTracker); + const game = room?.getGame(MafiaTracker); if (!game) return this.errorReply(`There is no game of mafia running in this room.`); if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return; if (!game.started) return this.errorReply(`The game has not started yet.`); @@ -2645,7 +2645,7 @@ export const commands: ChatCommands = { unactor: 'priest', unpriest: 'priest', priest(target, room, user, connection, cmd) { - const game = room && room.getGame(MafiaTracker); + const game = room?.getGame(MafiaTracker); if (!game) return this.errorReply(`There is no game of mafia running in this room.`); if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return; if (!game.started) return this.errorReply(`The game has not started yet.`); @@ -2822,7 +2822,7 @@ export const commands: ChatCommands = { spectate: 'view', view(target, room, user, connection) { - const game = room && room.getGame(MafiaTracker); + const game = room?.getGame(MafiaTracker); if (!game) return this.errorReply(`There is no game of mafia running in this room.`); if (!this.runBroadcast()) return; if (this.broadcasting) { @@ -3100,7 +3100,7 @@ export const commands: ChatCommands = { term: 'data', dt: 'data', data(target, room, user, connection, cmd) { - if (room && room.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`); + if (room?.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`); if (cmd === 'role' && !target && room) { // Support /mafia role showing your current role if you're in a game const game = room.getGame(MafiaTracker); @@ -3114,7 +3114,7 @@ export const commands: ChatCommands = { } // hack to let hosts broadcast - const game = room && room.getGame(MafiaTracker); + const game = room?.getGame(MafiaTracker); if (game && (game.hostid === user.id || game.cohosts.includes(user.id))) { this.broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); } diff --git a/server/chat-plugins/modlog.ts b/server/chat-plugins/modlog.ts index 37cd4cf2402c..07d2cc6f03a3 100644 --- a/server/chat-plugins/modlog.ts +++ b/server/chat-plugins/modlog.ts @@ -499,7 +499,7 @@ export const pages: PageTable = { }).map(tier => { // Use the official tier name const format = Dex.getFormat(tier); - if (format && format.exists) tier = format.name; + if (format?.exists) tier = format.name; // Otherwise format as best as possible if (tier.substring(0, 3) === 'gen') { return `[Gen ${tier.substring(3, 4)}] ${tier.substring(4)}`; diff --git a/server/chat-plugins/poll.ts b/server/chat-plugins/poll.ts index 67cb19281ffa..8d206be73912 100644 --- a/server/chat-plugins/poll.ts +++ b/server/chat-plugins/poll.ts @@ -147,7 +147,7 @@ export class Poll { let c = 0; const colors = ['#79A', '#8A8', '#88B']; while (!i.done) { - const selected = option && option.includes(i.value[0]); + const selected = option?.includes(i.value[0]); const percentage = Math.round((i.value[1].votes * 100) / (this.totalVotes || 1)); const answerMarkup = this.isQuiz ? diff --git a/server/chat-plugins/room-events.ts b/server/chat-plugins/room-events.ts index f8ba7df0ee25..dd986adb7b63 100644 --- a/server/chat-plugins/room-events.ts +++ b/server/chat-plugins/room-events.ts @@ -94,7 +94,7 @@ export const commands: ChatCommands = { } for (const u in room.users) { const activeUser = Users.get(u); - if (activeUser && activeUser.connected) { + if (activeUser?.connected) { activeUser.sendTo( room, Chat.html`|notify|A new roomevent in ${room.title} has started!|` + diff --git a/server/chat.ts b/server/chat.ts index 99587af46abd..8b2ffb108f58 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -790,6 +790,8 @@ export class CommandContext extends MessageContext { return true; } + /* The sucrase transformation of optional chaining is too expensive to be used in a hot function like this. */ + /* eslint-disable @typescript-eslint/prefer-optional-chain */ canTalk(message: string, room?: GameRoom | ChatRoom | null, targetUser?: User | null): string | null; canTalk(message?: null, room?: GameRoom | ChatRoom | null, targetUser?: User | null): true | null; canTalk(message: string | null = null, room: GameRoom | ChatRoom | null = null, targetUser: User | null = null) { @@ -921,14 +923,14 @@ export class CommandContext extends MessageContext { const allLinksWhitelisted = !links || links.every(link => { link = link.toLowerCase(); const domainMatches = /^(?:http:\/\/|https:\/\/)?(?:[^/]*\.)?([^/.]*\.[^/.]*)\.?($|\/|:)/.exec(link); - const domain = domainMatches && domainMatches[1]; + const domain = domainMatches?.[1]; const hostMatches = /^(?:http:\/\/|https:\/\/)?([^/]*[^/.])\.?($|\/|:)/.exec(link); - let host = hostMatches && hostMatches[1]; - if (host && host.startsWith('www.')) host = host.slice(4); + let host = hostMatches?.[1]; + if (host?.startsWith('www.')) host = host.slice(4); if (!domain || !host) return null; return LINK_WHITELIST.includes(host) || LINK_WHITELIST.includes(`*.${domain}`); }); - if (!allLinksWhitelisted && !(targetUser && targetUser.can('lock') || (room && room.isHelp))) { + if (!allLinksWhitelisted && !(targetUser?.can('lock') || room?.isHelp)) { this.errorReply("Your account must be autoconfirmed to send links to other users, except for global staff."); return null; } @@ -974,7 +976,7 @@ export class CommandContext extends MessageContext { user.lastMessageTime = Date.now(); } - if (room && room.highTraffic && + if (room?.highTraffic && toID(message).replace(/[^a-z]+/, '').length < 2 && !user.can('broadcast', null, room)) { this.errorReply( @@ -989,6 +991,7 @@ export class CommandContext extends MessageContext { return message; } + /* eslint-enable @typescript-eslint/prefer-optional-chain */ canEmbedURI(uri: string, isRelative = false) { if (uri.startsWith('https://')) return uri; if (uri.startsWith('//')) return uri; @@ -1660,8 +1663,8 @@ export const Chat = new class { const roundingBoundaries = [6, 15, 12, 30, 30]; const unitNames = ["second", "minute", "hour", "day", "month", "year"]; const positiveIndex = parts.findIndex(elem => elem > 0); - const precision = (options && options.precision ? options.precision : parts.length); - if (options && options.hhmmss) { + const precision = (options?.precision ? options.precision : parts.length); + if (options?.hhmmss) { const str = parts.slice(positiveIndex).map(value => value < 10 ? "0" + value : "" + value).join(":"); return str.length === 2 ? "00:" + str : str; } diff --git a/server/ladders-local.ts b/server/ladders-local.ts index d5f7610667fe..cac8bd88e26d 100644 --- a/server/ladders-local.ts +++ b/server/ladders-local.ts @@ -150,7 +150,7 @@ export class LadderStore { async getRating(userid: string) { const formatid = this.formatid; const user = Users.getExact(userid); - if (user && user.mmrCache[formatid]) { + if (user?.mmrCache[formatid]) { return user.mmrCache[formatid]; } const ladder = await this.getLadder(); diff --git a/server/ladders-remote.ts b/server/ladders-remote.ts index ed2bf68add48..b989fe42f672 100644 --- a/server/ladders-remote.ts +++ b/server/ladders-remote.ts @@ -25,8 +25,10 @@ export class LadderStore { * ladder toplist, to be displayed directly in the ladder tab of the * client. */ + // This requires to be `async` because it must conform with the `LadderStore` interface + // eslint-disable-next-line @typescript-eslint/require-await async getTop(prefix?: string): Promise<[string, string] | null> { - return new Promise(resolve => resolve(null)); + return null; } /** @@ -35,7 +37,7 @@ export class LadderStore { async getRating(userid: string) { const formatid = this.formatid; const user = Users.getExact(userid); - if (user && user.mmrCache[formatid]) { + if (user?.mmrCache[formatid]) { return user.mmrCache[formatid]; } const [data] = await LoginServer.request('mmr', { @@ -141,9 +143,9 @@ export class LadderStore { /** * Returns a Promise for an array of strings of s for ladder ratings of the user */ + // This requires to be `async` because it must conform with the `LadderStore` interface + // eslint-disable-next-line @typescript-eslint/require-await static async visualizeAll(username: string) { - return new Promise(resolve => { - resolve([`Please use the official client at play.pokemonshowdown.com`]); - }); + return [`Please use the official client at play.pokemonshowdown.com`]; } } diff --git a/server/ladders.ts b/server/ladders.ts index 5260ccbaf39d..61537730c5c8 100644 --- a/server/ladders.ts +++ b/server/ladders.ts @@ -343,7 +343,7 @@ class Ladder extends LadderStore { if (!user || !user.connected || user.id !== search.userid) { const formatTable = Ladders.searches.get(formatid); if (formatTable) formatTable.delete(search.userid); - if (user && user.connected) { + if (user?.connected) { user.popup(`You changed your name and are no longer looking for a battle in ${formatid}`); Ladder.updateSearch(user); } diff --git a/server/monitor.ts b/server/monitor.ts index e17142865c90..796c98e0b3b5 100644 --- a/server/monitor.ts +++ b/server/monitor.ts @@ -285,7 +285,7 @@ export const Monitor = new class { sh(command: string, options: ExecOptions = {}): Promise<[number, string, string]> { return new Promise((resolve, reject) => { exec(command, options, (error: ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => { - resolve([error && error.code || 0, '' + stdout, '' + stderr]); + resolve([error?.code || 0, '' + stdout, '' + stderr]); }); }); } diff --git a/server/punishments.ts b/server/punishments.ts index 49898ffbce41..8e394027af2d 100644 --- a/server/punishments.ts +++ b/server/punishments.ts @@ -683,7 +683,7 @@ export const Punishments = new class { const roomObject = Rooms.get(room); const userObject = Users.get(user); - if (roomObject && roomObject.battle && userObject && userObject.connections[0]) { + if (roomObject?.battle && userObject && userObject.connections[0]) { Chat.parse('/savereplay forpunishment', roomObject, userObject, userObject.connections[0]); } } @@ -691,7 +691,7 @@ export const Punishments = new class { const user = Users.get(name); let id: string = toID(name); const success: string[] = []; - if (user && user.locked && !user.namelocked) { + if (user?.locked && !user.namelocked) { id = user.locked; user.locked = null; user.namelocked = null; @@ -737,10 +737,10 @@ export const Punishments = new class { const user = Users.get(name); let id: string = toID(name); const success: string[] = []; - if (user && user.namelocked) name = user.namelocked; + if (user?.namelocked) name = user.namelocked; const unpunished = Punishments.unpunish(name, 'NAMELOCK'); - if (user && user.locked) { + if (user?.locked) { id = user.locked; user.locked = null; user.namelocked = null; @@ -1245,7 +1245,7 @@ export const Punishments = new class { if (punishment) { const user = Users.get(userid); - if (user && user.permalocked) return ` (never expires; you are permalocked)`; + if (user?.permalocked) return ` (never expires; you are permalocked)`; const expiresIn = new Date(punishment[2]).getTime() - Date.now(); const expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24); let expiresText = ''; @@ -1314,7 +1314,7 @@ export const Punishments = new class { if (punishment) { punishments.push([curRoom, punishment]); continue; - } else if (options && options.checkIps) { + } else if (options?.checkIps) { if (typeof user !== 'string') { for (const ip in user.ips) { punishment = Punishments.roomIps.nestedGet(curRoom.roomid, ip); @@ -1385,7 +1385,7 @@ export const Punishments = new class { }); if (roomid && ignoreMutes !== false) { const room = Rooms.get(roomid); - if (room && room.muteQueue) { + if (room?.muteQueue) { for (const mute of room.muteQueue) { punishmentTable.set(mute.userid, { userids: [], ips: [], punishType: "MUTE", expireTime: mute.time, reason: "", rest: [], diff --git a/server/room-battle.ts b/server/room-battle.ts index 5b35aefea452..8d24074fb6bf 100644 --- a/server/room-battle.ts +++ b/server/room-battle.ts @@ -193,7 +193,7 @@ export class RoomBattleTimer { const hasLongTurns = Dex.getFormat(battle.format, true).gameType !== 'singles'; const isChallenge = (!battle.rated && !battle.room.tour); const timerEntry = Dex.getRuleTable(Dex.getFormat(battle.format, true)).timer; - const timerSettings = timerEntry && timerEntry[0]; + const timerSettings = timerEntry?.[0]; // so that Object.assign doesn't overwrite anything with `undefined` for (const k in timerSettings) { @@ -380,7 +380,7 @@ export class RoomBattleTimer { checkActivity() { if (this.battle.ended) return; for (const player of this.battle.players) { - const isConnected = !!(player && player.active); + const isConnected = !!player?.active; if (isConnected === player.connected) continue; @@ -820,13 +820,13 @@ export class RoomBattle extends RoomGames.RoomGame { // reflect any changes that may have been made to the replay's hidden status). if (this.replaySaved || Config.autosavereplays) { const uploader = Users.get(winnerid || p1id); - if (uploader && uploader.connections[0]) { + if (uploader?.connections[0]) { Chat.parse('/savereplay silent', this.room, uploader, uploader.connections[0]); } } const parentGame = this.room.parent && this.room.parent.game; // @ts-ignore - Tournaments aren't TS'd yet - if (parentGame && parentGame.onBattleWin) { + if (parentGame?.onBattleWin) { // @ts-ignore parentGame.onBattleWin(this.room, winnerid); } @@ -947,7 +947,7 @@ export class RoomBattle extends RoomGames.RoomGame { } onLeave(user: User, oldUserid?: ID) { const player = this.playerTable[oldUserid || user.id]; - if (player && player.active) { + if (player?.active) { player.sendRoom(`|request|null`); player.active = false; this.timer.checkActivity(); @@ -1011,7 +1011,7 @@ export class RoomBattle extends RoomGames.RoomGame { } if (user) this.room.auth[user.id] = Users.PLAYER_SYMBOL; - if (user && user.inRooms.has(this.roomid)) this.onConnect(user); + if (user?.inRooms.has(this.roomid)) this.onConnect(user); return player; } @@ -1019,7 +1019,7 @@ export class RoomBattle extends RoomGames.RoomGame { if (!this.rated) return; for (const player of this.players) { const user = player.getUser(); - if (user && user.forcedPublic) return user.forcedPublic; + if (user?.forcedPublic) return user.forcedPublic; } } @@ -1134,7 +1134,7 @@ export class RoomBattleStream extends BattleStream { this.push(`update\n|html|
The battle crashed
Don't worry, we're working on fixing it.
`); if (battle) { for (const side of battle.sides) { - if (side && side.requestState) { + if (side?.requestState) { this.push(`sideupdate\n${side.id}\n|error|[Invalid choice] The battle crashed`); } } @@ -1151,21 +1151,21 @@ export class RoomBattleStream extends BattleStream { switch (type) { case 'eval': const battle = this.battle; - const p1 = battle && battle.sides[0]; - const p2 = battle && battle.sides[1]; - const p3 = battle && battle.sides[2]; - const p4 = battle && battle.sides[3]; - const p1active = p1 && p1.active[0]; - const p2active = p2 && p2.active[0]; - const p3active = p3 && p3.active[0]; - const p4active = p4 && p4.active[0]; + const p1 = battle?.sides[0]; + const p2 = battle?.sides[1]; + const p3 = battle?.sides[2]; + const p4 = battle?.sides[3]; + const p1active = p1?.active[0]; + const p2active = p2?.active[0]; + const p3active = p3?.active[0]; + const p4active = p4?.active[0]; battle.inputLog.push(`>${type} ${message}`); message = message.replace(/\f/g, '\n'); battle.add('', '>>> ' + message.replace(/\n/g, '\n||')); try { // tslint:disable-next-line: no-eval let result = eval(message); - if (result && result.then) { + if (result?.then) { result.then((unwrappedResult: any) => { unwrappedResult = Chat.stringify(unwrappedResult); battle.add('', 'Promise -> ' + unwrappedResult); diff --git a/server/rooms.ts b/server/rooms.ts index dae5009ad197..d44e4a5dd8e0 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -1330,7 +1330,7 @@ export class BasicChatRoom extends BasicRoom { * onRename, but without a userid change */ onUpdateIdentity(user: User) { - if (user && user.connected) { + if (user?.connected) { if (!this.users[user.id]) return false; if (user.named) { this.reportJoin('n', user.getIdentityWithStatus(this.roomid) + '|' + user.id, user); @@ -1632,7 +1632,7 @@ export class GameRoom extends BasicChatRoom { inputlog: battle.inputLog?.join('\n') || null, }); if (success) battle.replaySaved = true; - if (success && success.errorip) { + if (success?.errorip) { connection.popup(`This server's request IP ${success.errorip} is not a registered server.`); return; } diff --git a/server/team-validator-async.ts b/server/team-validator-async.ts index 45c5e1aa4ae4..d9fdfcb94c0e 100644 --- a/server/team-validator-async.ts +++ b/server/team-validator-async.ts @@ -55,7 +55,7 @@ export const PM = new QueryProcessManager(module, message => { ]; } - if (problems && problems.length) { + if (problems?.length) { return '0' + problems.join('\n'); } const packedTeam = Dex.packTeam(parsedTeam); diff --git a/server/tournaments/index.ts b/server/tournaments/index.ts index e261eb46291c..b2993eec798f 100644 --- a/server/tournaments/index.ts +++ b/server/tournaments/index.ts @@ -488,7 +488,7 @@ export class Tournament extends Rooms.RoomGame { } } } - if (matchPlayer && matchPlayer.inProgressMatch) { + if (matchPlayer?.inProgressMatch) { matchPlayer.inProgressMatch.to.isBusy = false; matchPlayer.isBusy = false; @@ -812,11 +812,11 @@ export class Tournament extends Rooms.RoomGame { player.autoDisqualifyWarned = false; continue; } - if (pendingChallenge && pendingChallenge.to) continue; + if (pendingChallenge?.to) continue; if (now > time + this.autoDisqualifyTimeout && player.autoDisqualifyWarned) { let reason; - if (pendingChallenge && pendingChallenge.from) { + if (pendingChallenge?.from) { reason = "You failed to accept your opponent's challenge in time."; } else { reason = "You failed to challenge your opponent in time."; @@ -1066,7 +1066,7 @@ export class Tournament extends Rooms.RoomGame { if (this.generator.isTournamentEnded()) { if (!this.room.isPrivate && this.generator.name.includes('Elimination') && !Config.autosavereplays) { const uploader = Users.get(winnerid); - if (uploader && uploader.connections[0]) { + if (uploader?.connections[0]) { Chat.parse('/savereplay', room, uploader, uploader.connections[0]); } } diff --git a/server/users.ts b/server/users.ts index a03b610f79a5..f9c4cfd6f3ba 100644 --- a/server/users.ts +++ b/server/users.ts @@ -639,7 +639,7 @@ export class User extends Chat.MessageContext { if (this.hasSysopAccess()) return true; let groupData = Config.groups[this.group]; - if (groupData && groupData['root']) { + if (groupData?.['root']) { return true; } @@ -672,7 +672,7 @@ export class User extends Chat.MessageContext { } } - if (groupData && groupData[permission]) { + if (groupData?.[permission]) { const jurisdiction = groupData[permission]; if (!targetUser && !targetGroup) { return !!jurisdiction; @@ -924,7 +924,7 @@ export class User extends Chat.MessageContext { let user = users.get(userid); const possibleUser = Users.get(userid); - if (possibleUser && possibleUser.namelocked) { + if (possibleUser?.namelocked) { // allows namelocked users to be merged user = possibleUser; } @@ -1173,7 +1173,7 @@ export class User extends Chat.MessageContext { this.isStaff = Config.groups[this.group] && (Config.groups[this.group].lock || Config.groups[this.group].root); if (!this.isStaff) { const staffRoom = Rooms.get('staff'); - this.isStaff = !!(staffRoom && staffRoom.auth && staffRoom.auth[this.id]); + this.isStaff = !!staffRoom?.auth?.[this.id]; } if (this.trusted) { if (this.locked && this.permalocked) { @@ -1204,7 +1204,7 @@ export class User extends Chat.MessageContext { this.isStaff = Config.groups[this.group] && (Config.groups[this.group].lock || Config.groups[this.group].root); if (!this.isStaff) { const staffRoom = Rooms.get('staff'); - this.isStaff = !!(staffRoom && staffRoom.auth && staffRoom.auth[this.id]); + this.isStaff = !!(staffRoom?.auth?.[this.id]); } if (wasStaff !== this.isStaff) this.update('isStaff'); Rooms.global.checkAutojoin(this); diff --git a/sim/battle.ts b/sim/battle.ts index 580b5364c794..4d3e90371315 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -1099,7 +1099,7 @@ export class Battle { getMaxTeamSize() { const teamLengthData = this.format.teamLength; - return (teamLengthData && teamLengthData.battle) || 6; + return teamLengthData?.battle || 6; } getRequests(type: RequestState, maxTeamSize: number) { @@ -1112,7 +1112,7 @@ export class Battle { const side = this.sides[i]; const switchTable = []; for (const pokemon of side.active) { - switchTable.push(!!(pokemon && pokemon.switchFlag)); + switchTable.push(!!pokemon?.switchFlag); } if (switchTable.some(flag => flag === true)) { requests[i] = {forceSwitch: switchTable, side: side.getRequestData()}; @@ -1639,7 +1639,7 @@ export class Battle { } if (boostBy) { success = true; - switch (effect && effect.id) { + switch (effect?.id) { case 'bellydrum': this.add('-setboost', target, 'atk', target.boosts['atk'], '[from] move: Belly Drum'); break; @@ -1809,7 +1809,7 @@ export class Battle { if (this.gen <= 1 && this.dex.currentMod !== 'stadium' && ['confusion', 'jumpkick', 'highjumpkick'].includes(effect.id) && target.volatiles['substitute']) { const hint = "In Gen 1, if a Pokemon with a Substitute hurts itself due to confusion or Jump Kick/Hi Jump Kick recoil and the target"; - if (source && source.volatiles['substitute']) { + if (source?.volatiles['substitute']) { source.volatiles['substitute'].hp -= damage; if (source.volatiles['substitute'].hp <= 0) { source.removeVolatile('substitute'); @@ -1857,7 +1857,7 @@ export class Battle { if (!target.isActive) return false; if (target.hp >= target.maxhp) return false; const finalDamage = target.heal(damage, source, effect); - switch (effect && effect.id) { + switch (effect?.id) { case 'leechseed': case 'rest': this.add('-heal', target, target.getHealth, '[silent]'); @@ -2245,7 +2245,7 @@ export class Battle { } if (move.target !== 'randomNormal' && this.validTargetLoc(targetLoc, pokemon, move.target)) { const target = this.getAtLoc(pokemon, targetLoc); - if (target && target.fainted && target.side === pokemon.side) { + if (target?.fainted && target.side === pokemon.side) { // Target is a fainted ally: attack shouldn't retarget return target; } diff --git a/sim/dex-data.ts b/sim/dex-data.ts index d12340a3735d..a3bd0c56db35 100644 --- a/sim/dex-data.ts +++ b/sim/dex-data.ts @@ -37,6 +37,8 @@ export class Tools { * Dex.getId is generally assigned to the global toID, because of how * commonly it's used. */ + /* The sucrase transformation of optional chaining is too expensive to be used in a hot function like this. */ + /* eslint-disable @typescript-eslint/prefer-optional-chain */ static getId(text: any): ID { if (text && text.id) { text = text.id; @@ -48,6 +50,7 @@ export class Tools { if (typeof text !== 'string' && typeof text !== 'number') return ''; return ('' + text).toLowerCase().replace(/[^a-z0-9]+/g, '') as ID; } + /* eslint-enable @typescript-eslint/prefer-optional-chain */ } const toID = Tools.getId; diff --git a/sim/dex.ts b/sim/dex.ts index a7a810946067..df4897c81bed 100644 --- a/sim/dex.ts +++ b/sim/dex.ts @@ -873,7 +873,7 @@ export class ModdedDex { continue; } const subformat = this.getFormat(ruleSpec); - if (repeals && repeals.has(subformat.id)) { + if (repeals?.has(subformat.id)) { repeals.set(subformat.id, -Math.abs(repeals.get(subformat.id)!)); continue; } @@ -888,7 +888,7 @@ export class ModdedDex { const subRuleTable = this.getRuleTable(subformat, depth + 1, repeals); for (const [k, v] of subRuleTable) { // don't check for "already exists" here; multiple inheritance is allowed - if (!(repeals && repeals.has(k))) { + if (!repeals?.has(k)) { ruleTable.set(k, v || subformat.name); } } @@ -937,7 +937,7 @@ export class ModdedDex { switch (rule.charAt(0)) { case '-': case '+': - if (format && format.team) throw new Error(`We don't currently support bans in generated teams`); + if (format?.team) throw new Error(`We don't currently support bans in generated teams`); if (rule.slice(1).includes('>') || rule.slice(1).includes('+')) { let buf = rule.slice(1); const gtIndex = buf.lastIndexOf('>'); @@ -1118,7 +1118,7 @@ export class ModdedDex { } generateTeam(format: Format | string, options: PlayerOptions | null = null): PokemonSet[] { - return this.getTeamGenerator(format, options && options.seed).getTeam(options); + return this.getTeamGenerator(format, options?.seed).getTeam(options); } dataSearch(target: string, searchIn?: DataType[] | null, isInexact?: boolean): AnyObject[] | false { diff --git a/sim/field.ts b/sim/field.ts index 643b5ebc06cb..35a3e7b202ea 100644 --- a/sim/field.ts +++ b/sim/field.ts @@ -51,7 +51,7 @@ export class Field { const result = this.battle.runEvent('SetWeather', source, source, status); if (!result) { if (result === false) { - if (sourceEffect && sourceEffect.weather) { + if (sourceEffect?.weather) { this.battle.add('-fail', source, sourceEffect, '[from] ' + this.weather); } else if (sourceEffect && sourceEffect.effectType === 'Ability') { this.battle.add('-ability', source, sourceEffect, '[from] ' + this.weather, '[fail]'); @@ -193,7 +193,7 @@ export class Field { effectData = this.pseudoWeather[status.id] = { id: status.id, source, - sourcePosition: source && source.position, + sourcePosition: source?.position, duration: status.duration, }; if (status.durationCallback) { diff --git a/sim/pokemon.ts b/sim/pokemon.ts index a9c9f3923ec6..dc328018b7de 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -1064,7 +1064,7 @@ export class Pokemon { if (this.template.num === 493) { // Arceus formes const item = this.getItem(); - const targetForme = (item && item.onPlate ? 'Arceus-' + item.onPlate : 'Arceus'); + const targetForme = (item?.onPlate ? 'Arceus-' + item.onPlate : 'Arceus'); if (this.template.species !== targetForme) { this.formeChange(targetForme); } @@ -1366,7 +1366,7 @@ export class Pokemon { if (this.status === status.id) { if (sourceEffect && sourceEffect.status === this.status) { this.battle.add('-fail', this, this.status); - } else if (sourceEffect && sourceEffect.status) { + } else if (sourceEffect?.status) { this.battle.add('-fail', source); this.battle.attrLastMove('[still]'); } @@ -1374,11 +1374,11 @@ export class Pokemon { } if (!ignoreImmunities && status.id && - !(source && source.hasAbility('corrosion') && ['tox', 'psn'].includes(status.id))) { + !(source?.hasAbility('corrosion') && ['tox', 'psn'].includes(status.id))) { // the game currently never ignores immunities if (!this.runStatusImmunity(status.id === 'tox' ? 'psn' : status.id)) { this.battle.debug('immune to status'); - if (sourceEffect && sourceEffect.status) this.battle.add('-immune', this); + if (sourceEffect?.status) this.battle.add('-immune', this); return false; } } @@ -1616,7 +1616,7 @@ export class Pokemon { } if (!this.runStatusImmunity(status.id)) { this.battle.debug('immune to volatile status'); - if (sourceEffect && sourceEffect.status) this.battle.add('-immune', this); + if (sourceEffect?.status) this.battle.add('-immune', this); return false; } result = this.battle.runEvent('TryAddVolatile', this, source, sourceEffect, status); diff --git a/sim/side.ts b/sim/side.ts index b0853d09f2f3..fc4e7958d8c6 100644 --- a/sim/side.ts +++ b/sim/side.ts @@ -702,7 +702,7 @@ export class Side { let forcedSwitches = 0; let forcedPasses = 0; if (this.battle.requestState === 'switch') { - const canSwitchOut = this.active.filter(pokemon => pokemon && pokemon.switchFlag).length; + const canSwitchOut = this.active.filter(pokemon => pokemon?.switchFlag).length; const canSwitchIn = this.pokemon.slice(this.active.length).filter(pokemon => pokemon && !pokemon.fainted).length; forcedSwitches = Math.min(canSwitchOut, canSwitchIn); forcedPasses = canSwitchOut - forcedSwitches; diff --git a/sim/team-validator.ts b/sim/team-validator.ts index 5c48f40684fe..045fa2f46e26 100644 --- a/sim/team-validator.ts +++ b/sim/team-validator.ts @@ -1696,7 +1696,7 @@ export class TeamValidator { const noFutureGen = !ruleTable.has('allowtradeback'); let tradebackEligible = false; - while (template && template.species && !alreadyChecked[template.speciesid]) { + while (template?.species && !alreadyChecked[template.speciesid]) { alreadyChecked[template.speciesid] = true; if (dex.gen <= 2 && template.gen === 1) tradebackEligible = true; if (!template.learnset) {