8000 feat(eslint-plugin): [no-unsafe-argument] add rule (#3256) · javascripter/typescript-eslint@b1aa7dc · GitHub
[go: up one dir, main page]

Skip to content

Commit b1aa7dc

Browse files
authored
feat(eslint-plugin): [no-unsafe-argument] add rule (typescript-eslint#3256)
Fixes typescript-eslint#791
1 parent 62dfcc6 commit b1aa7dc

File tree

7 files changed

+562
-1
lines changed

7 files changed

+562
-1
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
143143
| [`@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: |
144144
| [`@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: |
145145
| [`@typescript-eslint/no-unnecessary-type-constraint`](./docs/rules/no-unnecessary-type-constraint.md) | Disallows unnecessary constraints on generic types | | :wrench: | |
146+
| [`@typescript-eslint/no-unsafe-argument`](./docs/rules/no-unsafe-argument.md) | Disallows calling an function with an any type value | | | :thought_balloon: |
146147
| [`@typescript-eslint/no-unsafe-assignment`](./docs/rules/no-unsafe-assignment.md) | Disallows assigning any to variables and properties | :heavy_check_mark: | | :thought_balloon: |
147148
| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | :heavy_check_mark: | | :thought_balloon: |
148149
| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :heavy_check_mark: | | :thought_balloon: |
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Disallows calling an function with an any type value (`no-unsafe-argument`)
2+
3+
Despite your best intentions, the `any` type can sometimes leak into your codebase.
4+
Call a function with `any` typed argument are not checked at all by TypeScript, so it creates a potential safety hole, and source of bugs in your codebase.
5+
6+
## Rule Details
7+
8+
This rule disallows calling a function with `any` in its arguments, and it will disallow spreading `any[]`.
9+
This rule also disallows spreading a tuple type with one of its elements typed as `any`.
10+
This rule also compares the argument's type to the variable's type to ensure you don't pass an unsafe `any` in a generic position to a receiver that's expecting a specific type. For example, it will error if you assign `Set<any>` to an argument declared as `Set<string>`.
11+
12+
Examples of **incorrect** code for this rule:
13+
14+
```ts
15+
declare function foo(arg1: string, arg2: number, arg2: string): void;
16+
17+
const anyTyped = 1 as any;
18+
19+
foo(...anyTyped);
20+
foo(anyTyped, 1, 'a');
21+
22+
const anyArray: any[] = [];
23+
foo(...anyArray);
24+
25+
const tuple1 = ['a', anyTyped, 'b'] as const;
26+
foo(...tuple1);
27+
28+
const tuple2 = [1] as const;
29+
foo('a', ...tuple, anyTyped);
30+
31+
declare function bar(arg1: string, arg2: number, ...rest: string[]): void;
32+
const x = [1, 2] as [number, ...number[]];
33+
foo('a', ...x, anyTyped);
34+
35+
declare function baz(arg1: Set<string>, arg2: Map<string, string>): void;
36+
foo(new Set<any>(), new Map<any, string>());
37+
```
38+
39+
Examples of **correct** code for this rule:
40+
41+
```ts
42+
declare function foo(arg1: string, arg2: number, arg2: string): void;
43+
44+
foo('a', 1, 'b');
45+
46+
const tuple1 = ['a', 1, 'b'] as const;
47+
foo(...tuple1);
48+
49+
declare function bar(arg1: string, arg2: number, ...rest: string[]): void;
50+
const array: string[] = ['a'];
51+
bar('a', 1, ...array);
52+
53+
declare function baz(arg1: Set<string>, arg2: Map<string, string>): void;
54+
foo(new Set<string>(), new Map<string, string>());
55+
```
56+
57+
There are cases where the rule allows passing an argument of `any` to `unknown`.
58+
59+
Example of `any` to `unknown` assignment that are allowed.
60+
61+
```ts
62+
declare function foo(arg1: unknown, arg2: Set<unkown>, arg3: unknown[]): void;
63+
foo(1 as any, new Set<any>(), [] as any[]);
64+
```
65+
66+
## Related to
67+
68+
- [`no-explicit-any`](./no-explicit-any.md)
69+
- TSLint: [`no-unsafe-any`](https://palantir.github.io/tslint/rules/no-unsafe-any/)

packages/eslint-plugin/docs/rules/no-unsafe-return.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Disallows returning any from a function (`no-unsafe-return`)
22

33
Despite your best intentions, the `any` type can sometimes leak into your codebase.
4-
Returned `any` typed values not checked at all by TypeScript, so it creates a potential safety hole, and source of bugs in your codebase.
4+
Returned `any` typed values are not checked at all by TypeScript, so it creates a potential safety hole, and source of bugs in your codebase.
55

66
## Rule Details
77

packages/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export = {
9999
'@typescript-eslint/no-unnecessary-type-arguments': 'error',
100100
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
101101
'@typescript-eslint/no-unnecessary-type-constraint': 'error',
102+
'@typescript-eslint/no-unsafe-argument': 'error',
102103
'@typescript-eslint/no-unsafe-assignment': 'error',
103104
'@typescript-eslint/no-unsafe-call': 'error',
104105
'@typescript-eslint/no-unsafe-member-access': 'error',

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import noUnnecessaryQualifier from './no-unnecessary-qualifier';
6868
import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments';
6969
import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion';
7070
import noUnnecessaryTypeConstraint from './no-unnecessary-type-constraint';
71+
import noUnsafeArgument from './no-unsafe-argument';
7172
import noUnsafeAssignment from './no-unsafe-assignment';
7273
import noUnsafeCall from './no-unsafe-call';
7374
import noUnsafeMemberAccess from './no-unsafe-member-access';
@@ -185,6 +186,7 @@ export default {
185186
'no-unnecessary-type-arguments': noUnnecessaryTypeArguments,
186187
'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion,
187188
'no-unnecessary-type-constraint': noUnnecessaryTypeConstraint,
189+
'no-unsafe-argument': noUnsafeArgument,
188190
'no-unsafe-assignment': noUnsafeAssignment,
189191
'no-unsafe-call': noUnsafeCall,
190192
'no-unsafe-member-access': noUnsafeMemberAccess,
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESTree,
4+
} from '@typescript-eslint/experimental-utils';
5+
import * as ts from 'typescript';
6+
import * as util from '../util';
7+
8+
type MessageIds =
9+
| 'unsafeArgument'
10+
| 'unsafeTupleSpread'
11+
| 'unsafeArraySpread'
12+
| 'unsafeSpread';
13+
14+
class FunctionSignature {
15+
public static create(
16+
checker: ts.TypeChecker,
17+
tsNode: ts.CallLikeExpression,
18+
): FunctionSignature | null {
19+
const signature = checker.getResolvedSignature(tsNode);
20+
if (!signature) {
21+
return null;
22+
}
23+
24+
const paramTypes: ts.Type[] = [];
25+
let restType: ts.Type | null = null;
26+
27+
for (const param of signature.getParameters()) {
28+
const type = checker.getTypeOfSymbolAtLocation(param, tsNode);
29+
30+
const decl = param.getDeclarations()?.[0];
31+
if (decl && ts.isParameter(decl) && decl.dotDotDotToken) {
32+
// is a rest param
33+
if (checker.isArrayType(type)) {
34+
restType = checker.getTypeArguments(type)[0];
35+
} else {
36+
restType = type;
37+
}
38+
break;
39+
}
40+
41+
paramTypes.push(type);
42+
}
43+
44+
return new this(paramTypes, restType);
45+
}
46+
47+
private hasConsumedArguments = false;
48+
49+
private constructor(
50+
private paramTypes: ts.Type[],
51+
private restType: ts.Type | null,
52+
) {}
53+
54+
public getParameterType(index: number): ts.Type | null {
55+
if (index >= this.paramTypes.length || this.hasConsumedArguments) {
56+
return this.restType;
57+
}
58+
return this.paramTypes[index];
59+
}
60+
61+
public consumeRemainingArguments(): void {
62+
this.hasConsumedArguments = true;
63+
}
64+
}
65+
66+
export default util.createRule<[], MessageIds>({
67+
name: 'no-unsafe-argument',
68+
meta: {
69+
type: 'problem',
70+
docs: {
71+
description: 'Disallows calling an function with an any type value',
72+
category: 'Possible Errors',
73+
// TODO - enable this with next breaking
74+
recommended: false,
75+
requiresTypeChecking: true,
76+
},
77+
messages: {
78+
unsafeArgument:
79+
'Unsafe argument of type `{{sender}}` assigned to a parameter of type `{{receiver}}`.',
80+
unsafeTupleSpread:
81+
'Unsafe spread of a tuple type. The {{index}} element is of type `{{sender}}` and is assigned to a parameter of type `{{reciever}}`.',
82+
unsafeArraySpread: 'Unsafe spread of an `any` array type.',
83+
unsafeSpread: 'Unsafe spread of an `any` type.',
84+
},
85+
schema: [],
86+
},
87+
defaultOptions: [],
88+
create(context) {
89+
const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context);
90+
const checker = program.getTypeChecker();
91+
92+
return {
93+
'CallExpression, NewExpression'(
94+
node: TSESTree.CallExpression | TSESTree.NewExpression,
95+
): void {
96+
if (node.arguments.length === 0) {
97+
return;
98+
}
99+
100+
// ignore any-typed calls as these are caught by no-unsafe-call
101+
if (
102+
util.isTypeAnyType(
103+
checker.getTypeAtLocation(esTreeNodeToTSNodeMap.get(node.callee)),
104+
)
105+
) {
106+
return;
107+
}
108+
109+
const tsNode = esTreeNodeToTSNodeMap.get(node);
110+
const signature = FunctionSignature.create(checker, tsNode);
111+
if (!signature) {
112+
return;
113+
}
114+
115+
let parameterTypeIndex = 0;
116+
for (
117+
let i = 0;
118+
i < node.arguments.length;
119+
i += 1, parameterTypeIndex += 1
120+
) {
121+
const argument = node.arguments[i];
122+
123+
switch (argument.type) {
124+
// spreads consume
125+
case AST_NODE_TYPES.SpreadElement: {
126+
const spreadArgType = checker.getTypeAtLocation(
127+
esTreeNodeToTSNodeMap.get(argument.argument),
128+
);
129+
130+
if (util.isTypeAnyType(spreadArgType)) {
131+
// foo(...any)
132+
context.report({
133+
node: argument,
134+
messageId: 'unsafeSpread',
135+
});
136+
} else if (util.isTypeAnyArrayType(spreadArgType, checker)) {
137+
// foo(...any[])
138+
139+
// TODO - we could break down the spread and compare the array type against each argument
140+
context.report({
141+
node: argument,
142+
messageId: 'unsafeArraySpread',
143+
});
144+
} else if (checker.isTupleType(spreadArgType)) {
145+
// foo(...[tuple1, tuple2])
146+
const spreadTypeArguments = checker.getTypeArguments(
147+
spreadArgType,
148+
);
149+
for (
150+
let j = 0;
151+
j < spreadTypeArguments.length;
152+
j += 1, parameterTypeIndex += 1
153+
) {
154+
const tupleType = spreadTypeArguments[j];
155+
const parameterType = signature.getParameterType(
156+
parameterTypeIndex,
157+
);
158+
if (parameterType == null) {
159+
continue;
160+
}
161+
const result = util.isUnsafeAssignment(
162+
tupleType,
163+
parameterType,
164+
checker,
165+
);
166+
if (result) {
167+
context.report({
168+
node: argument,
169+
messageId: 'unsafeTupleSpread',
170+
data: {
171+
sender: checker.typeToString(tupleType),
172+
receiver: checker.typeToString(parameterType),
173+
},
174+
});
175+
}
176+
}
177+
if (spreadArgType.target.hasRestElement) {
178+
// the last element was a rest - so all remaining defined arguments can be considered "consumed"
179+
// all remaining arguments should be compared against the rest type (if one exists)
180+
signature.consumeRemainingArguments();
181+
}
182+
} else {
183+
// something that's iterable
184+
// handling this will be pretty complex - so we ignore it for now
185+
// TODO - handle generic iterable case
186+
}
187+
break;
188+
}
189+
190+
default: {
191+
const parameterType = signature.getParameterType(i);
192+
if (parameterType == null) {
193+
continue;
194+
}
195+
196+
const argumentType = checker.getTypeAtLocation(
197+
esTreeNodeToTSNodeMap.get(argument),
198+
);
199+
const result = util.isUnsafeAssignment(
200+
argumentType,
201+
parameterType,
202+
checker,
203+
);
204+
if (result) {
205+
context.report({
206+
node: argument,
207+
messageId: 'unsafeArgument',
208+
data: {
209+
sender: checker.typeToString(argumentType),
210+
receiver: checker.typeToString(parameterType),
211+
},
212+
});
213+
}
214+
}
215+
}
216+
}
217+
},
218+
};
219+
},
220+
});

0 commit comments

Comments
 (0)
0