Skip to content

fix(eslint-plugin): [use-unknown-in-catch-callback-variable] only flag function literals #10436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,41 @@ export default createRule<[], MessageIds>({
return false;
}

function shouldFlagArgument(node: TSESTree.Expression): boolean {
const argument = esTreeNodeToTSNodeMap.get(node);
const typeOfArgument = checker.getTypeAtLocation(argument);
return isFlaggableHandlerType(typeOfArgument);
function collectFlaggedNodes(
node: Exclude<TSESTree.Node, TSESTree.SpreadElement>,
): (TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression)[] {
switch (node.type) {
case AST_NODE_TYPES.LogicalExpression:
return [
...collectFlaggedNodes(node.left),
...collectFlaggedNodes(node.right),
];
case AST_NODE_TYPES.SequenceExpression:
return collectFlaggedNodes(
nullThrows(
node.expressions.at(-1),
'sequence expression must have multiple expressions',
),
);
case AST_NODE_TYPES.ConditionalExpression:
return [
...collectFlaggedNodes(node.consequent),
...collectFlaggedNodes(node.alternate),
];
case AST_NODE_TYPES.ArrowFunctionExpression:
case AST_NODE_TYPES.FunctionExpression:
{
const argument = esTreeNodeToTSNodeMap.get(node);
const typeOfArgument = checker.getTypeAtLocation(argument);
if (isFlaggableHandlerType(typeOfArgument)) {
return [node];
}
}
break;
default:
break;
}
return [];
}

/**
Expand All @@ -114,18 +145,8 @@ export default createRule<[], MessageIds>({
* rule _is reporting_, so it is not guaranteed to be sound to call otherwise.
*/
function refineReportIfPossible(
argument: TSESTree.Expression,
argument: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
): Partial<ReportDescriptor<MessageIds>> | undefined {
// Only know how to be helpful if a function literal has been provided.
if (
!(
argument.type === AST_NODE_TYPES.ArrowFunctionExpression ||
argument.type === AST_NODE_TYPES.FunctionExpression
)
) {
return undefined;
}

const catchVariableOuterWithIncorrectTypes = nullThrows(
argument.params.at(0),
'There should have been at least one parameter for the rule to have flagged.',
Expand Down Expand Up @@ -277,11 +298,12 @@ export default createRule<[], MessageIds>({
}

// the `some` check above has already excluded `SpreadElement`, so we are safe to assert the same
const node = argsToCheck[argIndexToCheck] as Exclude<
(typeof argsToCheck)[number],
const argToCheck = argsToCheck[argIndexToCheck] as Exclude<
TSESTree.Node,
TSESTree.SpreadElement
>;
if (shouldFlagArgument(node)) {

for (const node of collectFlaggedNodes(argToCheck)) {
// We are now guaranteed to report, but we have a bit of work to do
// to determine exactly where, and whether we can fix it.
const overrides = refineReportIfPossible(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,21 @@ Promise.resolve().catch(...x);
declare const thenArgs: [() => {}, (err: any) => {}];
Promise.resolve().then(...thenArgs);
`,
// this is valid, because the `any` is a passed-in handler, not a function literal.
// https://github.com/typescript-eslint/typescript-eslint/issues/9057
`
declare const yoloHandler: (x: any) => void;
Promise.reject(new Error('I will reject!')).catch(yoloHandler);
`,
// type assertion is not a function literal.
`
type InvalidHandler = (arg: any) => void;
Promise.resolve().catch(<InvalidHandler>(
function (err /* awkward spot for comment */) {
throw err;
}
));
`,
],

invalid: [
Expand Down Expand Up @@ -347,25 +362,6 @@ Promise.resolve().catch(function (err: unknown /* awkward spot for comment */) {
],
},

{
code: `
type InvalidHandler = (arg: any) => void;
Promise.resolve().catch(<InvalidHandler>(
function (err /* awkward spot for comment */) {
throw err;
}
));
`,
errors: [
{
line: 3,
messageId: 'useUnknown',
// We should _not_ make a suggestion due to the type assertion.
suggestions: null,
},
],
},

{
code: `
Promise.resolve().catch(function namedCallback(err: string) {
Expand Down Expand Up @@ -598,19 +594,6 @@ Promise.reject(new Error('I will reject!')).catch(([err]: [unknown]) => {
],
},

{
code: `
declare const yoloHandler: (x: any) => void;
Promise.reject(new Error('I will reject!')).catch(yoloHandler);
`,
errors: [
{
line: 3,
messageId: 'useUnknown',
},
],
},

{
code: `
Promise.resolve(' a string ').catch(
Expand Down Expand Up @@ -748,5 +731,132 @@ Promise.resolve().catch((...{ find }: [unknown]) => {
},
],
},

{
code: `
declare const condition: boolean;
Promise.resolve('foo').then(() => {}, condition ? err => {} : err => {});
`,
errors: [
{
column: 51,
endColumn: 54,
endLine: 3,
line: 3,

messageId: 'useUnknown',

suggestions: [
{
messageId: 'addUnknownTypeAnnotationSuggestion',
output: `
declare const condition: boolean;
Promise.resolve('foo').then(() => {}, condition ? (err: unknown) => {} : err => {});
`,
},
],
},
{
column: 63,
endColumn: 66,
endLine: 3,
line: 3,

messageId: 'useUnknown',

suggestions: [
{
messageId: 'addUnknownTypeAnnotationSuggestion',
output: `
declare const condition: boolean;
Promise.resolve('foo').then(() => {}, condition ? err => {} : (err: unknown) => {});
`,
},
],
},
],
},
{
code: `
declare const condition: boolean;
declare const maybeNullishHandler: null | ((err: any) => void);
Promise.resolve('foo').catch(
condition
? (err => {}, err => {}, maybeNullishHandler) ?? (err => {})
: (condition && (err => {})) || (err => {}),
);
`,
errors: [
{
column: 55,
endColumn: 58,
endLine: 6,
line: 6,

messageId: 'useUnknown',

suggestions: [
{
messageId: 'addUnknownTypeAnnotationSuggestion',
output: `
declare const condition: boolean;
declare const maybeNullishHandler: null | ((err: any) => void);
Promise.resolve('foo').catch(
condition
? (err => {}, err => {}, maybeNullishHandler) ?? ((err: unknown) => {})
: (condition && (err => {})) || (err => {}),
);
`,
},
],
},
{
column: 22,
endColumn: 25,
endLine: 7,
line: 7,

messageId: 'useUnknown',

suggestions: [
{
messageId: 'addUnknownTypeAnnotationSuggestion',
output: `
declare const condition: boolean;
declare const maybeNullishHandler: null | ((err: any) => void);
Promise.resolve('foo').catch(
condition
? (err => {}, err => {}, maybeNullishHandler) ?? (err => {})
: (condition && ((err: unknown) => {})) || (err => {}),
);
`,
},
],
},
{
column: 38,
endColumn: 41,
endLine: 7,
line: 7,

messageId: 'useUnknown',

suggestions: [
{
messageId: 'addUnknownTypeAnnotationSuggestion',
output: `
declare const condition: boolean;
declare const maybeNullishHandler: null | ((err: any) => void);
Promise.resolve('foo').catch(
condition
? (err => {}, err => {}, maybeNullishHandler) ?? (err => {})
: (condition && (err => {})) || ((err: unknown) => {}),
);
`,
},
],
},
],
},
],
});
Loading