Skip to content

Commit 0b040a3

Browse files
committed
Skip void (p.catch(...)) in parentheses, and report declarations after the assignment
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrbmayRraf25aTwCCj1iv2
1 parent 00a53b5 commit 0b040a3

2 files changed

Lines changed: 17 additions & 4 deletions

File tree

‎rules.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,13 @@ function skipParentheses(expr: ts.Expression): ts.Expression {
387387
return current;
388388
}
389389

390+
/** The nearest ancestor that is not a parenthesized expression. */
391+
function skipParenthesesUp(node: ts.Node): ts.Node {
392+
let current = node.parent;
393+
while (ts.isParenthesizedExpression(current)) current = current.parent;
394+
return current;
395+
}
396+
390397
/** The cast directly inside this one, looking through parentheses: `(x as A) as B`. */
391398
function innerCast(node: AssertionNode): AssertionNode | undefined {
392399
const inner = skipParentheses(node.expression);
@@ -1046,8 +1053,11 @@ function isFallbackCatch(clause: ts.CatchClause): boolean {
10461053
* a return after the try.
10471054
*/
10481055
function assignsOnlyOuterVariables(tryStatement: ts.TryStatement): boolean {
1049-
// Local declarations feed the assignment: `const stats = await stat(p); isDir = stats.isDirectory();`
1050-
const statements = tryStatement.tryBlock.statements.filter(statement => !ts.isVariableStatement(statement));
1056+
// Local declarations first feed the assignment: `const stats = await stat(p); isDir = stats.isDirectory();`.
1057+
// One after it is work whose failure the catch would hide.
1058+
const all = tryStatement.tryBlock.statements;
1059+
const firstAssignment = all.findIndex(statement => !ts.isVariableStatement(statement));
1060+
const statements = firstAssignment === -1 ? [] : all.slice(firstAssignment);
10511061
if (statements.length === 0) return false;
10521062
const scope = enclosingFunctionOrFile(tryStatement);
10531063
return statements.every(statement => {
@@ -1248,7 +1258,7 @@ export function analyzeSource(sourceText: string, fileName: string): Finding[] {
12481258
const handler = node.arguments[0];
12491259
// `void p.catch(() => {})` says the result is dropped on purpose.
12501260
if ((ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) && ts.isBlock(handler.body) &&
1251-
handler.body.statements.length === 0 && !ts.isVoidExpression(node.parent) &&
1261+
handler.body.statements.length === 0 && !ts.isVoidExpression(skipParenthesesUp(node)) &&
12521262
commentsWithin(handler.body).length === 0 && !isIgnoredBinding(handler.parameters[0]?.name)) {
12531263
report('empty_catch', node.expression.name);
12541264
}

‎tests/rules.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ test('empty catch with an underscore binding is deliberate', () => {
196196
assert.equal(countOf('p.catch((_err) => {});', 'empty_catch'), 0);
197197
assert.equal(countOf('p.catch((err) => {});', 'empty_catch'), 1);
198198
assert.equal(countOf('void reader.cancel().catch(() => {});', 'empty_catch'), 0);
199+
assert.equal(countOf('void (reader.cancel().catch(() => {}));', 'empty_catch'), 0);
199200
assert.equal(countOf('try { a(); } catch (err) {}', 'empty_catch'), 1);
200201
});
201202

@@ -233,7 +234,9 @@ test('empty catch: a variable assigned in the try keeps its earlier value as the
233234
'function f(src: S) {\n let x = 1;\n try {\n x = load();\n } catch {}\n ({ x } = src);\n [x] = [2];\n}',
234235
'function f(xs: number[]) {\n let x = 1;\n try {\n x = load();\n } catch {}\n return xs.map((x) => x * 2);\n}',
235236
'function f(xs: number[]) {\n let x = 1;\n try {\n x = load();\n } catch {}\n for (const x of xs) use(x);\n if (xs.length) { const x = 2; use(x); }\n xs.forEach(() => { var x = 3; use(x); });\n}',
236-
'function f() {\n try {\n const x = load();\n } catch {}\n}'
237+
'function f() {\n try {\n const x = load();\n } catch {}\n}',
238+
// A declaration after the assignment is more work the catch would hide.
239+
'function f() {\n let x = 1;\n try {\n x = load();\n const y = save(x);\n } catch {}\n return x;\n}'
237240
]) {
238241
assert.equal(countOf(code, 'empty_catch'), 1, code);
239242
}

0 commit comments

Comments
 (0)