diff --git a/gitnexus/src/core/ingestion/cfg/visitors/kotlin.ts b/gitnexus/src/core/ingestion/cfg/visitors/kotlin.ts index d97335a94..5d58052b5 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/kotlin.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/kotlin.ts @@ -739,9 +739,19 @@ class KotlinCfgWalk { const tryHandler = firstCatchEntry ?? finalizerEntry ?? this.currentHandler(); const protectedStart = this.builder.blockCount; this.handlers.push(tryHandler); - const bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null; + let bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null; this.handlers.pop(); + if (bodyRes === null && (catchBlocks.length > 0 || finalizerEntry !== undefined)) { + // An empty `try {}` body still establishes a protected region. Synthesize + // one block (like the empty-`catch` case above) so the throw-edge loop + // wires the catch handler(s) and the try's entry is the body — otherwise + // the catch handler block + its error binding are orphaned (unreachable + // from ENTRY) and control routes straight to the finally, bypassing catch. + const idx = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), ''); + bodyRes = { entry: idx, exits: [idx] }; + } + if (catchBlocks.length > 0 || finalizerEntry !== undefined) { for (let b = protectedStart; b < this.builder.blockCount; b++) { this.builder.edge(b, tryHandler, 'throw'); diff --git a/gitnexus/test/unit/cfg/kotlin-visitor.test.ts b/gitnexus/test/unit/cfg/kotlin-visitor.test.ts index 3391b83ad..15ec24a47 100644 --- a/gitnexus/test/unit/cfg/kotlin-visitor.test.ts +++ b/gitnexus/test/unit/cfg/kotlin-visitor.test.ts @@ -239,6 +239,18 @@ describe('Kotlin CfgVisitor — try/catch/finally', () => { expect(reaches(cfg, thr, block(cfg, 'done()'))).toBe(false); expect(reachable(cfg, block(cfg, 'done()'))).toBe(true); // via the if false branch }); + + it('empty try {} with catch + finally keeps the catch handler reachable (#2195)', () => { + // An empty try body still establishes a protected region — the catch (and + // its error binding) must not be orphaned/unreachable from ENTRY. + const cfg = kotlin.cfgOf( + `fun f() { try {} catch (e: Exception) { handle(e) } finally { cl() }; a() }`, + ); + expect(reachable(cfg, block(cfg, 'handle(e)'))).toBe(true); + expect(reachable(cfg, block(cfg, 'cl()'))).toBe(true); + expect(reachable(cfg, block(cfg, 'a()'))).toBe(true); + expect(definesBinding(cfg, bindingIdx(cfg, 'e'))).toBe(true); + }); }); describe('Kotlin CfgVisitor — labeled break/continue', () => {