diff --git a/.eslintrc.js b/.eslintrc.js index 32a476969d3..ef3d195354e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,6 +15,7 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', ], rules: { // diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 516f9c48dae..1ecc3e86cf9 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -51,7 +51,7 @@ You can also enable all the recommended rules for our plugin. Add `plugin:@types } ``` -You can also use [eslint:recommended](https://eslint.org/docs/rules/) with this plugin. Add both `eslint:recommended` and `plugin:@typescript-eslint/eslint-recommended`: +You can also use [eslint:recommended](https://eslint.org/docs/rules/) (the set of rules which are recommended for all projects by the ESLint Team) with this plugin. As noted in the root README, not all eslint core rules are compatible with TypeScript, so you need to add both `eslint:recommended` and `plugin:@typescript-eslint/eslint-recommended` (which will adjust the one from eslint appropriately for TypeScript) to your config: ```json { @@ -63,7 +63,24 @@ You can also use [eslint:recommended](https://eslint.org/docs/rules/) with this } ``` -If you want to use rules which require type information, you will need to specify a path to your tsconfig.json file in the "project" property of "parserOptions". +As of version 2 of this plugin, _by design_, none of the rules in the main `recommended` config require type-checking in order to run. This means that they are more lightweight and faster to run. + +Some highly valuable rules simply require type-checking in order to be implemented correctly, however, so we provide an additional config you can extend from called `recommended-requiring-type-checking`. You wou apply this _in addition_ to the recommended configs previously mentioned, e.g.: + +```json +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ] +} +``` + +Pro Tip: For larger codebases you may want to consider splitting our linting into two separate stages: 1. fast feedback rules which operate purely based on syntax (no type-checking), 2. rules which are based on semantics (type-checking). + +NOTE: If you want to use rules which require type information, you will need to specify a path to your tsconfig.json file in the "project" property of "parserOptions". If you do not do this, you will get a runtime error which explains this. ```json { diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 14b417ac5c7..e0d1c05a9fe 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -30,11 +30,11 @@ "main": "dist/index.js", "scripts": { "build": "tsc -p tsconfig.build.json", - "check:docs": "ts-node --files ./tools/validate-docs/index.ts", - "check:configs": "ts-node --files ./tools/validate-configs/index.ts", + "check:docs": "../../node_modules/.bin/ts-node --files ./tools/validate-docs/index.ts", + "check:configs": "../../node_modules/.bin/ts-node --files ./tools/validate-configs/index.ts", "clean": "rimraf dist/", "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", - "generate:configs": "ts-node --files tools/generate-configs.ts", + "generate:configs": "../../node_modules/.bin/ts-node --files tools/generate-configs.ts", "prebuild": "npm run clean", "test": "jest --coverage", "typecheck": "tsc --noEmit" diff --git a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.json b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.json new file mode 100644 index 00000000000..68867b53248 --- /dev/null +++ b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.json @@ -0,0 +1,19 @@ +{ + "extends": "./configs/base.json", + "rules": { + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/no-for-in-array": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/prefer-includes": "error", + "@typescript-eslint/prefer-regexp-exec": "error", + "@typescript-eslint/prefer-string-starts-ends-with": "error", + "require-await": "off", + "@typescript-eslint/require-await": "error", + "@typescript-eslint/unbound-method": "error", + "no-var": "error", + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error" + } +} diff --git a/packages/eslint-plugin/src/configs/recommended.json b/packages/eslint-plugin/src/configs/recommended.json index 95d18847686..7d7a5628c9d 100644 --- a/packages/eslint-plugin/src/configs/recommended.json +++ b/packages/eslint-plugin/src/configs/recommended.json @@ -2,7 +2,6 @@ "extends": "./configs/base.json", "rules": { "@typescript-eslint/adjacent-overload-signatures": "error", - "@typescript-eslint/await-thenable": "error", "@typescript-eslint/ban-ts-ignore": "error", "@typescript-eslint/ban-types": "error", "camelcase": "off", @@ -18,28 +17,19 @@ "@typescript-eslint/no-empty-function": "error", "@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-for-in-array": "error", "@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-non-null-assertion": "warn", "@typescript-eslint/no-this-alias": "error", - "@typescript-eslint/no-unnecessary-type-assertion": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "warn", "no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "error", "@typescript-eslint/no-var-requires": "error", - "@typescript-eslint/prefer-includes": "error", "@typescript-eslint/prefer-namespace-keyword": "error", - "@typescript-eslint/prefer-regexp-exec": "error", - "@typescript-eslint/prefer-string-starts-ends-with": "error", - "require-await": "off", - "@typescript-eslint/require-await": "error", "@typescript-eslint/triple-slash-reference": "error", "@typescript-eslint/type-annotation-spacing": "error", - "@typescript-eslint/unbound-method": "error", "no-var": "error", "prefer-const": "error", "prefer-rest-params": "error", diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts index 0fb5516ba72..d8e55844e1d 100644 --- a/packages/eslint-plugin/src/index.ts +++ b/packages/eslint-plugin/src/index.ts @@ -3,6 +3,7 @@ import rules from './rules'; import all from './configs/all.json'; import base from './configs/base.json'; import recommended from './configs/recommended.json'; +import recommendedRequiringTypeChecking from './configs/recommended-requiring-type-checking.json'; import eslintRecommended from './configs/eslint-recommended'; export = { @@ -12,5 +13,6 @@ export = { base, recommended, 'eslint-recommended': eslintRecommended, + 'recommended-requiring-type-checking': recommendedRequiringTypeChecking, }, }; diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 304321001f7..94dc75cf78d 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -10,6 +10,7 @@ export default util.createRule({ description: 'Disallows awaiting a value that is not a Thenable', category: 'Best Practices', recommended: 'error', + requiresTypeChecking: true, }, messages: { await: 'Unexpected `await` of a non-Promise (non-"Thenable") value.', diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 5bc2f75adaa..816cc084670 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -10,6 +10,7 @@ export default util.createRule({ description: 'Requires Promise-like values to be handled appropriately.', category: 'Best Practices', recommended: false, + requiresTypeChecking: true, }, messages: { floating: 'Promises must be handled appropriately', diff --git a/packages/eslint-plugin/src/rules/no-for-in-array.ts b/packages/eslint-plugin/src/rules/no-for-in-array.ts index 03171e7fdf7..db15d310457 100644 --- a/packages/eslint-plugin/src/rules/no-for-in-array.ts +++ b/packages/eslint-plugin/src/rules/no-for-in-array.ts @@ -8,6 +8,7 @@ export default util.createRule({ description: 'Disallow iterating over an array with a for-in loop', category: 'Best Practices', recommended: 'error', + requiresTypeChecking: true, }, messages: { forInViolation: diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 6d06ef8807a..63d0f0bf483 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -18,6 +18,7 @@ export default util.createRule({ description: 'Avoid using promises in places not designed to handle them', category: 'Best Practices', recommended: 'error', + requiresTypeChecking: true, }, messages: { voidReturn: diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts b/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts index 64f58848d79..1017be32222 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts @@ -10,6 +10,7 @@ export default util.createRule({ category: 'Best Practices', description: 'Warns when a namespace qualifier is unnecessary', recommended: false, + requiresTypeChecking: true, }, fixable: 'code', messages: { diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts index 100a9d261c0..040650ea05d 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts @@ -29,6 +29,7 @@ export default util.createRule<[], MessageIds>({ 'Warns if an explicitly specified type argument is the default for that type parameter', category: 'Best Practices', recommended: false, + requiresTypeChecking: true, }, fixable: 'code', messages: { diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index 11da74d88c9..61583878bae 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -28,6 +28,7 @@ export default util.createRule({ 'Warns if a type assertion does not change the type of an expression', category: 'Best Practices', recommended: 'error', + requiresTypeChecking: true, }, fixable: 'code', messages: { diff --git a/packages/eslint-plugin/src/rules/prefer-includes.ts b/packages/eslint-plugin/src/rules/prefer-includes.ts index 384edf01859..fcb5dfca4f5 100644 --- a/packages/eslint-plugin/src/rules/prefer-includes.ts +++ b/packages/eslint-plugin/src/rules/prefer-includes.ts @@ -14,6 +14,7 @@ export default createRule({ description: 'Enforce `includes` method over `indexOf` method', category: 'Best Practices', recommended: 'error', + requiresTypeChecking: true, }, fixable: 'code', messages: { diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index 22c85fd0343..2c8ab3671be 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -32,6 +32,7 @@ export default util.createRule({ "Requires that private members are marked as `readonly` if they're never modified outside of the constructor", category: 'Best Practices', recommended: false, + requiresTypeChecking: true, }, fixable: 'code', messages: { diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index bd1a48fcb61..fea08554c3a 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -13,6 +13,7 @@ export default createRule({ 'Prefer RegExp#exec() over String#match() if no global flag is provided', category: 'Best Practices', recommended: 'error', + requiresTypeChecking: true, }, messages: { regExpExecOverStringMatch: 'Use the `RegExp#exec()` method instead.', diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index 2ffac6728ac..56344783b34 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -21,6 +21,7 @@ export default createRule({ 'Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings', category: 'Best Practices', recommended: 'error', + requiresTypeChecking: true, }, messages: { preferStartsWith: "Use 'String#startsWith' method instead.", diff --git a/packages/eslint-plugin/src/rules/promise-function-async.ts b/packages/eslint-plugin/src/rules/promise-function-async.ts index faf55f95fa7..225b2bd8367 100644 --- a/packages/eslint-plugin/src/rules/promise-function-async.ts +++ b/packages/eslint-plugin/src/rules/promise-function-async.ts @@ -22,6 +22,7 @@ export default util.createRule({ 'Requires any function or method that returns a Promise to be marked async', category: 'Best Practices', recommended: false, + requiresTypeChecking: true, }, messages: { missingAsync: 'Functions that return promises must be async.', diff --git a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts index 5b3f430420f..e88f2791f8f 100644 --- a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts +++ b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts @@ -12,6 +12,7 @@ export default util.createRule({ description: 'Enforce giving `compare` argument to `Array#sort`', category: 'Best Practices', recommended: false, + requiresTypeChecking: true, }, messages: { requireCompare: "Require 'compare' argument.", diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index ef1092b46db..7441b464896 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -24,6 +24,7 @@ export default util.createRule({ description: 'Disallow async functions which have no `await` expression', category: 'Best Practices', recommended: 'error', + requiresTypeChecking: true, }, schema: baseRule.meta.schema, messages: baseRule.meta.messages, diff --git a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts index df523033bf7..1f715f1f902 100644 --- a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts +++ b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts @@ -11,6 +11,7 @@ export default util.createRule({ 'When adding two variables, operands must both be of type number or of type string', category: 'Best Practices', recommended: false, + requiresTypeChecking: true, }, messages: { notNumbers: diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index d9077bf3154..9c3c5b6ea98 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -27,6 +27,7 @@ export default util.createRule({ description: 'Restricts the types allowed in boolean expressions', category: 'Best Practices', recommended: false, + requiresTypeChecking: true, }, schema: [ { diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 0db82b2c770..c76ac76f9d0 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -26,6 +26,7 @@ export default util.createRule({ description: 'Enforces unbound methods are called with their expected scope', recommended: 'error', + requiresTypeChecking: true, }, messages: { unbound: diff --git a/packages/eslint-plugin/tools/generate-configs.ts b/packages/eslint-plugin/tools/generate-configs.ts index d7f5fb29afd..cda1b0771ea 100644 --- a/packages/eslint-plugin/tools/generate-configs.ts +++ b/packages/eslint-plugin/tools/generate-configs.ts @@ -58,6 +58,7 @@ function reducer( settings: { errorLevel?: 'error' | 'warn'; filterDeprecated: boolean; + filterRequiresTypeChecking?: 'include' | 'exclude'; }, ): LinterConfigRules { const key = entry[0]; @@ -67,6 +68,22 @@ function reducer( return config; } + // Explicitly exclude rules requiring type-checking + if ( + settings.filterRequiresTypeChecking === 'exclude' && + value.meta.docs.requiresTypeChecking === true + ) { + return config; + } + + // Explicitly include rules requiring type-checking + if ( + settings.filterRequiresTypeChecking === 'include' && + value.meta.docs.requiresTypeChecking !== true + ) { + return config; + } + const ruleName = `${RULE_NAME_PREFIX}${key}`; const recommendation = value.meta.docs.recommended; const usedSetting = settings.errorLevel @@ -119,7 +136,7 @@ writeConfig(baseConfig, path.resolve(__dirname, '../src/configs/base.json')); console.log(); console.log( - '---------------------------------- all.json ----------------------------------', + '------------------------------------------------ all.json ------------------------------------------------', ); const allConfig: LinterConfig = { extends: './configs/base.json', @@ -133,12 +150,16 @@ writeConfig(allConfig, path.resolve(__dirname, '../src/configs/all.json')); console.log(); console.log( - '------------------------------ recommended.json ------------------------------', + '------------------------------ recommended.json (should not require program) ------------------------------', ); const recommendedRules = ruleEntries .filter(entry => !!entry[1].meta.docs.recommended) .reduce( - (config, entry) => reducer(config, entry, { filterDeprecated: false }), + (config, entry) => + reducer(config, entry, { + filterDeprecated: false, + filterRequiresTypeChecking: 'exclude', + }), {}, ); BASE_RULES_THAT_ARE_RECOMMENDED.forEach(ruleName => { @@ -152,3 +173,32 @@ writeConfig( recommendedConfig, path.resolve(__dirname, '../src/configs/recommended.json'), ); + +console.log(); +console.log( + '--------------------------------- recommended-requiring-type-checking.json ---------------------------------', +); +const recommendedRulesRequiringProgram = ruleEntries + .filter(entry => !!entry[1].meta.docs.recommended) + .reduce( + (config, entry) => + reducer(config, entry, { + filterDeprecated: false, + filterRequiresTypeChecking: 'include', + }), + {}, + ); +BASE_RULES_THAT_ARE_RECOMMENDED.forEach(ruleName => { + recommendedRulesRequiringProgram[ruleName] = 'error'; +}); +const recommendedRequiringTypeCheckingConfig: LinterConfig = { + extends: './configs/base.json', + rules: recommendedRulesRequiringProgram, +}; +writeConfig( + recommendedRequiringTypeCheckingConfig, + path.resolve( + __dirname, + '../src/configs/recommended-requiring-type-checking.json', + ), +); diff --git a/packages/eslint-plugin/tools/validate-configs/checkConfigRecommended.ts b/packages/eslint-plugin/tools/validate-configs/checkConfigRecommended.ts index ae256143112..28f0c7b21a5 100644 --- a/packages/eslint-plugin/tools/validate-configs/checkConfigRecommended.ts +++ b/packages/eslint-plugin/tools/validate-configs/checkConfigRecommended.ts @@ -10,7 +10,11 @@ function checkConfigRecommended(): boolean { const recommendedNames = new Set(Object.keys(recommended)); return Object.entries(rules).reduce((acc, [ruleName, rule]) => { - if (!rule.meta.deprecated && rule.meta.docs.recommended !== false) { + if ( + !rule.meta.deprecated && + rule.meta.docs.recommended !== false && + rule.meta.docs.requiresTypeChecking !== true + ) { const prefixed = `${prefix}${ruleName}` as keyof typeof recommended; if (recommendedNames.has(prefixed)) { if (recommended[prefixed] !== rule.meta.docs.recommended) { diff --git a/packages/eslint-plugin/tools/validate-configs/checkConfigRecommendedRequiringTypeChecking.ts b/packages/eslint-plugin/tools/validate-configs/checkConfigRecommendedRequiringTypeChecking.ts new file mode 100644 index 00000000000..a63f5e42c23 --- /dev/null +++ b/packages/eslint-plugin/tools/validate-configs/checkConfigRecommendedRequiringTypeChecking.ts @@ -0,0 +1,45 @@ +import plugin from '../../src/index'; +import { logRule } from '../log'; + +const prefix = '@typescript-eslint/'; + +function checkConfigRecommendedRequiringTypeChecking(): boolean { + const { rules } = plugin; + + const recommendedRequiringTypeChecking = + plugin.configs['recommended-requiring-type-checking'].rules; + const recommendedNames = new Set( + Object.keys(recommendedRequiringTypeChecking), + ); + + return Object.entries(rules).reduce((acc, [ruleName, rule]) => { + if ( + !rule.meta.deprecated && + rule.meta.docs.recommended !== false && + rule.meta.docs.requiresTypeChecking === true + ) { + const prefixed = `${prefix}${ruleName}` as keyof typeof recommendedRequiringTypeChecking; + if (recommendedNames.has(prefixed)) { + if ( + recommendedRequiringTypeChecking[prefixed] !== + rule.meta.docs.recommended + ) { + logRule( + false, + ruleName, + 'incorrect setting compared to the rule meta.', + ); + return true; + } + } else { + logRule(false, ruleName, 'missing in the config.'); + return true; + } + } + + logRule(true, ruleName); + return acc; + }, false); +} + +export { checkConfigRecommendedRequiringTypeChecking }; diff --git a/packages/eslint-plugin/tools/validate-configs/index.ts b/packages/eslint-plugin/tools/validate-configs/index.ts index cb64df53648..1c05196a677 100644 --- a/packages/eslint-plugin/tools/validate-configs/index.ts +++ b/packages/eslint-plugin/tools/validate-configs/index.ts @@ -1,11 +1,17 @@ import chalk from 'chalk'; import { checkConfigRecommended } from './checkConfigRecommended'; +import { checkConfigRecommendedRequiringTypeChecking } from './checkConfigRecommendedRequiringTypeChecking'; import { checkConfigAll } from './checkConfigAll'; let hasErrors = false; console.log(chalk.underline('Checking config "recommended"')); hasErrors = checkConfigRecommended() || hasErrors; +console.log( + chalk.underline('Checking config "recommended-requiring-type-checking"'), +); +hasErrors = checkConfigRecommendedRequiringTypeChecking() || hasErrors; + console.log(); console.log(chalk.underline('Checking config "all"')); hasErrors = checkConfigAll() || hasErrors; diff --git a/packages/eslint-plugin/tools/validate-docs/validate-table-rules.ts b/packages/eslint-plugin/tools/validate-docs/validate-table-rules.ts index c3e712ca525..b85172d0fdc 100644 --- a/packages/eslint-plugin/tools/validate-docs/validate-table-rules.ts +++ b/packages/eslint-plugin/tools/validate-docs/validate-table-rules.ts @@ -105,7 +105,31 @@ function validateTableRules( const ruleFileContents = fs.readFileSync( path.resolve(__dirname, `../../src/rules/${ruleName}.ts`), ); + const usesTypeInformation = ruleFileContents.includes('getParserServices'); + const tableRowHasThoughtBalloon = !!rowNeedsTypeInfo; + if (rule.meta.docs.requiresTypeChecking === true) { + if (!usesTypeInformation) { + errors.push( + 'Rule has `requiresTypeChecking` set in its meta, but it does not actually use type information - fix by removing `meta.docs.requiresTypeChecking`', + ); + } else if (!tableRowHasThoughtBalloon) { + errors.push( + 'Rule was documented as not using type information, when it actually does - fix by updating the plugin README.md', + ); + } + } else { + if (usesTypeInformation) { + errors.push( + 'Rule does not have `requiresTypeChecking` set in its meta, despite using type information - fix by setting `meta.docs.requiresTypeChecking: true` in the rule', + ); + } else if (tableRowHasThoughtBalloon) { + errors.push( + `Rule was documented as using type information, when it actually doesn't - fix by updating the plugin README.md`, + ); + } + } + validateTableBoolean( usesTypeInformation, rowNeedsTypeInfo, diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index 89ac7b53288..75acea5988c 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -32,6 +32,11 @@ interface RuleMetaDataDocs { * The URL of the rule's docs */ url: string; + /** + * Does the rule require us to create a full TypeScript Program in order for it + * to type-check code. This is only used for documentation purposes. + */ + requiresTypeChecking?: boolean; } interface RuleMetaData { /** diff --git a/tests/integration/docker-compose.yml b/tests/integration/docker-compose.yml index 5d10d766177..74bfb63dab2 100644 --- a/tests/integration/docker-compose.yml +++ b/tests/integration/docker-compose.yml @@ -36,3 +36,20 @@ services: - /usr/eslint-plugin/tests # Runtime link to all the specific integration test files, so that most updates don't require a rebuild. - ./fixtures/vue-sfc:/usr/linked + + recommended-does-not-require-program: + build: ./fixtures/recommended-does-not-require-program + container_name: "recommended-does-not-require-program" + volumes: + # Runtime link to the relevant built @typescript-eslint packages and integration test utils, + # but apply an empty volume for the package tests, we don't need those. + - ../../package.json/:/usr/root-package.json + - ./utils/:/usr/utils + - ../../packages/parser/:/usr/parser + - /usr/parser/tests + - ../../packages/typescript-estree/:/usr/typescript-estree + - /usr/typescript-estree/tests + - ../../packages/eslint-plugin/:/usr/eslint-plugin + - /usr/eslint-plugin/tests + # Runtime link to all the specific integration test files, so that most updates don't require a rebuild. + - ./fixtures/recommended-does-not-require-program:/usr/linked diff --git a/tests/integration/fixtures/recommended-does-not-require-program/.eslintrc.yml b/tests/integration/fixtures/recommended-does-not-require-program/.eslintrc.yml new file mode 100644 index 00000000000..75de006c68d --- /dev/null +++ b/tests/integration/fixtures/recommended-does-not-require-program/.eslintrc.yml @@ -0,0 +1,17 @@ +# This integration test exists to make sure that the recommended config does +# not require a program to be specified to ensure a fast and simple initial +# setup. Users can add on one of our other configs if they want to opt in to +# more expensive checks. +root: true + +# Local version of @typescript-eslint/parser +parser: '@typescript-eslint/parser' + +extends: +- 'eslint:recommended' +- 'plugin:@typescript-eslint/eslint-recommended' +- 'plugin:@typescript-eslint/recommended' + +plugins: +# Local version of @typescript-eslint/eslint-plugin +- '@typescript-eslint' diff --git a/tests/integration/fixtures/recommended-does-not-require-program/Dockerfile b/tests/integration/fixtures/recommended-does-not-require-program/Dockerfile new file mode 100644 index 00000000000..3b281e624c8 --- /dev/null +++ b/tests/integration/fixtures/recommended-does-not-require-program/Dockerfile @@ -0,0 +1,17 @@ +FROM node:carbon + +# Copy the test.sh into the container. Every other file will be linked, rather +# than copied to allow for changes without rebuilds wherever possible +WORKDIR /usr +COPY ./test.sh /usr/ + +# Create file which will be executed by jest +# to assert that the lint output is what we expect +RUN echo "const actualLintOutput = require('./lint-output.json');\n" \ + "\n" \ + "test('it should produce the expected lint ouput', () => {\n" \ + " expect(actualLintOutput).toMatchSnapshot();\n" \ + "});\n" > test.js + +# Run the integration test +CMD [ "./test.sh" ] diff --git a/tests/integration/fixtures/recommended-does-not-require-program/index.ts b/tests/integration/fixtures/recommended-does-not-require-program/index.ts new file mode 100644 index 00000000000..838b04b9475 --- /dev/null +++ b/tests/integration/fixtures/recommended-does-not-require-program/index.ts @@ -0,0 +1 @@ +var foo = true diff --git a/tests/integration/fixtures/recommended-does-not-require-program/test.js.snap b/tests/integration/fixtures/recommended-does-not-require-program/test.js.snap new file mode 100644 index 00000000000..36d059733d5 --- /dev/null +++ b/tests/integration/fixtures/recommended-does-not-require-program/test.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it should produce the expected lint ouput 1`] = ` +Array [ + Object { + "errorCount": 1, + "filePath": "/usr/linked/index.ts", + "fixableErrorCount": 1, + "fixableWarningCount": 0, + "messages": Array [ + Object { + "column": 1, + "endColumn": 15, + "endLine": 1, + "fix": Object { + "range": Array [ + 0, + 3, + ], + "text": "let", + }, + "line": 1, + "message": "Unexpected var, use let or const instead.", + "nodeType": "VariableDeclaration", + "ruleId": "no-var", + "severity": 2, + }, + Object { + "column": 5, + "endColumn": 8, + "endLine": 1, + "line": 1, + "message": "'foo' is assigned a value but never used.", + "nodeType": "Identifier", + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 1, + }, + ], + "source": "var foo = true +", + "warningCount": 1, + }, +] +`; diff --git a/tests/integration/fixtures/recommended-does-not-require-program/test.sh b/tests/integration/fixtures/recommended-does-not-require-program/test.sh new file mode 100755 index 00000000000..1fa77f5cbdf --- /dev/null +++ b/tests/integration/fixtures/recommended-does-not-require-program/test.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Generate the package.json to use +node /usr/utils/generate-package-json.js + +# Install dependencies +npm install + +# Use the local volumes for our own packages +npm install $(npm pack /usr/typescript-estree | tail -1) +npm install $(npm pack /usr/parser | tail -1) +npm install $(npm pack /usr/eslint-plugin | tail -1) + +# Run the linting +# (the "|| true" helps make sure that we run our tests on failed linting runs as well) +npx eslint --format json --output-file /usr/lint-output.json --config /usr/linked/.eslintrc.yml /usr/linked/**/*.ts || true + +# Run our assertions against the linting output +npx jest /usr/test.js --snapshotResolver=/usr/utils/jest-snapshot-resolver.js diff --git a/tests/integration/run-all-tests.sh b/tests/integration/run-all-tests.sh index 5b43af06246..4506da77349 100755 --- a/tests/integration/run-all-tests.sh +++ b/tests/integration/run-all-tests.sh @@ -9,3 +9,6 @@ docker-compose -f tests/integration/docker-compose.yml up --build --abort-on-con # vue-sfc docker-compose -f tests/integration/docker-compose.yml up --build --abort-on-container-exit vue-sfc + +# recommended-does-not-require-program +docker-compose -f tests/integration/docker-compose.yml up --build --abort-on-container-exit recommended-does-not-require-program