Skip to content

Commit b2c608c

Browse files
authored
fix: NewExpression with parenthesized callee in preserve-caught-error (#21083)
1 parent 5d2f866 commit b2c608c

2 files changed

Lines changed: 162 additions & 104 deletions

File tree

lib/rules/preserve-caught-error.js

Lines changed: 91 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const astUtils = require("./utils/ast-utils");
1515
//------------------------------------------------------------------------------
1616

1717
/** @typedef {import("estree").Node} ASTNode */
18+
/** @typedef {import("eslint").Rule.Fix} Fix */
19+
/** @typedef {import("eslint").Rule.RuleFixer} RuleFixer */
1820

1921
//------------------------------------------------------------------------------
2022
// Helpers
@@ -309,21 +311,19 @@ module.exports = {
309311
}
310312

311313
/**
312-
* Gets the token after which new arguments should be inserted for the
313-
* given argument, accounting for parentheses wrapping the argument so
314-
* that insertions land outside them (e.g. `new Error(("msg"))`).
315-
* @param {ASTNode} argNode The argument node.
316-
* @param {ASTNode} callNode The enclosing call or new expression.
317-
* @returns {Token} The token to insert after.
314+
* Finds the first token that isn't followed by a closing parenthesis in a specified range.
315+
* This is the token after which new arguments should be inserted.
316+
* @param {Token} firstToken The first token to start searching from.
317+
* @param {Token} lastToken The last token to scan up to (inclusive).
318+
* @returns {Token} The first token that isn't followed by a closing parenthesis, or `lastToken` if none is found.
318319
*/
319-
function getLastArgumentToken(argNode, callNode) {
320-
const callLastToken = sourceCode.getLastToken(callNode);
321-
let token = sourceCode.getLastToken(argNode);
320+
function findInsertionTokenAfterParens(firstToken, lastToken) {
321+
const lastIndex = lastToken.range[1];
322+
let token = firstToken;
322323
let nextToken = sourceCode.getTokenAfter(token);
323324

324325
while (
325-
nextToken &&
326-
nextToken !== callLastToken &&
326+
nextToken.range[1] <= lastIndex &&
327327
astUtils.isClosingParenToken(nextToken)
328328
) {
329329
token = nextToken;
@@ -332,6 +332,61 @@ module.exports = {
332332
return token;
333333
}
334334

335+
/**
336+
* Adds arguments for a call/new expression with no arguments.
337+
* Works for `new Error`, `new (Error)`, and forms that already include empty argument parentheses.
338+
* @param {RuleFixer} fixer The fixer instance.
339+
* @param {ASTNode & { callee: ASTNode }} throwExpression The thrown CallExpression or NewExpression node.
340+
* @param {string} text The arguments to insert.
341+
* @returns {Fix} The fixer operation.
342+
*/
343+
function addArgumentsToEmptyCall(fixer, throwExpression, text) {
344+
const callClosingParenToken =
345+
sourceCode.getLastToken(throwExpression);
346+
const lastCalleeToken = sourceCode.getLastToken(
347+
throwExpression.callee,
348+
);
349+
const parenToken = sourceCode.getFirstTokenBetween(
350+
lastCalleeToken,
351+
callClosingParenToken,
352+
astUtils.isOpeningParenToken,
353+
);
354+
355+
if (parenToken) {
356+
return fixer.insertTextAfter(parenToken, text);
357+
}
358+
359+
const insertionToken = findInsertionTokenAfterParens(
360+
lastCalleeToken,
361+
callClosingParenToken,
362+
);
363+
364+
return fixer.insertTextAfter(insertionToken, `(${text})`);
365+
}
366+
367+
/**
368+
* Appends additional arguments after the last argument of a call/new expression,
369+
* accounting for any wrapping parentheses around that argument.
370+
* @param {RuleFixer} fixer The fixer instance.
371+
* @param {ASTNode & { arguments: ASTNode[] }} throwExpression The thrown CallExpression or NewExpression node.
372+
* @param {string} text The additional arguments to insert, including the leading comma.
373+
* @returns {Fix} The fixer operation.
374+
*/
375+
function appendArguments(fixer, throwExpression, text) {
376+
const lastArgument = throwExpression.arguments.at(-1);
377+
const lastArgumentToken = sourceCode.getLastToken(lastArgument);
378+
const lastTokenBeforeArgListParen = sourceCode.getLastToken(
379+
throwExpression,
380+
{ skip: 1 },
381+
);
382+
const insertionToken = findInsertionTokenAfterParens(
383+
lastArgumentToken,
384+
lastTokenBeforeArgListParen,
385+
);
386+
387+
return fixer.insertTextAfter(insertionToken, text);
388+
}
389+
335390
//----------------------------------------------------------------------
336391
// Public
337392
//----------------------------------------------------------------------
@@ -450,51 +505,27 @@ module.exports = {
450505

451506
if (!errorsArg) {
452507
// Case: `throw new AggregateError()` → insert all arguments
453-
const lastToken =
454-
sourceCode.getLastToken(
455-
throwExpression,
456-
);
457-
const lastCalleeToken =
458-
sourceCode.getLastToken(
459-
throwExpression.callee,
460-
);
461-
const parenToken =
462-
sourceCode.getFirstTokenBetween(
463-
lastCalleeToken,
464-
lastToken,
465-
astUtils.isOpeningParenToken,
466-
);
467-
468-
if (parenToken) {
469-
return fixer.insertTextAfter(
470-
parenToken,
471-
`[], "", { cause: ${caughtError.name} }`,
472-
);
473-
}
474-
return fixer.insertTextAfter(
475-
throwExpression.callee,
476-
`([], "", { cause: ${caughtError.name} })`,
508+
return addArgumentsToEmptyCall(
509+
fixer,
510+
throwExpression,
511+
`[], "", { cause: ${caughtError.name} }`,
477512
);
478513
}
479514

480515
if (!messageArg) {
481516
// Case: `throw new AggregateError([])` → insert message and options
482-
return fixer.insertTextAfter(
483-
getLastArgumentToken(
484-
errorsArg,
485-
throwExpression,
486-
),
517+
return appendArguments(
518+
fixer,
519+
throwExpression,
487520
`, "", { cause: ${caughtError.name} }`,
488521
);
489522
}
490523

491524
if (!optionsArg) {
492525
// Case: `throw new AggregateError([], "")` → insert error options only
493-
return fixer.insertTextAfter(
494-
getLastArgumentToken(
495-
messageArg,
496-
throwExpression,
497-
),
526+
return appendArguments(
527+
fixer,
528+
throwExpression,
498529
`, { cause: ${caughtError.name} }`,
499530
);
500531
}
@@ -511,39 +542,17 @@ module.exports = {
511542

512543
if (!messageArg) {
513544
// Case: `throw new Error()` → insert both message and options
514-
const lastToken =
515-
sourceCode.getLastToken(
516-
throwExpression,
517-
);
518-
const lastCalleeToken =
519-
sourceCode.getLastToken(
520-
throwExpression.callee,
521-
);
522-
const parenToken =
523-
sourceCode.getFirstTokenBetween(
524-
lastCalleeToken,
525-
lastToken,
526-
astUtils.isOpeningParenToken,
527-
);
528-
529-
if (parenToken) {
530-
return fixer.insertTextAfter(
531-
parenToken,
532-
`"", { cause: ${caughtError.name} }`,
533-
);
534-
}
535-
return fixer.insertTextAfter(
536-
throwExpression.callee,
537-
`("", { cause: ${caughtError.name} })`,
545+
return addArgumentsToEmptyCall(
546+
fixer,
547+
throwExpression,
548+
`"", { cause: ${caughtError.name} }`,
538549
);
539550
}
540551
if (!optionsArg) {
541552
// Case: `throw new Error("Some message")` → insert only options
542-
return fixer.insertTextAfter(
543-
getLastArgumentToken(
544-
messageArg,
545-
throwExpression,
546-
),
553+
return appendArguments(
554+
fixer,
555+
throwExpression,
547556
`, { cause: ${caughtError.name} }`,
548557
);
549558
}
@@ -569,40 +578,18 @@ module.exports = {
569578

570579
if (lastProvidedArg) {
571580
// Options slot missing, all prior args provided → append options
572-
return fixer.insertTextAfter(
573-
getLastArgumentToken(
574-
lastProvidedArg,
575-
throwExpression,
576-
),
581+
return appendArguments(
582+
fixer,
583+
throwExpression,
577584
`, { cause: ${caughtError.name} }`,
578585
);
579586
}
580587

581-
// argumentPosition: 1 and no args → insert `{ cause: err }` inside parens
582-
const lastToken =
583-
sourceCode.getLastToken(
584-
throwExpression,
585-
);
586-
const lastCalleeToken =
587-
sourceCode.getLastToken(
588-
throwExpression.callee,
589-
);
590-
const parenToken =
591-
sourceCode.getFirstTokenBetween(
592-
lastCalleeToken,
593-
lastToken,
594-
astUtils.isOpeningParenToken,
595-
);
596-
597-
if (parenToken) {
598-
return fixer.insertTextAfter(
599-
parenToken,
600-
`{ cause: ${caughtError.name} }`,
601-
);
602-
}
603-
return fixer.insertTextAfter(
604-
throwExpression.callee,
605-
`({ cause: ${caughtError.name} })`,
588+
// argumentPosition: 1 and no args → insert options inside parens
589+
return addArgumentsToEmptyCall(
590+
fixer,
591+
throwExpression,
592+
`{ cause: ${caughtError.name} }`,
606593
);
607594
}
608595

tests/lib/rules/preserve-caught-error.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,5 +1404,76 @@ ruleTester.run("preserve-caught-error", rule, {
14041404
},
14051405
],
14061406
},
1407+
1408+
/* Built-in error constructor without argument list */
1409+
{
1410+
code: `try {} catch (err) { throw new Error; }`,
1411+
errors: [
1412+
{
1413+
messageId: "missingCause",
1414+
suggestions: [
1415+
{
1416+
messageId: "includeCause",
1417+
output: `try {} catch (err) { throw new Error("", { cause: err }); }`,
1418+
},
1419+
],
1420+
},
1421+
],
1422+
},
1423+
1424+
/* AggregateError without argument list */
1425+
{
1426+
code: `try {} catch (err) { throw new AggregateError; }`,
1427+
errors: [
1428+
{
1429+
messageId: "missingCause",
1430+
suggestions: [
1431+
{
1432+
messageId: "includeCause",
1433+
output: `try {} catch (err) { throw new AggregateError([], "", { cause: err }); }`,
1434+
},
1435+
],
1436+
},
1437+
],
1438+
},
1439+
1440+
/* Custom error type without argument list */
1441+
{
1442+
code: `try {} catch (err) { throw new CustomError; }`,
1443+
options: [
1444+
{
1445+
errorClassNames: [
1446+
{ name: "CustomError", argumentPosition: 1 },
1447+
],
1448+
},
1449+
],
1450+
errors: [
1451+
{
1452+
messageId: "missingCause",
1453+
suggestions: [
1454+
{
1455+
messageId: "includeCause",
1456+
output: `try {} catch (err) { throw new CustomError({ cause: err }); }`,
1457+
},
1458+
],
1459+
},
1460+
],
1461+
},
1462+
1463+
/* Parenthesized error constructor without argument list */
1464+
{
1465+
code: `try {} catch (err) { throw new (Error); }`,
1466+
errors: [
1467+
{
1468+
messageId: "missingCause",
1469+
suggestions: [
1470+
{
1471+
messageId: "includeCause",
1472+
output: `try {} catch (err) { throw new (Error)("", { cause: err }); }`,
1473+
},
1474+
],
1475+
},
1476+
],
1477+
},
14071478
],
14081479
});

0 commit comments

Comments
 (0)