pokemon-showdown/eslint-ps-standard.mjs
Guangcong Luo 78439b4a02
Update to ESLint 9 (#10926)
ESLint has a whole new config format, so I figure it's a good time to
make the config system saner.

- First, we no longer have separate eslint-no-types configs. Lint
  performance shouldn't be enough of a problem to justify the
  relevant maintenance complexity.

- Second, our base config should work out-of-the-box now. `npx eslint`
  will work as expected, without any CLI flags. You should still use
  `npm run lint` which adds the `--cached` flag for performance.

- Third, whatever updates I did fixed style linting, which apparently
  has been bugged for quite some time, considering all the obvious
  mixed-tabs-and-spaces issues I found in the upgrade.

Also here are some changes to our style rules. In particular:

- Curly brackets (for objects etc) now have spaces inside them. Sorry
  for the huge change. ESLint doesn't support our old style, and most
  projects use Prettier style, so we might as well match them in this way.
  See https://github.com/eslint-stylistic/eslint-stylistic/issues/415

- String + number concatenation is no longer allowed. We now
  consistently use template strings for this.
2025-02-25 20:03:46 -08:00

436 lines
15 KiB
JavaScript

/**
* 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<ConfigFile>[number]} Config */
/** @typedef {NonNullable<Config['rules']>} 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<Config['rules']>} */
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<Config['rules']>} */
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<Config['rules']>} */
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<Config['rules']>} */
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<Config['rules']>}
*/
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
['"`\/]
*/