diff --git a/jacodb-ets/ts-frontend/src/lowering/stmtLowering.ts b/jacodb-ets/ts-frontend/src/lowering/stmtLowering.ts index 5f79f73f4..66dd6389b 100644 --- a/jacodb-ets/ts-frontend/src/lowering/stmtLowering.ts +++ b/jacodb-ets/ts-frontend/src/lowering/stmtLowering.ts @@ -629,7 +629,11 @@ export class StmtLowerer { cfg.placeLabel(catchLabel); const decl = node.catchClause.variableDeclaration; - if (decl !== undefined && ts.isIdentifier(decl.name)) { + if ( + decl !== undefined && + ts.isIdentifier(decl.name) && + this.referencesCatchBinding(node.catchClause.block, decl.name) + ) { const caughtType = decl.type !== undefined ? this.m.converter.convertTypeNode(decl.type) : UNKNOWN_TYPE; const caught = this.m.localForIdentifier(decl.name, caughtType); @@ -660,6 +664,25 @@ export class StmtLowerer { } } + private referencesCatchBinding(block: ts.Block, name: ts.Identifier): boolean { + const bindingSymbol = this.m.converter.symbolOf(name); + if (bindingSymbol === undefined) { + return referencesName(block, name.text); + } + + let found = false; + const visit = (node: ts.Node): void => { + if (found || ts.isTypeNode(node)) return; + if (ts.isIdentifier(node) && this.m.converter.symbolOf(node) === bindingSymbol) { + found = true; + return; + } + ts.forEachChild(node, visit); + }; + visit(block); + return found; + } + /** * Emit copies of the enclosing finally blocks with stack depth > `downTo`, * innermost first — used before abrupt exits (return/throw/break/continue). diff --git a/jacodb-ets/ts-frontend/test/try.spec.ts b/jacodb-ets/ts-frontend/test/try.spec.ts index 07fd39818..cd80ddedb 100644 --- a/jacodb-ets/ts-frontend/test/try.spec.ts +++ b/jacodb-ets/ts-frontend/test/try.spec.ts @@ -41,6 +41,68 @@ describe("try/catch/finally lowering", () => { expect(blocks[0].successors).toHaveLength(2); }); + it("omits an unused catch binding while preserving its return", () => { + const blocks = blocksOf(` + function f(): boolean { + try { + return BUILD_FLAG; + } catch (error) { + return false; + } + } + `); + + const stmts = blocks.flatMap((block) => block.stmts); + const catchBindings = stmts.filter( + (stmt) => stmt._ === "AssignStmt" && stmt.right._ === "CaughtExceptionRef", + ); + const returns = stmts.filter((stmt) => stmt._ === "ReturnStmt"); + + expect(catchBindings).toHaveLength(0); + expect(returns).toHaveLength(2); + }); + + it("ignores matching property names and shadowed locals in a catch body", () => { + const sources = [ + `function f(obj: { error: number }): number { + try { return 0; } catch (error) { return obj.error; } + }`, + `function f(): number { + try { return 0; } catch (error) { + { const error = 1; return error; } + } + }`, + `function f(): number { + try { return 0; } catch (error) { + type CaughtType = typeof error; + return 1; + } + }`, + ]; + + for (const source of sources) { + const stmts = blocksOf(source).flatMap((block) => block.stmts); + const catchBindings = stmts.filter( + (stmt) => stmt._ === "AssignStmt" && stmt.right._ === "CaughtExceptionRef", + ); + + expect(catchBindings).toHaveLength(0); + } + }); + + it("keeps a catch binding referenced by a shorthand property", () => { + const stmts = blocksOf(` + function f(): object { + try { return {}; } catch (error) { return { error }; } + } + `).flatMap((block) => block.stmts); + const catchBindings = stmts.filter( + (stmt) => stmt._ === "AssignStmt" && stmt.right._ === "CaughtExceptionRef", + ); + + expect(catchBindings).toHaveLength(1); + }); + it("joins try and catch paths on the finally block", () => { const blocks = blocksOf(` function f(): void { @@ -245,6 +307,7 @@ describe("try/catch/finally lowering", () => { const stmts = blocks.flatMap((b) => b.stmts); expect(stmts.some((s) => s._ === "ThrowStmt")).toBe(true); const caught = stmts.filter((s) => s._ === "AssignStmt" && s.right._ === "CaughtExceptionRef"); - expect(caught).toHaveLength(2); + expect(caught).toHaveLength(0); + expect(stmts.filter((s) => s._ === "ReturnStmt")).toHaveLength(3); }); });