From f11e79495c11887fd4b89b0eeda8b191f7db44a2 Mon Sep 17 00:00:00 2001 From: Caleb Eby Date: Tue, 22 Aug 2023 11:14:10 -0700 Subject: [PATCH] TS improvements (#551) --- .changeset/funny-guests-shout.md | 22 ++ .changeset/thick-trains-hide.md | 5 + README.md | 2 + generate-changeset.mjs | 119 +++++--- src/config.js | 471 ++++++++++++++++--------------- 5 files changed, 349 insertions(+), 270 deletions(-) create mode 100644 .changeset/funny-guests-shout.md create mode 100644 .changeset/thick-trains-hide.md diff --git a/.changeset/funny-guests-shout.md b/.changeset/funny-guests-shout.md new file mode 100644 index 00000000..b6b80dfa --- /dev/null +++ b/.changeset/funny-guests-shout.md @@ -0,0 +1,22 @@ +--- +'@cloudfour/eslint-plugin': major +--- + +Improve linting of `type` modifiers on imports. + +**Reconfigured Rules (`recommended` Config)** + +- [`@cloudfour/import/no-duplicates`](https://github.com/import-js/eslint-plugin-import/blob/v2.28.0/docs/rules/no-duplicates.md) + ```diff + - "error" + + [ + + "error", + + { + + "prefer-inline": true + + } + + ] + ``` + +**Newly Enabled Rules (`recommended` Config » TS Overrides)** + +- [`@cloudfour/typescript-eslint/no-import-type-side-effects`](https://typescript-eslint.io/rules/no-import-type-side-effects) diff --git a/.changeset/thick-trains-hide.md b/.changeset/thick-trains-hide.md new file mode 100644 index 00000000..1cb06dd7 --- /dev/null +++ b/.changeset/thick-trains-hide.md @@ -0,0 +1,5 @@ +--- +'@cloudfour/eslint-plugin': minor +--- + +Re-export `disable-type-checked` config from `@typescript-eslint/eslint-plugin` diff --git a/README.md b/README.md index d5db411a..41c0e012 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ with `cloudfour/`, because the plugins are exposed through this "super-plugin". } ``` +In addition to the `recommended` configuration, `@cloudfour/eslint-plugin` also re-exports typescript-eslint's [`disable-type-checked` configuration](https://typescript-eslint.io/linting/configs/#disable-type-checked). This configuration disables any rules that depend on typescript-eslint's typescript integration. + ## Installation ```sh diff --git a/generate-changeset.mjs b/generate-changeset.mjs index 7fc06a62..14b947ab 100644 --- a/generate-changeset.mjs +++ b/generate-changeset.mjs @@ -18,6 +18,7 @@ const writeChangeset = _writeChangeset.default; * @param {string} command * @param {readonly string[]} args * @param {import("child_process").SpawnOptionsWithoutStdio} [opts] + * @returns {Promise} */ const runCommand = (command, args, opts) => new Promise((resolve, reject) => { @@ -32,20 +33,21 @@ const runCommand = (command, args, opts) => }); /** @param {string} path */ -const stat = (path) => fs.stat(path).catch(() => /** @type {null} */ (null)); +const stat = (path) => fs.stat(path).catch(() => null); /** @param {string} message */ const log = (message) => console.log(kleur.bold(kleur.green(`\n🌀 ${message}`))); -/** @param {string} dir */ +/** + * @param {string} dir + * @returns {Promise} + */ const loadConfig = async (dir) => { const configModule = await import(join(dir, 'index.js')).then( (m) => m.default, ); - const allRules = configModule.rules; - const config = configModule.configs.recommended.rules; - return [allRules, config]; + return configModule; }; /** @@ -95,18 +97,21 @@ log('Building on this branch'); await runCommand('npm', ['run', 'build']); log('Parsing out differences'); -const [mainRules, mainConfig] = await loadConfig(dir); -const [branchRules, branchConfig] = await loadConfig(process.cwd()); +const mainConfig = await loadConfig(dir); +const branchConfig = await loadConfig(process.cwd()); + +const mainRules = mainConfig.rules || {}; +const branchRules = mainConfig.rules || {}; /** @param {string} _ruleName */ const printRuleLink = (_ruleName) => { const isBuiltIn = !_ruleName.includes('/'); const ruleName = removePrefix(_ruleName); const fullName = isBuiltIn ? ruleName : prefix + ruleName; - const rule = branchRules[ruleName] || mainRules[ruleName]; + const rule = branchRules?.[ruleName] || mainRules?.[ruleName]; const url = isBuiltIn ? `https://eslint.org/docs/rules/${fullName}` - : rule?.meta?.docs?.url; + : typeof rule === 'object' && rule?.meta?.docs?.url; return url ? `[\`${fullName}\`](${url})` : `\`${fullName}\``; }; @@ -146,45 +151,73 @@ const isEnabled = (rule) => ? isEnabled(rule[0]) : rule === 'error' || rule === 2 || rule === 'warn' || rule === 1); -const newlyEnabledRules = Object.entries(branchConfig) - .filter( - ([ruleName, value]) => isEnabled(value) && !isEnabled(mainConfig[ruleName]), - ) - .map(([ruleName]) => ruleName); -printRuleList(newlyEnabledRules, 'Newly Enabled Rules'); - -const newlyDisabledRules = Object.entries(mainConfig) - .filter( - ([ruleName, value]) => - isEnabled(value) && !isEnabled(branchConfig[ruleName]), - ) - .map(([ruleName]) => ruleName); -printRuleList(newlyDisabledRules, 'Newly Disabled Rules'); - -let hasOutputReconfiguredRules = false; -for (const ruleName of Object.keys(branchConfig)) { - const branchConfigPrinted = printRuleConfig(branchConfig[ruleName]); - const mainConfigPrinted = printRuleConfig(mainConfig[ruleName]); - if ( - branchConfigPrinted !== mainConfigPrinted && - // Make sure that the enabled status did not change - isEnabled(branchConfig[ruleName]) === isEnabled(mainConfig[ruleName]) - ) { - if (!hasOutputReconfiguredRules) { - output += '\n**Reconfigured Rules**\n'; - console.log(`${kleur.blue(kleur.bold('Reconfigured Rules'))}`); - hasOutputReconfiguredRules = true; - } - console.log(printRuleForCLI(ruleName)); - console.log(kleur.red(indent(mainConfigPrinted, '- '))); - console.log(kleur.green(indent(branchConfigPrinted, '+ '))); +/** + * @type {{ + * name: string, + * get: (config: import('eslint').ESLint.Plugin) => Partial | undefined}[] + } */ +const scopes = [ + { + name: '`recommended` Config', + get: (config) => config.configs?.recommended?.rules, + }, + { + name: '`recommended` Config » TS Overrides', + get: (config) => config.configs?.recommended?.overrides?.[0].rules, + }, + { + name: '`disable-type-checked` Config', + get: (config) => config.configs?.['disable-type-checked']?.rules, + }, +]; - output += ` +for (const scope of scopes) { + const branchScopeRules = scope.get(branchConfig) || {}; + const mainScopeRules = scope.get(mainConfig) || {}; + const newlyEnabledRules = Object.entries(branchScopeRules) + .filter( + ([ruleName, value]) => + isEnabled(value) && !isEnabled(mainScopeRules[ruleName]), + ) + .map(([ruleName]) => ruleName); + printRuleList(newlyEnabledRules, `Newly Enabled Rules (${scope.name})`); + + const newlyDisabledRules = Object.entries(mainScopeRules) + .filter( + ([ruleName, value]) => + isEnabled(value) && !isEnabled(branchScopeRules[ruleName]), + ) + .map(([ruleName]) => ruleName); + printRuleList(newlyDisabledRules, `Newly Disabled Rules (${scope.name})`); + + let hasOutputReconfiguredRules = false; + for (const ruleName of Object.keys(branchScopeRules)) { + const branchConfigPrinted = printRuleConfig(branchScopeRules[ruleName]); + const mainConfigPrinted = printRuleConfig(mainScopeRules[ruleName]); + if ( + branchConfigPrinted !== mainConfigPrinted && + // Make sure that it is enabled on both branches + isEnabled(branchScopeRules[ruleName]) && + isEnabled(mainScopeRules[ruleName]) + ) { + if (!hasOutputReconfiguredRules) { + output += `\n**Reconfigured Rules (${scope.name})**\n`; + console.log( + `${kleur.blue(kleur.bold(`Reconfigured Rules (${scope.name})`))}`, + ); + hasOutputReconfiguredRules = true; + } + console.log(printRuleForCLI(ruleName)); + console.log(kleur.red(indent(mainConfigPrinted, '- '))); + console.log(kleur.green(indent(branchConfigPrinted, '+ '))); + + output += ` - ${printRuleLink(ruleName)} \`\`\`diff ${indent(mainConfigPrinted, ' - ')} ${indent(branchConfigPrinted, ' + ')} \`\`\``; + } } } @@ -215,7 +248,7 @@ const { versionBump, summary } = await prompts( output = `${summary}\n${output}`; const changeset = { - summary: prettier.format(output, { parser: 'markdown' }), + summary: await prettier.format(output, { parser: 'markdown' }), releases: [{ name: pkgName, type: versionBump }], }; diff --git a/src/config.js b/src/config.js index e5d0700f..16b97c4b 100644 --- a/src/config.js +++ b/src/config.js @@ -54,249 +54,266 @@ const changeWarnToError = (rules) => }), ); -module.exports.configs = { - recommended: { - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - ecmaFeatures: { jsx: true }, - }, - env: { - node: true, - es6: true, +/** @type {import('eslint').Linter.BaseConfig} */ +const recommendedConfig = { + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + env: { + node: true, + es6: true, + }, + settings: { + jsdoc: { + mode: 'typescript', + tagNamePreference: { + TODO: 'todo', + }, + preferredTypes: { + '*': 'any', + Function: '() => void', + function: '() => void', + }, }, - settings: { - jsdoc: { - mode: 'typescript', - tagNamePreference: { - TODO: 'todo', + }, + globals: { + document: false, + navigator: false, + window: false, + }, + plugins: ['@cloudfour'], + rules: removeUnused( + prefix({ + // Plugins' recommended configs + ...node.rules, + ...unicorn.rules, + ...changeWarnToError(jsdoc.rules), + + // "standards" + ...xo.rules, + ...standard.rules, + + ...prettier.rules, // Undoes stylistic rules + + // Overrides + 'no-unused-expressions': [ + 'error', + { + allowShortCircuit: false, + allowTernary: false, + allowTaggedTemplates: false, }, - preferredTypes: { - '*': 'any', - Function: '() => void', - function: '() => void', + ], + '@cloudfour/prefer-early-return': 'error', + 'no-return-assign': ['error'], + 'func-names': 'off', + 'prefer-const': [ + 'error', + // If there is a destructuring assignment + // and some of the properties should be const + // but others shouldn't be, let it use let + { destructuring: 'all' }, + ], + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-object-spread': 'error', + 'prefer-spread': 'error', + 'prefer-destructuring': ['error', { array: false }], + 'prefer-rest-params': 'error', + // We decided that since devs can use blank lines to create logical groupings in code, + // it is best not to have ESLint enforce adding newlines + 'padding-line-between-statements': 'off', + 'prefer-template': 'error', + 'no-param-reassign': 'off', // We don't use `arguments`, and assigning to parameters can be useful + 'no-promise-executor-return': 'off', // Allow implicit return in promise executor + 'capitalized-comments': [ + 'error', + 'always', + { + ignorePattern: + 'pragma|ignore|prettier-ignore|webpack\\w+:|c8|return|const|let|var|await|function|console', + ignoreInlineComments: true, + ignoreConsecutiveComments: true, }, + ], + + 'n/no-unsupported-features/es-syntax': 'off', // Does not account for transpilation + 'n/no-unpublished-require': 'off', // Does not account for "build" scripts + 'n/shebang': 'off', // Tons of false positives + 'n/file-extension-in-import': ['error', 'always'], // Don't allow extension-less relative imports (e.g. use ./foo.js instead of ./foo) + // Used for sorting/grouping import statements + 'import/order': [ + 'error', + { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + ], + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, + }, + ], + // Avoid multiple import statements in the same file for the same module + // prefer-inline means it is preferred to use inline `type` imports combined with non-types + // instead of separate imports for types and non-types + // e.g. import { Foo, type Bar } from 'something' is preferred over having separate import statements + 'import/no-duplicates': ['error', { 'prefer-inline': true }], + // Used for sorting members within an import statement alphabetically + 'sort-imports': ['error', { ignoreDeclarationSort: true }], + + 'unicorn/import-style': 'off', // It doesn't seem useful to force people to use named, default, or namespace imports + 'unicorn/prevent-abbreviations': 'off', // Causes more issues than it's worth + // Null is ok, even though Sindre Sorhus doesn't like it + // It is ok to avoid using null and use undefined instead + // but enforcing it in all code via a lint rule is too annoying + 'unicorn/no-null': 'off', + // This rule is meant to avoid the edge case of breaking changes occuring + // due to the `index` parameter being passed unexpectedly into the callback function, + // causing unexpected behavior if the callback expects something that is not the index + // But this is an edge case that can be avoided through careful manual review + // and sometimes through TS + 'unicorn/no-array-callback-reference': 'off', + // https://github.com/sindresorhus/eslint-plugin-unicorn/pull/1750 + // This rule got removed from the recommended preset, but that hasn't been published yet. + 'unicorn/prefer-json-parse-buffer': 'off', + // This rule changes arrays to sets if you call .includes on it + // Converting from array to set has a cost itself, just like .includes has a cost + // We decided to leave the decision of using arrays vs sets to human reviewers + 'unicorn/prefer-set-has': 'off', + // Reduce is often useful. Don't need a lint rule to tell us not to use it + 'unicorn/no-array-reduce': 'off', + 'unicorn/prefer-module': 'off', // A lot of projects still use commonjs by default for non-browser code. We can revisit this rule once commonjs is basically never used. + 'unicorn/prefer-switch': 'off', // Switch statements are often longer than if/else chains, and they are still read aloud as "if ... is ... then" + 'unicorn/prefer-number-properties': [ + 'error', + // There isn't a good reason to force use of Number.POSITIVE_INFINITY instead of Infinity + { checkInfinity: false }, + ], + // String#replaceAll doesn't quite have enough browser/node support to enable this rule by default. + // TODO [2024-01-01] Reconsider browser/node support for those two methods + 'unicorn/prefer-string-replace-all': 'off', + // String#at and Array#at don't quite have enough browser/node support to enable this rule by default. + // TODO [2024-01-01] Reconsider browser/node support for those two methods + 'unicorn/prefer-at': 'off', + // This rule suggests incorrect code with the destructured object is modified + // That is a fairly common case, and it is too annoying to always disable the rule on each line + 'unicorn/consistent-destructuring': 'off', + + // Allow _only_ TODO comments with expirations/conditions + 'no-warning-comments': 'off', + 'unicorn/expiring-todo-comments': [ + 'error', + { allowWarningComments: false }, + ], + + // Disabling jsdoc rules that check the types themselves + // If you want to have type checking on a project, use typescript instead + 'jsdoc/newline-after-description': 'off', + 'jsdoc/no-undefined-types': 'off', + 'jsdoc/valid-types': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-returns-check': 'off', // Does not handle @returns with void or undefined + 'jsdoc/tag-lines': ['error', 'any', { startLines: 1 }], + }), + ), + overrides: [ + { + files: ['*.ts', '*.tsx', '.mts', '.cts'], + parser: require.resolve('@typescript-eslint/parser'), // Force it to resolve from this directory + parserOptions: { + project: './tsconfig.json', }, - }, - globals: { - document: false, - navigator: false, - window: false, - }, - plugins: ['@cloudfour'], - rules: removeUnused( - prefix({ - // Plugins' recommended configs - ...node.rules, - ...unicorn.rules, - ...changeWarnToError(jsdoc.rules), + rules: prefix({ + ...typescript.configs['eslint-recommended'].overrides[0].rules, + ...typescript.configs.recommended.rules, + ...typescript.configs['recommended-type-checked'].rules, - // "standards" - ...xo.rules, - ...standard.rules, + // TS handles checking these + 'n/no-missing-import': 'off', + 'n/no-missing-require': 'off', - ...prettier.rules, // Undoes stylistic rules + 'no-import-assign': 'off', // TS handles this - // Overrides - 'no-unused-expressions': [ - 'error', - { - allowShortCircuit: false, - allowTernary: false, - allowTaggedTemplates: false, - }, - ], - '@cloudfour/prefer-early-return': 'error', - 'no-return-assign': ['error'], - 'func-names': 'off', - 'prefer-const': [ + // With TS, the only reason to have a @param tag + // is if a particular parameter needs a description, + // which is not true for all parameters + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-type': 'off', // Types should be in type annotations instead + 'jsdoc/require-param-description': 'error', // The only reason to have an @param in TS is to add a description + 'jsdoc/require-returns-type': 'off', // Return types should be in type annotations instead + 'jsdoc/require-returns-description': 'error', // The only reason to have an @returns in TS is to add a description + // Auto-fixes type imports to use the `import type` syntax + // This syntax is preferred because it makes the TS -> JS transformation easier + // because it doesn't require checking which imports are only referenced as types + '@typescript-eslint/consistent-type-imports': [ 'error', - // If there is a destructuring assignment - // and some of the properties should be const - // but others shouldn't be, let it use let - { destructuring: 'all' }, + // We have set it to allow import('...') for types because that is the only kind of import that is allowed in global type augmentations + { disallowTypeAnnotations: false }, ], - 'no-var': 'error', - 'object-shorthand': 'error', - 'prefer-object-spread': 'error', - 'prefer-spread': 'error', - 'prefer-destructuring': ['error', { array: false }], - 'prefer-rest-params': 'error', - // We decided that since devs can use blank lines to create logical groupings in code, - // it is best not to have ESLint enforce adding newlines - 'padding-line-between-statements': 'off', - 'prefer-template': 'error', - 'no-param-reassign': 'off', // We don't use `arguments`, and assigning to parameters can be useful - 'no-promise-executor-return': 'off', // Allow implicit return in promise executor - 'capitalized-comments': [ + // Don't try to use the result of expression whose type is `void` + '@typescript-eslint/no-confusing-void-expression': [ 'error', - 'always', - { - ignorePattern: - 'pragma|ignore|prettier-ignore|webpack\\w+:|c8|return|const|let|var|await|function|console', - ignoreInlineComments: true, - ignoreConsecutiveComments: true, - }, + { ignoreArrowShorthand: true }, ], - - 'n/no-unsupported-features/es-syntax': 'off', // Does not account for transpilation - 'n/no-unpublished-require': 'off', // Does not account for "build" scripts - 'n/shebang': 'off', // Tons of false positives - 'n/file-extension-in-import': ['error', 'always'], // Don't allow extension-less relative imports (e.g. use ./foo.js instead of ./foo) - // Used for sorting/grouping import statements - 'import/order': [ + // Don't use the void operator an an expression whose type is already `void` + '@typescript-eslint/no-meaningless-void-operator': 'error', + '@typescript-eslint/no-unnecessary-type-constraint': 'error', + '@typescript-eslint/array-type': ['error', { default: 'array' }], // Require consistency: Use foo[] instead of Array + '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'off', // Type inference is useful even for public functions + '@typescript-eslint/no-explicit-any': 'off', // Any is an escape hatch, it should be allowed + '@typescript-eslint/no-floating-promises': 'off', // Don't force every promise rejection to be caught. Humans can decide when it makes sense to handle errors and when it doesn't + '@typescript-eslint/no-non-null-assertion': 'error', // Default is warn + '@typescript-eslint/no-unsafe-assignment': 'off', // Any is an escape hatch, let it be an escape hatch + '@typescript-eslint/no-unsafe-call': 'off', // Any is an escape hatch, let it be an escape hatch + '@typescript-eslint/no-unsafe-member-access': 'off', // Any is an escape hatch, let it be an escape hatch + '@typescript-eslint/no-unsafe-return': 'off', // Any is an escape hatch, let it be an escape hatch + '@typescript-eslint/no-unsafe-argument': 'off', // Any is an escape hatch, let it be an escape hatch + '@typescript-eslint/restrict-template-expressions': 'off', // Allow using any-typed-values in template expressions + '@typescript-eslint/no-unnecessary-condition': 'error', // This catches a lot of dead code that TS itself doesn't flag + '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', + '@typescript-eslint/prefer-optional-chain': 'error', // More readable syntax + 'no-unused-vars': 'off', // TS checks this via noUnusedLocals / noUnusedParameters + '@typescript-eslint/no-unused-vars': 'off', // TS checks this via noUnusedLocals / noUnusedParameters + '@typescript-eslint/no-empty-function': 'off', // Non-TS version of rule is not used either + '@typescript-eslint/unbound-method': 'off', // It is pretty common for this already being handled outside of what TS/ESLint can be aware of + '@typescript-eslint/no-import-type-side-effects': 'error', + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': ['error'], // This rule is like the built in ESLint rule but it supports optional chaining + // Replacing the built-in rule with the version that works well with TS + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': [ 'error', { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - ], - 'newlines-between': 'always', - alphabetize: { order: 'asc', caseInsensitive: true }, + functions: false, + classes: false, + variables: false, + ignoreTypeReferences: true, }, ], - // Used for sorting members within an import statement alphabetically - 'sort-imports': ['error', { ignoreDeclarationSort: true }], - - 'unicorn/import-style': 'off', // It doesn't seem useful to force people to use named, default, or namespace imports - 'unicorn/prevent-abbreviations': 'off', // Causes more issues than it's worth - // Null is ok, even though Sindre Sorhus doesn't like it - // It is ok to avoid using null and use undefined instead - // but enforcing it in all code via a lint rule is too annoying - 'unicorn/no-null': 'off', - // This rule is meant to avoid the edge case of breaking changes occuring - // due to the `index` parameter being passed unexpectedly into the callback function, - // causing unexpected behavior if the callback expects something that is not the index - // But this is an edge case that can be avoided through careful manual review - // and sometimes through TS - 'unicorn/no-array-callback-reference': 'off', - // https://github.com/sindresorhus/eslint-plugin-unicorn/pull/1750 - // This rule got removed from the recommended preset, but that hasn't been published yet. - 'unicorn/prefer-json-parse-buffer': 'off', - // This rule changes arrays to sets if you call .includes on it - // Converting from array to set has a cost itself, just like .includes has a cost - // We decided to leave the decision of using arrays vs sets to human reviewers - 'unicorn/prefer-set-has': 'off', - // Reduce is often useful. Don't need a lint rule to tell us not to use it - 'unicorn/no-array-reduce': 'off', - 'unicorn/prefer-module': 'off', // A lot of projects still use commonjs by default for non-browser code. We can revisit this rule once commonjs is basically never used. - 'unicorn/prefer-switch': 'off', // Switch statements are often longer than if/else chains, and they are still read aloud as "if ... is ... then" - 'unicorn/prefer-number-properties': [ - 'error', - // There isn't a good reason to force use of Number.POSITIVE_INFINITY instead of Infinity - { checkInfinity: false }, - ], - // String#replaceAll doesn't quite have enough browser/node support to enable this rule by default. - // TODO [2024-01-01] Reconsider browser/node support for those two methods - 'unicorn/prefer-string-replace-all': 'off', - // String#at and Array#at don't quite have enough browser/node support to enable this rule by default. - // TODO [2024-01-01] Reconsider browser/node support for those two methods - 'unicorn/prefer-at': 'off', - // This rule suggests incorrect code with the destructured object is modified - // That is a fairly common case, and it is too annoying to always disable the rule on each line - 'unicorn/consistent-destructuring': 'off', - - // Allow _only_ TODO comments with expirations/conditions - 'no-warning-comments': 'off', - 'unicorn/expiring-todo-comments': [ - 'error', - { allowWarningComments: false }, - ], - - // Disabling jsdoc rules that check the types themselves - // If you want to have type checking on a project, use typescript instead - 'jsdoc/newline-after-description': 'off', - 'jsdoc/no-undefined-types': 'off', - 'jsdoc/valid-types': 'off', - 'jsdoc/require-returns': 'off', - 'jsdoc/require-param-description': 'off', - 'jsdoc/require-property-description': 'off', - 'jsdoc/require-returns-description': 'off', - 'jsdoc/require-jsdoc': 'off', - 'jsdoc/require-returns-check': 'off', // Does not handle @returns with void or undefined - 'jsdoc/tag-lines': ['error', 'any', { startLines: 1 }], }), - ), - overrides: [ - { - files: ['*.ts', '*.tsx'], - parser: require.resolve('@typescript-eslint/parser'), // Force it to resolve from this directory - parserOptions: { - project: './tsconfig.json', - }, - rules: prefix({ - ...typescript.configs['eslint-recommended'].overrides[0].rules, - ...typescript.configs.recommended.rules, - ...typescript.configs['recommended-requiring-type-checking'].rules, - - // TS handles checking these - 'n/no-missing-import': 'off', - 'n/no-missing-require': 'off', + }, + ], +}; - 'no-import-assign': 'off', // TS handles this +/** @type {import('eslint').Linter.BaseConfig} */ +const disableTypeCheckedConfig = { + rules: prefix({ + ...typescript.configs['disable-type-checked'].rules, + }), +}; - // With TS, the only reason to have a @param tag - // is if a particular parameter needs a description, - // which is not true for all parameters - 'jsdoc/require-param': 'off', - 'jsdoc/require-param-type': 'off', // Types should be in type annotations instead - 'jsdoc/require-param-description': 'error', // The only reason to have an @param in TS is to add a description - 'jsdoc/require-returns-type': 'off', // Return types should be in type annotations instead - 'jsdoc/require-returns-description': 'error', // The only reason to have an @returns in TS is to add a description - // Auto-fixes type imports to use the `import type` syntax - // This syntax is preferred because it makes the TS -> JS transformation easier - // because it doesn't require checking which imports are only referenced as types - '@typescript-eslint/consistent-type-imports': [ - 'error', - // We have set it to allow import('...') for types because that is the only kind of import that is allowed in global type augmentations - { disallowTypeAnnotations: false }, - ], - // Don't try to use the result of expression whose type is `void` - '@typescript-eslint/no-confusing-void-expression': [ - 'error', - { ignoreArrowShorthand: true }, - ], - // Don't use the void operator an an expression whose type is already `void` - '@typescript-eslint/no-meaningless-void-operator': 'error', - '@typescript-eslint/no-unnecessary-type-constraint': 'error', - '@typescript-eslint/array-type': ['error', { default: 'array' }], // Require consistency: Use foo[] instead of Array - '@typescript-eslint/ban-ts-comment': 'error', - '@typescript-eslint/explicit-module-boundary-types': 'off', // Type inference is useful even for public functions - '@typescript-eslint/no-explicit-any': 'off', // Any is an escape hatch, it should be allowed - '@typescript-eslint/no-floating-promises': 'off', // Don't force every promise rejection to be caught. Humans can decide when it makes sense to handle errors and when it doesn't - '@typescript-eslint/no-non-null-assertion': 'error', // Default is warn - '@typescript-eslint/no-unsafe-assignment': 'off', // Any is an escape hatch, let it be an escape hatch - '@typescript-eslint/no-unsafe-call': 'off', // Any is an escape hatch, let it be an escape hatch - '@typescript-eslint/no-unsafe-member-access': 'off', // Any is an escape hatch, let it be an escape hatch - '@typescript-eslint/no-unsafe-return': 'off', // Any is an escape hatch, let it be an escape hatch - '@typescript-eslint/no-unsafe-argument': 'off', // Any is an escape hatch, let it be an escape hatch - '@typescript-eslint/restrict-template-expressions': 'off', // Allow using any-typed-values in template expressions - '@typescript-eslint/no-unnecessary-condition': 'error', // This catches a lot of dead code that TS itself doesn't flag - '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', - '@typescript-eslint/prefer-optional-chain': 'error', // More readable syntax - 'no-unused-vars': 'off', // TS checks this via noUnusedLocals / noUnusedParameters - '@typescript-eslint/no-unused-vars': 'off', // TS checks this via noUnusedLocals / noUnusedParameters - '@typescript-eslint/no-empty-function': 'off', // Non-TS version of rule is not used either - '@typescript-eslint/unbound-method': 'off', // It is pretty common for this already being handled outside of what TS/ESLint can be aware of - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': ['error'], // This rule is like the built in ESLint rule but it supports optional chaining - // Replacing the built-in rule with the version that works well with TS - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': [ - 'error', - { - functions: false, - classes: false, - variables: false, - ignoreTypeReferences: true, - }, - ], - }), - }, - ], - }, +module.exports.configs = { + recommended: recommendedConfig, + 'disable-type-checked': disableTypeCheckedConfig, };