diff --git a/packages/eslint-plugin/src/rules/no-unsafe-argument.ts b/packages/eslint-plugin/src/rules/no-unsafe-argument.ts index 177954392bb..d55d8f75b06 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-argument.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-argument.ts @@ -9,6 +9,7 @@ import { isTypeAnyArrayType, isTypeAnyType, isUnsafeAssignment, + nullThrows, } from '../util'; type MessageIds = @@ -162,114 +163,131 @@ export default createRule<[], MessageIds>({ const services = getParserServices(context); const checker = services.program.getTypeChecker(); - return { - 'CallExpression, NewExpression'( - node: TSESTree.CallExpression | TSESTree.NewExpression, - ): void { - if (node.arguments.length === 0) { - return; - } + function checkUnsafeArguments( + args: TSESTree.Expression[] | TSESTree.CallExpressionArgument[], + callee: TSESTree.LeftHandSideExpression, + node: + | TSESTree.CallExpression + | TSESTree.NewExpression + | TSESTree.TaggedTemplateExpression, + ): void { + if (args.length === 0) { + return; + } - // ignore any-typed calls as these are caught by no-unsafe-call - if (isTypeAnyType(services.getTypeAtLocation(node.callee))) { - return; - } + // ignore any-typed calls as these are caught by no-unsafe-call + if (isTypeAnyType(services.getTypeAtLocation(callee))) { + return; + } - const tsNode = services.esTreeNodeToTSNodeMap.get(node); - const signature = FunctionSignature.create(checker, tsNode); - if (!signature) { - return; - } + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + const signature = nullThrows( + FunctionSignature.create(checker, tsNode), + 'Expected to a signature resolved', + ); - for (const argument of node.arguments) { - switch (argument.type) { - // spreads consume - case AST_NODE_TYPES.SpreadElement: { - const spreadArgType = services.getTypeAtLocation( - argument.argument, - ); + if (node.type === AST_NODE_TYPES.TaggedTemplateExpression) { + // Consumes the first parameter (TemplateStringsArray) of the function called with TaggedTemplateExpression. + signature.getNextParameterType(); + } + + for (const argument of args) { + switch (argument.type) { + // spreads consume + case AST_NODE_TYPES.SpreadElement: { + const spreadArgType = services.getTypeAtLocation(argument.argument); - if (isTypeAnyType(spreadArgType)) { - // foo(...any) - context.report({ - node: argument, - messageId: 'unsafeSpread', - }); - } else if (isTypeAnyArrayType(spreadArgType, checker)) { - // foo(...any[]) + if (isTypeAnyType(spreadArgType)) { + // foo(...any) + context.report({ + node: argument, + messageId: 'unsafeSpread', + }); + } else if (isTypeAnyArrayType(spreadArgType, checker)) { + // foo(...any[]) - // TODO - we could break down the spread and compare the array type against each argument - context.report({ - node: argument, - messageId: 'unsafeArraySpread', - }); - } else if (checker.isTupleType(spreadArgType)) { - // foo(...[tuple1, tuple2]) - const spreadTypeArguments = - checker.getTypeArguments(spreadArgType); - for (const tupleType of spreadTypeArguments) { - const parameterType = signature.getNextParameterType(); - if (parameterType == null) { - continue; - } - const result = isUnsafeAssignment( - tupleType, - parameterType, - checker, - // we can't pass the individual tuple members in here as this will most likely be a spread variable - // not a spread array - null, - ); - if (result) { - context.report({ - node: argument, - messageId: 'unsafeTupleSpread', - data: { - sender: checker.typeToString(tupleType), - receiver: checker.typeToString(parameterType), - }, - }); - } + // TODO - we could break down the spread and compare the array type against each argument + context.report({ + node: argument, + messageId: 'unsafeArraySpread', + }); + } else if (checker.isTupleType(spreadArgType)) { + // foo(...[tuple1, tuple2]) + const spreadTypeArguments = + checker.getTypeArguments(spreadArgType); + for (const tupleType of spreadTypeArguments) { + const parameterType = signature.getNextParameterType(); + if (parameterType == null) { + continue; } - if (spreadArgType.target.hasRestElement) { - // the last element was a rest - so all remaining defined arguments can be considered "consumed" - // all remaining arguments should be compared against the rest type (if one exists) - signature.consumeRemainingArguments(); + const result = isUnsafeAssignment( + tupleType, + parameterType, + checker, + // we can't pass the individual tuple members in here as this will most likely be a spread variable + // not a spread array + null, + ); + if (result) { + context.report({ + node: argument, + messageId: 'unsafeTupleSpread', + data: { + sender: checker.typeToString(tupleType), + receiver: checker.typeToString(parameterType), + }, + }); } - } else { - // something that's iterable - // handling this will be pretty complex - so we ignore it for now - // TODO - handle generic iterable case } - break; + if (spreadArgType.target.hasRestElement) { + // the last element was a rest - so all remaining defined arguments can be considered "consumed" + // all remaining arguments should be compared against the rest type (if one exists) + signature.consumeRemainingArguments(); + } + } else { + // something that's iterable + // handling this will be pretty complex - so we ignore it for now + // TODO - handle generic iterable case } + break; + } - default: { - const parameterType = signature.getNextParameterType(); - if (parameterType == null) { - continue; - } + default: { + const parameterType = signature.getNextParameterType(); + if (parameterType == null) { + continue; + } - const argumentType = services.getTypeAtLocation(argument); - const result = isUnsafeAssignment( - argumentType, - parameterType, - checker, - argument, - ); - if (result) { - context.report({ - node: argument, - messageId: 'unsafeArgument', - data: { - sender: checker.typeToString(argumentType), - receiver: checker.typeToString(parameterType), - }, - }); - } + const argumentType = services.getTypeAtLocation(argument); + const result = isUnsafeAssignment( + argumentType, + parameterType, + checker, + argument, + ); + if (result) { + context.report({ + node: argument, + messageId: 'unsafeArgument', + data: { + sender: checker.typeToString(argumentType), + receiver: checker.typeToString(parameterType), + }, + }); } } } + } + } + + return { + 'CallExpression, NewExpression'( + node: TSESTree.CallExpression | TSESTree.NewExpression, + ): void { + checkUnsafeArguments(node.arguments, node.callee, node); + }, + TaggedTemplateExpression(node: TSESTree.TaggedTemplateExpression): void { + checkUnsafeArguments(node.quasi.expressions, node.tag, node); }, }; }, diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-argument.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-argument.test.ts index e525ac42873..01b97cfb34b 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-argument.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-argument.test.ts @@ -63,6 +63,10 @@ const x: string[] = []; foo(...x); `, ` +function foo(arg1: number, arg2: number) {} +foo(...([1, 1, 1] as [number, number, number])); + `, + ` declare function foo(arg1: Set, arg2: Map): void; const x = [new Map()] as const; @@ -105,6 +109,14 @@ declare function foo(t: T): T; const t: T = []; foo(t); `, + ` +function foo(templates: TemplateStringsArray) {} +foo\`\`; + `, + ` +function foo(templates: TemplateStringsArray, arg: any) {} +foo\`\${1 as any}\`; + `, ], invalid: [ { @@ -241,6 +253,20 @@ foo(...x); }, { code: ` +declare function foo(arg1: string, arg2: number): void; +foo(...(['foo', 1, 2] as [string, any, number])); + `, + errors: [ + { + messageId: 'unsafeTupleSpread', + line: 3, + column: 5, + endColumn: 48, + }, + ], + }, + { + code: ` declare function foo(arg1: string, arg2: number, arg2: string): void; const x = [1] as const; @@ -385,5 +411,78 @@ foo(t as any); }, ], }, + { + code: ` +function foo( + templates: TemplateStringsArray, + arg1: number, + arg2: any, + arg3: string, +) {} +declare const arg: any; +foo\`\${arg}\${arg}\${arg}\`; + `, + errors: [ + { + messageId: 'unsafeArgument', + line: 9, + column: 15, + endColumn: 18, + data: { + sender: 'any', + receiver: 'number', + }, + }, + { + messageId: 'unsafeArgument', + line: 9, + column: 27, + endColumn: 30, + data: { + sender: 'any', + receiver: 'string', + }, + }, + ], + }, + { + code: ` +function foo(templates: TemplateStringsArray, arg: number) {} +declare const arg: any; +foo\`\${arg}\`; + `, + errors: [ + { + messageId: 'unsafeArgument', + line: 4, + column: 7, + endColumn: 10, + data: { + sender: 'any', + receiver: 'number', + }, + }, + ], + }, + { + code: ` +type T = [number, T[]]; +function foo(templates: TemplateStringsArray, arg: T) {} +declare const arg: any; +foo\`\${arg}\`; + `, + errors: [ + { + messageId: 'unsafeArgument', + line: 5, + column: 7, + endColumn: 10, + data: { + sender: 'any', + receiver: 'T', + }, + }, + ], + }, ], });