diff --git a/gitnexus/src/core/ingestion/cfg/visitors/kotlin-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/kotlin-harvest.ts index 15c12b88f..c59fee602 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/kotlin-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/kotlin-harvest.ts @@ -425,6 +425,20 @@ export class KotlinHarvester { } return; } + case 'postfix_expression': + case 'prefix_expression': { + // `x++` / `--x` — def AND use the operand when it is a plain identifier + // and the operator is an increment/decrement. Other pre/postfix forms + // (`-x`, `!x`, `x!!`, `x?`) are pure reads → walk the operand as a use. + const operand = node.namedChild(0); + if (operand?.type === 'simple_identifier' && this.isIncDec(node)) { + this.def(operand, acc); + this.use(operand, acc); + } else if (operand) { + this.walkValue(operand, acc); + } + return; + } case 'navigation_expression': { // `a.b` / `a?.b` — value read of the chain root only; the suffix name is // not a scalar binding. @@ -467,6 +481,15 @@ export class KotlinHarvester { } } + /** True iff `node` carries a `++` / `--` operator token (`x++` / `--x`). */ + private isIncDec(node: SyntaxNode): boolean { + for (let i = 0; i < node.childCount; i++) { + const c = node.child(i); + if (c && !c.isNamed && (c.text === '++' || c.text === '--')) return true; + } + return false; + } + /** The `= value` expression of a `property_declaration` (the child after `=`). */ private propertyValue(node: SyntaxNode): SyntaxNode | undefined { let sawEq = false; diff --git a/gitnexus/test/unit/cfg/kotlin-visitor.test.ts b/gitnexus/test/unit/cfg/kotlin-visitor.test.ts index f41e1b299..3391b83ad 100644 --- a/gitnexus/test/unit/cfg/kotlin-visitor.test.ts +++ b/gitnexus/test/unit/cfg/kotlin-visitor.test.ts @@ -316,6 +316,24 @@ describe('Kotlin CfgVisitor — def/use harvest', () => { expect(definesBinding(cfg, x)).toBe(true); expect(usesBinding(cfg, x)).toBe(true); }); + + const defStmtCount = (cfg: FunctionCfg, idx: number): number => + cfg.blocks.flatMap((bl) => bl.statements ?? []).filter((s) => s.defs.includes(idx)).length; + + it('postfix `x++` defines AND uses the operand (#2195 P2)', () => { + const cfg = kotlin.cfgOf(`fun f() { var x = 0; x++ }`); + const x = bindingIdx(cfg, 'x'); + // `var x = 0` defs x once; `x++` must def it AGAIN (the loop-counter + // reaching-def the bug dropped) — not record x as a use-only. + expect(defStmtCount(cfg, x)).toBe(2); + expect(usesBinding(cfg, x)).toBe(true); + }); + + it('prefix `--x` defines the operand too (#2195 P2)', () => { + const cfg = kotlin.cfgOf(`fun f() { var x = 0; --x }`); + const x = bindingIdx(cfg, 'x'); + expect(defStmtCount(cfg, x)).toBe(2); + }); }); describe('Kotlin CfgVisitor — functionStartColumn', () => {