Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin)!: recommended-requiring-type-checking config #846

Merged
merged 5 commits into from Aug 13, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -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: {
//
Expand Down
21 changes: 19 additions & 2 deletions packages/eslint-plugin/README.md
Expand Up @@ -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
{
Expand All @@ -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
{
Expand Down
6 changes: 3 additions & 3 deletions packages/eslint-plugin/package.json
Expand Up @@ -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"
Expand Down
@@ -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"
}
}
10 changes: 0 additions & 10 deletions packages/eslint-plugin/src/configs/recommended.json
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/index.ts
Expand Up @@ -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 = {
Expand All @@ -12,5 +13,6 @@ export = {
base,
recommended,
'eslint-recommended': eslintRecommended,
'recommended-requiring-type-checking': recommendedRequiringTypeChecking,
},
};
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/await-thenable.ts
Expand Up @@ -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.',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/no-for-in-array.ts
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/no-misused-promises.ts
Expand Up @@ -18,6 +18,7 @@ export default util.createRule<Options, 'conditional' | 'voidReturn'>({
description: 'Avoid using promises in places not designed to handle them',
category: 'Best Practices',
recommended: 'error',
requiresTypeChecking: true,
},
messages: {
voidReturn:
Expand Down
Expand Up @@ -28,6 +28,7 @@ export default util.createRule<Options, MessageIds>({
'Warns if a type assertion does not change the type of an expression',
category: 'Best Practices',
recommended: 'error',
requiresTypeChecking: true,
},
fixable: 'code',
messages: {
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/prefer-includes.ts
Expand Up @@ -14,6 +14,7 @@ export default createRule({
description: 'Enforce `includes` method over `indexOf` method',
category: 'Best Practices',
recommended: 'error',
requiresTypeChecking: true,
},
fixable: 'code',
messages: {
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/prefer-regexp-exec.ts
Expand Up @@ -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.',
Expand Down
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/require-await.ts
Expand Up @@ -24,6 +24,7 @@ export default util.createRule<Options, MessageIds>({
description: 'Disallow async functions which have no `await` expression',
category: 'Best Practices',
recommended: 'error',
requiresTypeChecking: true,
},
schema: baseRule.meta.schema,
messages: baseRule.meta.messages,
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/unbound-method.ts
Expand Up @@ -26,6 +26,7 @@ export default util.createRule<Options, MessageIds>({
description:
'Enforces unbound methods are called with their expected scope',
recommended: 'error',
requiresTypeChecking: true,
},
messages: {
unbound:
Expand Down
56 changes: 53 additions & 3 deletions packages/eslint-plugin/tools/generate-configs.ts
Expand Up @@ -58,6 +58,7 @@ function reducer<TMessageIds extends string>(
settings: {
errorLevel?: 'error' | 'warn';
filterDeprecated: boolean;
filterRequiresTypeChecking?: 'include' | 'exclude';
},
): LinterConfigRules {
const key = entry[0];
Expand All @@ -67,6 +68,22 @@ function reducer<TMessageIds extends string>(
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
Expand Down Expand Up @@ -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',
Expand All @@ -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<LinterConfigRules>(
(config, entry) => reducer(config, entry, { filterDeprecated: false }),
(config, entry) =>
reducer(config, entry, {
filterDeprecated: false,
filterRequiresTypeChecking: 'exclude',
}),
{},
);
BASE_RULES_THAT_ARE_RECOMMENDED.forEach(ruleName => {
Expand All @@ -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<LinterConfigRules>(
(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',
),
);
Expand Up @@ -10,7 +10,11 @@ function checkConfigRecommended(): boolean {
const recommendedNames = new Set(Object.keys(recommended));

return Object.entries(rules).reduce<boolean>((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) {
Expand Down
@@ -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<boolean>((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 };
6 changes: 6 additions & 0 deletions 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;
Expand Down
5 changes: 5 additions & 0 deletions packages/experimental-utils/src/ts-eslint/Rule.ts
Expand Up @@ -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
JamesHenry marked this conversation as resolved.
Show resolved Hide resolved
*/
requiresTypeChecking?: boolean;
}
interface RuleMetaData<TMessageIds extends string> {
/**
Expand Down