Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6f8cfe6

Browse files
authored
fix(eslint-plugin): [no-unsafe-argument] handle tuple types on rest arguments (typescript-eslint#3269)
1 parent 60f47a0 commit 6f8cfe6

File tree

2 files changed

+162
-23
lines changed

2 files changed

+162
-23
lines changed

packages/eslint-plugin/src/rules/no-unsafe-argument.ts

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,31 @@ type MessageIds =
1111
| 'unsafeArraySpread'
1212
| 'unsafeSpread';
1313

14+
const enum RestTypeKind {
15+
Array,
16+
Tuple,
17+
Other,
18+
}
19+
type RestType =
20+
| {
21+
type: ts.Type;
22+
kind: RestTypeKind.Array;
23+
index: number;
24+
}
25+
| {
26+
typeArguments: readonly ts.Type[];
27+
kind: RestTypeKind.Tuple;
28+
index: number;
29+
}
30+
| {
31+
type: ts.Type;
32+
kind: RestTypeKind.Other;
33+
index: number;
34+
};
35+
1436
class FunctionSignature {
37+
private parameterTypeIndex = 0;
38+
1539
public static create(
1640
checker: ts.TypeChecker,
1741
tsNode: ts.CallLikeExpression,
@@ -22,18 +46,34 @@ class FunctionSignature {
2246
}
2347

2448
const paramTypes: ts.Type[] = [];
25-
let restType: ts.Type | null = null;
49+
let restType: RestType | null = null;
2650

27-
for (const param of signature.getParameters()) {
51+
const parameters = signature.getParameters();
52+
for (let i = 0; i < parameters.length; i += 1) {
53+
const param = parameters[i];
2854
const type = checker.getTypeOfSymbolAtLocation(param, tsNode);
2955

3056
const decl = param.getDeclarations()?.[0];
3157
if (decl && ts.isParameter(decl) && decl.dotDotDotToken) {
3258
// is a rest param
3359
if (checker.isArrayType(type)) {
34-
restType = checker.getTypeArguments(type)[0];
60+
restType = {
61+
type: checker.getTypeArguments(type)[0],
62+
kind: RestTypeKind.Array,
63+
index: i,
64+
};
65+
} else if (checker.isTupleType(type)) {
66+
restType = {
67+
typeArguments: checker.getTypeArguments(type),
68+
kind: RestTypeKind.Tuple,
69+
index: i,
70+
};
3571
} else {
36-
restType = type;
72+
restType = {
73+
type,
74+
kind: RestTypeKind.Other,
75+
index: i,
76+
};
3777
}
3878
break;
3979
}
@@ -48,12 +88,41 @@ class FunctionSignature {
4888

4989
private constructor(
5090
private paramTypes: ts.Type[],
51-
private restType: ts.Type | null,
91+
private restType: RestType | null,
5292
) {}
5393

54-
public getParameterType(index: number): ts.Type | null {
94+
public getNextParameterType(): ts.Type | null {
95+
const index = this.parameterTypeIndex;
96+
this.parameterTypeIndex += 1;
97+
5598
if (index >= this.paramTypes.length || this.hasConsumedArguments) {
56-
return this.restType;
99+
if (this.restType == null) {
100+
return null;
101+
}
102+
103+
switch (this.restType.kind) {
104+
case RestTypeKind.Tuple: {
105+
const typeArguments = this.restType.typeArguments;
106+
if (this.hasConsumedArguments) {
107+
// all types consumed by a rest - just assume it's the last type
108+
// there is one edge case where this is wrong, but we ignore it because
109+
// it's rare and really complicated to handle
110+
// eg: function foo(...a: [number, ...string[], number])
111+
return typeArguments[typeArguments.length - 1];
112+
}
113+
114+
const typeIndex = index - this.restType.index;
115+
if (typeIndex >= typeArguments.length) {
116+
return typeArguments[typeArguments.length - 1];
117+
}
118+
119+
return typeArguments[typeIndex];
120+
}
121+
122+
case RestTypeKind.Array:
123+
case RestTypeKind.Other:
124+
return this.restType.type;
125+
}
57126
}
58127
return this.paramTypes[index];
59128
}
@@ -112,12 +181,7 @@ export default util.createRule<[], MessageIds>({
112181
return;
113182
}
114183

115-
let parameterTypeIndex = 0;
116-
for (
117-
let i = 0;
118-
i < node.arguments.length;
119-
i += 1, parameterTypeIndex += 1
120-
) {
184+
for (let i = 0; i < node.arguments.length; i += 1) {
121185
const argument = node.arguments[i];
122186

123187
switch (argument.type) {
@@ -146,15 +210,9 @@ export default util.createRule<[], MessageIds>({
146210
const spreadTypeArguments = checker.getTypeArguments(
147211
spreadArgType,
148212
);
149-
for (
150-
let j = 0;
151-
j < spreadTypeArguments.length;
152-
j += 1, parameterTypeIndex += 1
153-
) {
213+
for (let j = 0; j < spreadTypeArguments.length; j += 1) {
154214
const tupleType = spreadTypeArguments[j];
155-
const parameterType = signature.getParameterType(
156-
parameterTypeIndex,
157-
);
215+
const parameterType = signature.getNextParameterType();
158216
if (parameterType == null) {
159217
continue;
160218
}
@@ -188,7 +246,7 @@ export default util.createRule<[], MessageIds>({
188246
}
189247

190248
default: {
191-
const parameterType = signature.getParameterType(i);
249+
const parameterType = signature.getNextParameterType();
192250
if (parameterType == null) {
193251
continue;
194252
}

packages/eslint-plugin/tests/rules/no-unsafe-argument.test.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@ const ruleTester = new RuleTester({
1111

1212
ruleTester.run('no-unsafe-argument', rule, {
1313
valid: [
14+
// unknown function should be ignored
15+
`
16+
doesNotExist(1 as any);
17+
`,
18+
// non-function call should be ignored
19+
`
20+
const foo = 1;
21+
foo(1 as any);
22+
`,
23+
// too many arguments should be ignored as this is a TS error
1424
`
1525
declare function foo(arg: number): void;
16-
foo(1);
26+
foo(1, 1 as any, 2 as any);
1727
`,
1828
`
1929
declare function foo(arg: number, arg2: string): void;
@@ -60,6 +70,21 @@ foo(new Set<string>(), ...x);
6070
declare function foo(arg1: unknown, arg2: Set<unkown>, arg3: unknown[]): void;
6171
foo(1 as any, new Set<any>(), [] as any[]);
6272
`,
73+
`
74+
declare function foo(...params: [number, string, any]): void;
75+
foo(1, 'a', 1 as any);
76+
`,
77+
// Unfortunately - we cannot handle this case because TS infers `params` to be a tuple type
78+
// that tuple type is the same as the type of
79+
`
80+
declare function foo<E extends string[]>(...params: E): void;
81+
82+
foo('a', 'b', 1 as any);
83+
`,
84+
`
85+
declare function toHaveBeenCalledWith<E extends any[]>(...params: E): void;
86+
toHaveBeenCalledWith(1 as any);
87+
`,
6388
],
6489
invalid: [
6590
{
@@ -264,5 +289,61 @@ foo(new Set<any>(), ...x);
264289
},
265290
],
266291
},
292+
{
293+
code: `
294+
declare function foo(...params: [number, string, any]): void;
295+
foo(1 as any, 'a' as any, 1 as any);
296+
`,
297+
errors: [
298+
{
299+
messageId: 'unsafeArgument',
300+
line: 3,
301+
column: 5,
302+
endColumn: 13,
303+
data: {
304+
sender: 'any',
305+
receiver: 'number',
306+
},
307+
},
308+
{
309+
messageId: 'unsafeArgument',
310+
line: 3,
311+
column: 15,
312+
endColumn: 25,
313+
data: {
314+
sender: 'any',
315+
receiver: 'string',
316+
},
317+
},
318+
],
319+
},
320+
{
321+
code: `
322+
declare function foo(param1: string, ...params: [number, string, any]): void;
323+
foo('a', 1 as any, 'a' as any, 1 as any);
324+
`,
325+
errors: [
326+
{
327+
messageId: 'unsafeArgument',
328+
line: 3,
329+
column: 10,
330+
endColumn: 18,
331+
data: {
332+
sender: 'any',
333+
receiver: 'number',
334+
},
335+
},
336+
{
337+
messageId: 'unsafeArgument',
338+
line: 3,
339+
column: 20,
340+
endColumn: 30,
341+
data: {
342+
sender: 'any',
343+
receiver: 'string',
344+
},
345+
},
346+
],
347+
},
267348
],
268349
});

0 commit comments

Comments
 (0)