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): add no-implicit-any-catch rule #2202

Merged
merged 18 commits into from Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from 7 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 packages/eslint-plugin/README.md
Expand Up @@ -122,6 +122,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | |
| [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-implicit-any-catch`](./docs/rules/no-implicit-any-catch.md) | Disallow usage of the implicit `any` type in catch clauses | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/no-invalid-void-type`](./docs/rules/no-invalid-void-type.md) | Disallows usage of `void` type outside of generic or return types | | | |
Expand Down
57 changes: 57 additions & 0 deletions packages/eslint-plugin/docs/rules/no-implicit-any-catch.md
@@ -0,0 +1,57 @@
# Disallow usage of the implicit `any` type in catch clauses (`no-implicit-any-catch`)

Using the `any` type defeats the purpose of using TypeScript.
When `any` is used, all compiler type checks around that value are ignored.

The noImplicitAny flag in TypeScript does not cover this due to backwards compatibility reasons.

## Rule Details

This rule requires an explicit type to be declared in the catch clause error argument.

The following pattern is considered a warning:

```ts
try {
// ...
} catch (e) {
// ...
}
```

The following patterns are not warnings:

```ts
try {
// ...
} catch (e) {
phiresky marked this conversation as resolved.
Show resolved Hide resolved
// ...
}
```

```ts
try {
// ...
} catch (e) {
phiresky marked this conversation as resolved.
Show resolved Hide resolved
// ...
}
```

## Options

The rule accepts an options object with the following properties:

```ts
type Options = {
// if false, disallow specifying : any as the error type as well. See also `no-explicit-any`
allowExplicitAny: boolean;
};

const defaults = {
allowExplicitAny: true,
};
```

## Further Reading

- The original issue report for allowing `: unknown` in error clauses: https://github.com/microsoft/TypeScript/issues/36775
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -54,6 +54,7 @@ export = {
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-for-in-array': 'error',
'@typescript-eslint/no-implicit-any-catch': 'error',
'@typescript-eslint/no-implied-eval': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'no-invalid-this': 'off',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/recommended.ts
Expand Up @@ -10,6 +10,7 @@ export = {
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-implicit-any-catch': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-extra-non-null-assertion': 'error',
'no-extra-semi': 'off',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -29,6 +29,7 @@ import noDynamicDelete from './no-dynamic-delete';
import noEmptyFunction from './no-empty-function';
import noEmptyInterface from './no-empty-interface';
import noExplicitAny from './no-explicit-any';
import noImplicitAnyCatch from './no-implicit-any-catch';
import noExtraneousClass from './no-extraneous-class';
import noExtraNonNullAssertion from './no-extra-non-null-assertion';
import noExtraParens from './no-extra-parens';
Expand Down Expand Up @@ -131,6 +132,7 @@ export default {
'no-empty-function': noEmptyFunction,
'no-empty-interface': noEmptyInterface,
'no-explicit-any': noExplicitAny,
'no-implicit-any-catch': noImplicitAnyCatch,
'no-extra-non-null-assertion': noExtraNonNullAssertion,
'no-extra-parens': noExtraParens,
'no-extra-semi': noExtraSemi,
Expand Down
95 changes: 95 additions & 0 deletions packages/eslint-plugin/src/rules/no-implicit-any-catch.ts
@@ -0,0 +1,95 @@
import * as util from '../util';
import {
TSESLint,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';

export type Options = [
{
allowExplicitAny: boolean;
},
];
export type MessageIds =
| 'implicitAnyInCatch'
| 'explicitAnyInCatch'
| 'suggestExplicitUnknown';

export default util.createRule<Options, MessageIds>({
name: 'no-implicit-any-catch',
meta: {
type: 'suggestion',
docs: {
description: 'Disallow usage of the implicit `any` type in catch clauses',
category: 'Best Practices',
recommended: false,
suggestion: true,
},
fixable: 'code',
messages: {
implicitAnyInCatch: 'Implicit any in catch clause',
explicitAnyInCatch: 'Explicit any in catch clause',
suggestExplicitUnknown:
'Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct.',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
allowExplicitAny: {
type: 'boolean',
},
},
},
],
},
defaultOptions: [
{
allowExplicitAny: true,
},
],
create(context, [{ allowExplicitAny }]) {
return {
CatchClause(node): void {
if (!node.param) {
return; // ignore catch without variable
}

if (!node.param.typeAnnotation) {
context.report({
node,
messageId: 'implicitAnyInCatch',
suggest: [
{
messageId: 'suggestExplicitUnknown',
fix(fixer): TSESLint.RuleFix {
return fixer.insertTextAfter(node.param!, ': unknown');
},
},
],
});
} else if (
!allowExplicitAny &&
node.param.typeAnnotation.typeAnnotation.type ===
AST_NODE_TYPES.TSAnyKeyword
) {
context.report({
node,
messageId: 'explicitAnyInCatch',
suggest: [
{
messageId: 'suggestExplicitUnknown',
fix(fixer): TSESLint.RuleFix {
return fixer.replaceText(
node.param!.typeAnnotation!,
': unknown',
);
},
},
],
});
}
},
};
},
});
73 changes: 73 additions & 0 deletions packages/eslint-plugin/tests/rules/no-implicit-any-catch.test.ts
@@ -0,0 +1,73 @@
import rule from '../../src/rules/no-implicit-any-catch';
import { RuleTester } from '../RuleTester';

//type InvalidTestCase = TSESLint.InvalidTestCase<MessageIds, Options>;
//type SuggestionOutput = TSESLint.SuggestionOutput<MessageIds>;

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('no-implicit-any-catch', rule, {
valid: [
`
try {
} catch (e1: unknown) {}
`,
`
try {
} catch (e2: any) {}
`,
],
invalid: [
{
code: `
try {
} catch (e3) {}
`.trim(),
errors: [
{
line: 2,
column: 3,
messageId: 'implicitAnyInCatch',
endLine: 2,
endColumn: 16,
suggestions: [
{
messageId: 'suggestExplicitUnknown',
output: `
try {
} catch (e3: unknown) {}
`.trim(),
},
],
},
],
},
{
code: `
try {
} catch (e4: any) {}
`.trim(),
options: [{ allowExplicitAny: false }],
errors: [
{
line: 2,
column: 3,
messageId: 'explicitAnyInCatch',
endLine: 2,
endColumn: 21,
suggestions: [
{
messageId: 'suggestExplicitUnknown',
output: `
try {
} catch (e4: unknown) {}
`.trim(),
},
],
},
],
},
],
});
@@ -0,0 +1,5 @@
try {

} catch (e: any) {

}
@@ -0,0 +1,5 @@
try {

} catch (e: unknown) {

}
2 changes: 1 addition & 1 deletion packages/typescript-estree/src/convert.ts
Expand Up @@ -697,7 +697,7 @@ export class Converter {
return this.createNode<TSESTree.CatchClause>(node, {
type: AST_NODE_TYPES.CatchClause,
param: node.variableDeclaration
? this.convertChild(node.variableDeclaration.name)
? this.convertChild(node.variableDeclaration).id
phiresky marked this conversation as resolved.
Show resolved Hide resolved
: null,
body: this.convertChild(node.block),
});
Expand Down
Expand Up @@ -418,6 +418,11 @@ tester.addFixturePatternConfig('typescript/basics', {
* babel uses a representation that does not match the ESTree spec: https://github.com/estree/estree/pull/205
*/
'export-star-as-ns-from',
/**
* error catch types (TS 4.0), not yet supported in babel as of 2020-06-22
*/
'catch-type-any',
'catch-type-unknown',
],
ignoreSourceType: [
/**
Expand Down
Expand Up @@ -1736,6 +1736,10 @@ exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" e

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/cast-as-simple.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/catch-type-any.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/catch-type-unknown.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/class-multi-line-keyword-abstract.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/class-multi-line-keyword-declare.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;
Expand Down