From 91423e49d19163fae7b03cbc79bb3cd3db8c2c6d Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 3 Mar 2020 01:26:29 -0800 Subject: [PATCH] feat(eslint-plugin): add rule no-unsafe-call (#1647) --- packages/eslint-plugin/README.md | 1 + .../docs/rules/no-unsafe-call.md | 41 +++++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../eslint-plugin/src/rules/no-unsafe-call.ts | 52 +++++++++ .../tests/rules/no-unsafe-call.test.ts | 106 ++++++++++++++++++ 6 files changed, 203 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 7e956af5742..b7927757924 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -132,6 +132,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 00000000000..7a9cdb79f54 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-call.md @@ -0,0 +1,41 @@ +# 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'](); + +new anyVar(); +new nestedAny.prop(); +``` + +Examples of **correct** code for this rule: + +```ts +declare const properlyTyped: { prop: { a: () => void } }; + +nestedAny.prop.a(); + +(() => {})(); + +new Map(); +``` + +## 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 f4a7bd43562..2626767b84c 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -61,6 +61,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 0befe184fbe..486b8a97945 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -53,6 +53,7 @@ import noUnnecessaryCondition from './no-unnecessary-condition'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments'; 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'; @@ -146,6 +147,7 @@ export default { 'no-unnecessary-qualifier': noUnnecessaryQualifier, 'no-unnecessary-type-arguments': noUnnecessaryTypeArguments, '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 00000000000..906a82d53d2 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-call.ts @@ -0,0 +1,52 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +type MessageIds = 'unsafeCall' | 'unsafeNew'; + +export default util.createRule<[], MessageIds>({ + 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', + unsafeNew: 'Unsafe construction of an any type value', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context); + const checker = program.getTypeChecker(); + + function checkCall( + node: + | TSESTree.CallExpression + | TSESTree.OptionalCallExpression + | TSESTree.NewExpression, + reportingNode: TSESTree.Expression = node.callee, + messageId: MessageIds = 'unsafeCall', + ): void { + const tsNode = esTreeNodeToTSNodeMap.get(node.callee); + const type = checker.getTypeAtLocation(tsNode); + if (util.isTypeAnyType(type)) { + context.report({ + node: reportingNode, + messageId: messageId, + }); + } + } + + return { + 'CallExpression, OptionalCallExpression': checkCall, + NewExpression(node): void { + checkCall(node, node, 'unsafeNew'); + }, + }; + }, +}); 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 00000000000..b5fc6408ed6 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts @@ -0,0 +1,106 @@ +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?.() }', + 'new Map()', + ], + 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, + }, + ], + }), + ...batchedSingleLineTests({ + code: ` +function foo(x: any) { new x() } +function foo(x: { a: any }) { new x.a() } + `, + errors: [ + { + messageId: 'unsafeNew', + line: 2, + column: 24, + endColumn: 31, + }, + { + messageId: 'unsafeNew', + line: 3, + column: 31, + endColumn: 40, + }, + ], + }), + ], +});