From ee060d9b23dddab6832558ab62a7a83701be43c7 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 27 Feb 2020 09:36:09 -0800 Subject: [PATCH] feat(eslint-plugin): add rule no-unsafe-call --- packages/eslint-plugin/README.md | 1 + .../docs/rules/no-unsafe-call.md | 36 ++++++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../eslint-plugin/src/rules/no-unsafe-call.ts | 38 +++++++++ .../tests/rules/no-unsafe-call.test.ts | 85 +++++++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-unsafe-call.md create mode 100644 packages/eslint-plugin/src/rules/no-unsafe-call.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 4b30cf49b791..ac10a378edbb 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -131,6 +131,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | | | :thought_balloon: | | [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | | | :thought_balloon: | | [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | | | :thought_balloon: | | [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-call.md b/packages/eslint-plugin/docs/rules/no-unsafe-call.md new file mode 100644 index 000000000000..23750e11c519 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-call.md @@ -0,0 +1,36 @@ +# Disallows calling an any type value (`no-unsafe-call`) + +Despite your best intentions, the `any` type can sometimes leak into your codebase. +Member access on `any` typed variables is not checked at all by TypeScript, so it creates a potential safety hole, and source of bugs in your codebase. + +## Rule Details + +This rule disallows calling any variable that is typed as `any`. + +Examples of **incorrect** code for this rule: + +```ts +declare const anyVar: any; +declare const nestedAny: { prop: any }; + +anyVar(); +anyVar.a.b(); + +nestedAny.prop(); +nestedAny.prop['a'](); +``` + +Examples of **correct** code for this rule: + +```ts +declare const properlyTyped: { prop: { a: () => void } }; + +nestedAny.prop.a(); + +(() => {})(); +``` + +## Related to + +- [`no-explicit-any`](./no-explicit-any.md) +- TSLint: [`no-unsafe-any`](https://palantir.github.io/tslint/rules/no-unsafe-any/) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 1e2be87bff0f..a7a1bd0a6c52 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -60,6 +60,7 @@ "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-member-access": "error", "@typescript-eslint/no-unsafe-return": "error", "no-unused-expressions": "off", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 2ee036489991..bdd2d5e29a0b 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -51,6 +51,7 @@ import noUnnecessaryBooleanLiteralCompare from './no-unnecessary-boolean-literal import noUnnecessaryCondition from './no-unnecessary-condition'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; +import noUnsafeCall from './no-unsafe-call'; import noUnsafeMemberAccess from './no-unsafe-member-access'; import noUnsafeReturn from './no-unsafe-return'; import noUntypedPublicSignature from './no-untyped-public-signature'; @@ -143,6 +144,7 @@ export default { 'no-unnecessary-qualifier': noUnnecessaryQualifier, 'no-unnecessary-type-arguments': useDefaultTypeParameter, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, + 'no-unsafe-call': noUnsafeCall, 'no-unsafe-member-access': noUnsafeMemberAccess, 'no-unsafe-return': noUnsafeReturn, 'no-untyped-public-signature': noUntypedPublicSignature, diff --git a/packages/eslint-plugin/src/rules/no-unsafe-call.ts b/packages/eslint-plugin/src/rules/no-unsafe-call.ts new file mode 100644 index 000000000000..a1f08a85d1f9 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-call.ts @@ -0,0 +1,38 @@ +import * as util from '../util'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +export default util.createRule({ + name: 'no-unsafe-call', + meta: { + type: 'problem', + docs: { + description: 'Disallows calling an any type value', + category: 'Possible Errors', + recommended: false, + requiresTypeChecking: true, + }, + messages: { + unsafeCall: 'Unsafe call of an any typed value', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context); + const checker = program.getTypeChecker(); + + return { + 'CallExpression, OptionalCallExpression'( + node: TSESTree.CallExpression | TSESTree.OptionalCallExpression, + ): void { + const tsNode = esTreeNodeToTSNodeMap.get(node.callee); + if (util.isAnyType(tsNode, checker)) { + context.report({ + node: node.callee, + messageId: 'unsafeCall', + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts new file mode 100644 index 000000000000..a94facb95ee2 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts @@ -0,0 +1,85 @@ +import rule from '../../src/rules/no-unsafe-call'; +import { + RuleTester, + batchedSingleLineTests, + getFixturesRootDir, +} from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: getFixturesRootDir(), + }, +}); + +ruleTester.run('no-unsafe-call', rule, { + valid: [ + 'function foo(x: () => void) { x() }', + 'function foo(x?: { a: () => void }) { x?.a() }', + 'function foo(x: { a?: () => void }) { x.a?.() }', + ], + invalid: [ + ...batchedSingleLineTests({ + code: ` +function foo(x: any) { x() } +function foo(x: any) { x?.() } +function foo(x: any) { x.a.b.c.d.e.f.g() } +function foo(x: any) { x.a.b.c.d.e.f.g?.() } + `, + errors: [ + { + messageId: 'unsafeCall', + line: 2, + column: 24, + endColumn: 25, + }, + { + messageId: 'unsafeCall', + line: 3, + column: 24, + endColumn: 25, + }, + { + messageId: 'unsafeCall', + line: 4, + column: 24, + endColumn: 39, + }, + { + messageId: 'unsafeCall', + line: 5, + column: 24, + endColumn: 39, + }, + ], + }), + ...batchedSingleLineTests({ + code: ` +function foo(x: { a: any }) { x.a() } +function foo(x: { a: any }) { x?.a() } +function foo(x: { a: any }) { x.a?.() } + `, + errors: [ + { + messageId: 'unsafeCall', + line: 2, + column: 31, + endColumn: 34, + }, + { + messageId: 'unsafeCall', + line: 3, + column: 31, + endColumn: 35, + }, + { + messageId: 'unsafeCall', + line: 4, + column: 31, + endColumn: 34, + }, + ], + }), + ], +});