Skip to content

Commit

Permalink
feat(eslint-plugin): add rule no-unsafe-call (#1647)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Mar 3, 2020
1 parent cfc3ef1 commit 91423e4
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -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: |
Expand Down
41 changes: 41 additions & 0 deletions 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/)
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
52 changes: 52 additions & 0 deletions 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');
},
};
},
});
106 changes: 106 additions & 0 deletions 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,
},
],
}),
],
});

0 comments on commit 91423e4

Please sign in to comment.