Skip to content

Commit

Permalink
fix(eslint-plugin): [no-unsafe-argument] handle tagged templates (#8746)
Browse files Browse the repository at this point in the history
* feat(eslint-plugin): [no-unsafe-argument] handle  tagged templates

* add istanbul comment

* add test cases

* fix error loc

* fix: add null throws
  • Loading branch information
yeonjuan committed Apr 23, 2024
1 parent ba1eb20 commit b0f7aa4
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 94 deletions.
206 changes: 112 additions & 94 deletions packages/eslint-plugin/src/rules/no-unsafe-argument.ts
Expand Up @@ -9,6 +9,7 @@ import {
isTypeAnyArrayType,
isTypeAnyType,
isUnsafeAssignment,
nullThrows,
} from '../util';

type MessageIds =
Expand Down Expand Up @@ -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);
},
};
},
Expand Down
99 changes: 99 additions & 0 deletions packages/eslint-plugin/tests/rules/no-unsafe-argument.test.ts
Expand Up @@ -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<string>, arg2: Map<string, string>): void;
const x = [new Map<string, string>()] as const;
Expand Down Expand Up @@ -105,6 +109,14 @@ declare function foo<T>(t: T): T;
const t: T = [];
foo(t);
`,
`
function foo(templates: TemplateStringsArray) {}
foo\`\`;
`,
`
function foo(templates: TemplateStringsArray, arg: any) {}
foo\`\${1 as any}\`;
`,
],
invalid: [
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -385,5 +411,78 @@ foo(t as any);
},
],
},
{
code: `
function foo(
templates: TemplateStringsArray,
arg1: number,
arg2: any,
arg3: string,
) {}
declare const arg: any;
foo<number>\`\${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',
},
},
],
},
],
});

0 comments on commit b0f7aa4

Please sign in to comment.