/** * Pokemon Showdown standard style * * This is Showdown's shared ESLint configuration. Each project overrides * at least a little of it here and there, but these are the rules we use * unless there's a good reason otherwise. */ // @ts-check import eslint from '@eslint/js'; import globals from 'globals'; import tseslint from 'typescript-eslint'; import stylistic from '@stylistic/eslint-plugin'; /** @typedef {import('typescript-eslint').Config} ConfigFile */ /** @typedef {Awaited[number]} Config */ /** @typedef {NonNullable} Rules */ export { eslint, globals, tseslint, stylistic }; /** @type {Config} */ export const plugin = { plugins: { '@stylistic': stylistic, '@typescript-eslint': tseslint.plugin, }, }; /** @type {typeof tseslint.config} */ export const configure = (...args) => [ plugin, ...tseslint.config(...args), ]; /** @type {NonNullable} */ export const defaultRules = { ...stylistic.configs.customize({ braceStyle: '1tbs', indent: 'tab', semi: true, jsx: true, // ... }).rules, // TODO rules to revisit // ===================== // nice to have but we mostly know && || precedence so not urgent to fix "@stylistic/no-mixed-operators": "off", // test only (should never be committed, but useful when testing) // ============================================================== // do we want unused args/destructures to start with _? unsure "no-unused-vars": ["warn", { args: "all", argsIgnorePattern: ".", caughtErrors: "all", destructuredArrayIgnorePattern: ".", ignoreRestSiblings: true, }], // "no-unused-vars": ["warn", { // args: "all", // argsIgnorePattern: "^_", // caughtErrors: "all", // destructuredArrayIgnorePattern: "^_", // ignoreRestSiblings: true // }], "@stylistic/max-len": ["warn", { "code": 120, "tabWidth": 0, // DO NOT EDIT DIRECTLY: see bottom of file for source "ignorePattern": "^\\s*(?:\\/\\/ \\s*)?(?:(?:export )?(?:let |const |readonly )?[a-zA-Z0-9_$.]+(?: \\+?=>? )|[a-zA-Z0-9$]+: \\[?|(?:return |throw )?(?:new )?(?:[a-zA-Z0-9$.]+\\()?)?(?:Utils\\.html|(?:this\\.)?(?:room\\.)?tr|\\$\\()?['\"`/]", }], "prefer-const": ["warn", { "destructuring": "all" }], // PS code (code specific to PS) // ============================= "@stylistic/new-parens": "off", // used for the `new class {...}` pattern "no-prototype-builtins": "off", // defaults too strict // =================== "no-empty": ["error", { "allowEmptyCatch": true }], "no-case-declarations": "off", // probably bugs // ============= "array-callback-return": "error", "no-constructor-return": "error", "no-dupe-class-members": "error", "no-extend-native": "error", "no-extra-bind": "warn", "no-extra-label": "warn", "no-eval": "error", "no-implied-eval": "error", "no-inner-declarations": ["error", "functions"], "no-iterator": "error", "no-fallthrough": ["error", { allowEmptyCase: true, reportUnusedFallthroughComment: true }], "no-promise-executor-return": ["error", { allowVoid: true }], "no-return-assign": "error", "no-self-compare": "error", "no-sequences": "error", "no-shadow": "error", "no-template-curly-in-string": "error", "no-throw-literal": "warn", "no-unmodified-loop-condition": "error", // best way to read first key of object // "no-unreachable-loop": "error", // ternary is used to convert callbacks to Promises // tagged templates are used for the SQL library "no-unused-expressions": ["error", { allowTernary: true, allowTaggedTemplates: true, enforceForJSX: true }], "no-useless-call": "error", // "no-useless-assignment": "error", "require-atomic-updates": "error", // syntax style (local syntactical, usually autofixable formatting decisions) // =========================================================================== "@stylistic/member-delimiter-style": ["error", { multiline: { delimiter: "comma", requireLast: true }, singleline: { delimiter: "comma", requireLast: false }, overrides: { interface: { multiline: { delimiter: "semi", requireLast: true }, singleline: { delimiter: "semi", requireLast: false }, } }, }], "default-case-last": "error", "eqeqeq": ["error", "always", { null: "ignore" }], "no-array-constructor": "error", "no-duplicate-imports": "error", "no-implicit-coercion": ["error", { allow: ["!!", "+"] }], "no-multi-str": "error", "no-object-constructor": "error", "no-proto": "error", "no-unneeded-ternary": "error", "no-useless-computed-key": "error", "no-useless-constructor": "error", "no-useless-rename": "error", "no-useless-return": "error", "no-var": "error", "object-shorthand": ["error", "always"], "operator-assignment": ["error", "always"], "prefer-arrow-callback": "error", "prefer-exponentiation-operator": "error", "prefer-numeric-literals": "error", "prefer-object-has-own": "error", "prefer-object-spread": "error", "prefer-promise-reject-errors": "error", "prefer-regex-literals": "error", "prefer-rest-params": "error", "prefer-spread": "error", "radix": ["error", "as-needed"], // syntax style, overriding base // ============================= "@stylistic/quotes": "off", "@stylistic/quote-props": "off", "@stylistic/function-call-spacing": "error", "@stylistic/arrow-parens": ["error", "as-needed"], "@stylistic/comma-dangle": ["error", { "arrays": "always-multiline", "objects": "always-multiline", "imports": "always-multiline", "exports": "always-multiline", "functions": "never", "importAttributes": "always-multiline", "dynamicImports": "always-multiline", "enums": "always-multiline", "generics": "always-multiline", "tuples": "always-multiline", }], "@stylistic/jsx-wrap-multilines": "off", "@stylistic/jsx-closing-bracket-location": ["error", "line-aligned"], // "@stylistic/jsx-closing-tag-location": ["error", "line-aligned"], "@stylistic/jsx-closing-tag-location": "off", "@stylistic/jsx-one-expression-per-line": "off", "@stylistic/jsx-max-props-per-line": "off", "@stylistic/jsx-function-call-newline": "off", "no-restricted-syntax": ["error", { selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" }, ], // whitespace // ========== "@stylistic/block-spacing": "error", "@stylistic/operator-linebreak": ["error", "after"], "@stylistic/max-statements-per-line": ["error", { max: 3, ignoredNodes: ['BreakStatement'] }], "@stylistic/lines-between-class-members": "off", "@stylistic/multiline-ternary": "off", "@stylistic/object-curly-spacing": ["error", "always"], "@stylistic/indent": ["error", "tab", { "flatTernaryExpressions": true }], }; /** @type {NonNullable} */ export const defaultRulesTS = { ...defaultRules, // TODO: revisit // we should do this someday but it'd have to be a gradual manual process // "@typescript-eslint/explicit-module-boundary-types": "off", // like above but slightly harder, so do that one first // "@typescript-eslint/explicit-function-return-type": "off", // probably we should settle on a standard someday // "@typescript-eslint/member-ordering": "off", // "@typescript-eslint/no-extraneous-class": "error", // maybe we should consider this "@typescript-eslint/consistent-indexed-object-style": "off", // typescript-eslint specific // ========================== "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": defaultRules["no-unused-vars"], "no-shadow": "off", "@typescript-eslint/no-shadow": defaultRules["no-shadow"], "no-dupe-class-members": "off", "@typescript-eslint/no-dupe-class-members": defaultRules["no-dupe-class-members"], "no-unused-expressions": "off", "@typescript-eslint/no-unused-expressions": defaultRules["no-unused-expressions"], // defaults too strict // =================== "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-non-null-assertion": "off", // probably bugs // ============= "@typescript-eslint/no-empty-object-type": "error", "@typescript-eslint/no-extra-non-null-assertion": "error", "@typescript-eslint/no-misused-new": "error", // no way to get it to be less strict unfortunately // "@typescript-eslint/no-misused-spread": "error", "@typescript-eslint/no-non-null-asserted-optional-chain": "error", // naming style // ============ "@typescript-eslint/naming-convention": ["error", { "selector": ["class", "interface", "typeAlias"], "format": ["PascalCase"], }], // syntax style (local syntactical, usually autofixable formatting decisions) // =========================================================================== "@typescript-eslint/no-namespace": ["error", { allowDeclarations: true }], "@typescript-eslint/prefer-namespace-keyword": "error", "@typescript-eslint/adjacent-overload-signatures": "error", "@typescript-eslint/array-type": "error", "@typescript-eslint/consistent-type-assertions": ["error", { "assertionStyle": "as" }], "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }], "@typescript-eslint/explicit-member-accessibility": ["error", { "accessibility": "no-public" }], "@typescript-eslint/parameter-properties": "error", // `source` and `target` are frequently used as variables that may point to `this` // or to another `Pokemon` object, depending on how the given method is invoked "@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["source", "target"] }], // unfortunately this has lots of false positives without strict array/object property access // "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", "@typescript-eslint/prefer-as-const": "error", "@typescript-eslint/prefer-for-of": "error", "@typescript-eslint/prefer-function-type": "error", "@typescript-eslint/prefer-return-this-type": "error", "@typescript-eslint/triple-slash-reference": "error", "@typescript-eslint/unified-signatures": "error", }; /** @type {NonNullable} */ export const defaultRulesTSChecked = { ...defaultRulesTS, // style // ===== "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/restrict-plus-operands": ["error", { allowBoolean: false, allowNullish: false, allowNumberAndString: false, allowRegExp: false, }], "@typescript-eslint/restrict-template-expressions": ["error", { allow: [{ name: ['Error', 'URL', 'URLSearchParams'], from: 'lib' }], allowBoolean: false, allowNever: false, allowNullish: false, allowRegExp: false, }], // we use `any` // ============ "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-argument": "off", // yes-types syntax style, overriding base // ======================================= "@typescript-eslint/prefer-includes": "error", "@typescript-eslint/prefer-nullish-coalescing": "off", "@typescript-eslint/dot-notation": "off", "@typescript-eslint/no-confusing-non-null-assertion": "off", }; /** @type {NonNullable} */ export const defaultRulesES3 = { ...defaultRules, // required in ES3 // ================ "no-var": "off", "object-shorthand": ["error", "never"], "prefer-arrow-callback": "off", "prefer-exponentiation-operator": "off", "prefer-object-has-own": "off", "prefer-object-spread": "off", "prefer-rest-params": "off", "prefer-spread": "off", "radix": "off", "@stylistic/comma-dangle": "error", "no-unused-vars": ["warn", { args: "all", argsIgnorePattern: ".", caughtErrors: "all", caughtErrorsIgnorePattern: "^e(rr)?$", destructuredArrayIgnorePattern: ".", ignoreRestSiblings: true, }], "no-restricted-syntax": ["error", { selector: "TaggedTemplateExpression", message: "Hard to compile down to ES3" }, { selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" }, ], // with no block scoping, coming up with original variable names is too hard "no-redeclare": "off", // treat var as let // unfortunately doesn't actually let me redeclare // "block-scoped-var": "error", "no-caller": "error", "no-invalid-this": "error", "no-new-wrappers": "error", // Map/Set can be polyfilled but it's nontrivial and it's easier just to use bare objects "no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet", "WeakMap", "Set", "Map"], "unicode-bom": "error", }; /** * Actually very different from defaultRulesES3, because we don't have to * worry about syntax that's easy to transpile to ES3 (which is basically * all syntax). * @type {NonNullable} */ export const defaultRulesES3TSChecked = { ...defaultRulesTSChecked, "radix": "off", "no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet", "WeakMap", "Set", "Map"], "no-restricted-syntax": ["error", "TaggedTemplateExpression", "YieldExpression", "AwaitExpression", "BigIntLiteral"], }; /** * @param {Config[]} configs * @returns {Config} */ function extractPlugin(configs) { return configs.find(config => !config.rules) || (() => { throw new Error('No plugin found'); })(); } /** * @param {Config[]} configs * @returns {Rules} */ function extractRules(configs) { const rules = {}; for (const config of configs.filter(c => c.rules)) { Object.assign(rules, config.rules); } return rules; } const tseslintPlugin = extractPlugin(tseslint.configs.stylisticTypeChecked); /** @type {{[k: string]: Config[]}} */ export const configs = { js: [{ rules: { ...eslint.configs.recommended.rules, ...defaultRules, }, }], ts: [tseslintPlugin, { rules: { ...eslint.configs.recommended.rules, ...extractRules(tseslint.configs.recommendedTypeChecked), ...extractRules(tseslint.configs.stylisticTypeChecked), ...defaultRulesTSChecked, }, }], es3: [{ rules: { ...eslint.configs.recommended.rules, ...defaultRulesES3, }, }], es3ts: [tseslintPlugin, { rules: { ...eslint.configs.recommended.rules, ...extractRules(tseslint.configs.recommendedTypeChecked), ...extractRules(tseslint.configs.stylisticTypeChecked), ...defaultRulesES3TSChecked, }, }], }; /* SOURCE FOR IGNOREPATTERN (compile with https://regexfree.k55.io/ ) # indentation ^\s* # possibly commented out (\/\/\ \s*)? ( # define a variable, append to a variable, or define a single-arg arrow function (export\ )? (let\ |const\ |readonly\ )? [a-zA-Z0-9_$.]+ (\ \+?=>?\ ) | # define a property (oversize arrays are only allowed in properties) [a-zA-Z0-9$]+:\ \[? | # optionally return or throw (return\ |throw\ )? # call a function or constructor (new\ )?([a-zA-Z0-9$.]+\()? )? ( Utils\.html | (this\.)?(room\.)?tr | \$\( )? # start of string or regex ['"`\/] */