Skip to content

Commit

Permalink
fix(eslint-plugin): [no-unsafe-argument] handle tuple types on rest a…
Browse files Browse the repository at this point in the history
…rguments (#3269)
  • Loading branch information
bradzacher committed Apr 6, 2021
1 parent 60f47a0 commit 6f8cfe6
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 23 deletions.
102 changes: 80 additions & 22 deletions packages/eslint-plugin/src/rules/no-unsafe-argument.ts
Expand Up @@ -11,7 +11,31 @@ type MessageIds =
| 'unsafeArraySpread'
| 'unsafeSpread';

const enum RestTypeKind {
Array,
Tuple,
Other,
}
type RestType =
| {
type: ts.Type;
kind: RestTypeKind.Array;
index: number;
}
| {
typeArguments: readonly ts.Type[];
kind: RestTypeKind.Tuple;
index: number;
}
| {
type: ts.Type;
kind: RestTypeKind.Other;
index: number;
};

class FunctionSignature {
private parameterTypeIndex = 0;

public static create(
checker: ts.TypeChecker,
tsNode: ts.CallLikeExpression,
Expand All @@ -22,18 +46,34 @@ class FunctionSignature {
}

const paramTypes: ts.Type[] = [];
let restType: ts.Type | null = null;
let restType: RestType | null = null;

for (const param of signature.getParameters()) {
const parameters = signature.getParameters();
for (let i = 0; i < parameters.length; i += 1) {
const param = parameters[i];
const type = checker.getTypeOfSymbolAtLocation(param, tsNode);

const decl = param.getDeclarations()?.[0];
if (decl && ts.isParameter(decl) && decl.dotDotDotToken) {
// is a rest param
if (checker.isArrayType(type)) {
restType = checker.getTypeArguments(type)[0];
restType = {
type: checker.getTypeArguments(type)[0],
kind: RestTypeKind.Array,
index: i,
};
} else if (checker.isTupleType(type)) {
restType = {
typeArguments: checker.getTypeArguments(type),
kind: RestTypeKind.Tuple,
index: i,
};
} else {
restType = type;
restType = {
type,
kind: RestTypeKind.Other,
index: i,
};
}
break;
}
Expand All @@ -48,12 +88,41 @@ class FunctionSignature {

private constructor(
private paramTypes: ts.Type[],
private restType: ts.Type | null,
private restType: RestType | null,
) {}

public getParameterType(index: number): ts.Type | null {
public getNextParameterType(): ts.Type | null {
const index = this.parameterTypeIndex;
this.parameterTypeIndex += 1;

if (index >= this.paramTypes.length || this.hasConsumedArguments) {
return this.restType;
if (this.restType == null) {
return null;
}

switch (this.restType.kind) {
case RestTypeKind.Tuple: {
const typeArguments = this.restType.typeArguments;
if (this.hasConsumedArguments) {
// all types consumed by a rest - just assume it's the last type
// there is one edge case where this is wrong, but we ignore it because
// it's rare and really complicated to handle
// eg: function foo(...a: [number, ...string[], number])
return typeArguments[typeArguments.length - 1];
}

const typeIndex = index - this.restType.index;
if (typeIndex >= typeArguments.length) {
return typeArguments[typeArguments.length - 1];
}

return typeArguments[typeIndex];
}

case RestTypeKind.Array:
case RestTypeKind.Other:
return this.restType.type;
}
}
return this.paramTypes[index];
}
Expand Down Expand Up @@ -112,12 +181,7 @@ export default util.createRule<[], MessageIds>({
return;
}

let parameterTypeIndex = 0;
for (
let i = 0;
i < node.arguments.length;
i += 1, parameterTypeIndex += 1
) {
for (let i = 0; i < node.arguments.length; i += 1) {
const argument = node.arguments[i];

switch (argument.type) {
Expand Down Expand Up @@ -146,15 +210,9 @@ export default util.createRule<[], MessageIds>({
const spreadTypeArguments = checker.getTypeArguments(
spreadArgType,
);
for (
let j = 0;
j < spreadTypeArguments.length;
j += 1, parameterTypeIndex += 1
) {
for (let j = 0; j < spreadTypeArguments.length; j += 1) {
const tupleType = spreadTypeArguments[j];
const parameterType = signature.getParameterType(
parameterTypeIndex,
);
const parameterType = signature.getNextParameterType();
if (parameterType == null) {
continue;
}
Expand Down Expand Up @@ -188,7 +246,7 @@ export default util.createRule<[], MessageIds>({
}

default: {
const parameterType = signature.getParameterType(i);
const parameterType = signature.getNextParameterType();
if (parameterType == null) {
continue;
}
Expand Down
83 changes: 82 additions & 1 deletion packages/eslint-plugin/tests/rules/no-unsafe-argument.test.ts
Expand Up @@ -11,9 +11,19 @@ const ruleTester = new RuleTester({

ruleTester.run('no-unsafe-argument', rule, {
valid: [
// unknown function should be ignored
`
doesNotExist(1 as any);
`,
// non-function call should be ignored
`
const foo = 1;
foo(1 as any);
`,
// too many arguments should be ignored as this is a TS error
`
declare function foo(arg: number): void;
foo(1);
foo(1, 1 as any, 2 as any);
`,
`
declare function foo(arg: number, arg2: string): void;
Expand Down Expand Up @@ -60,6 +70,21 @@ foo(new Set<string>(), ...x);
declare function foo(arg1: unknown, arg2: Set<unkown>, arg3: unknown[]): void;
foo(1 as any, new Set<any>(), [] as any[]);
`,
`
declare function foo(...params: [number, string, any]): void;
foo(1, 'a', 1 as any);
`,
// Unfortunately - we cannot handle this case because TS infers `params` to be a tuple type
// that tuple type is the same as the type of
`
declare function foo<E extends string[]>(...params: E): void;
foo('a', 'b', 1 as any);
`,
`
declare function toHaveBeenCalledWith<E extends any[]>(...params: E): void;
toHaveBeenCalledWith(1 as any);
`,
],
invalid: [
{
Expand Down Expand Up @@ -264,5 +289,61 @@ foo(new Set<any>(), ...x);
},
],
},
{
code: `
declare function foo(...params: [number, string, any]): void;
foo(1 as any, 'a' as any, 1 as any);
`,
errors: [
{
messageId: 'unsafeArgument',
line: 3,
column: 5,
endColumn: 13,
data: {
sender: 'any',
receiver: 'number',
},
},
{
messageId: 'unsafeArgument',
line: 3,
column: 15,
endColumn: 25,
data: {
sender: 'any',
receiver: 'string',
},
},
],
},
{
code: `
declare function foo(param1: string, ...params: [number, string, any]): void;
foo('a', 1 as any, 'a' as any, 1 as any);
`,
errors: [
{
messageId: 'unsafeArgument',
line: 3,
column: 10,
endColumn: 18,
data: {
sender: 'any',
receiver: 'number',
},
},
{
messageId: 'unsafeArgument',
line: 3,
column: 20,
endColumn: 30,
data: {
sender: 'any',
receiver: 'string',
},
},
],
},
],
});

0 comments on commit 6f8cfe6

Please sign in to comment.