From 33446d61c356c2b5aafac260cd6ec2411b334508 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 19:27:20 +0000 Subject: [PATCH] fix(cfg): harvest Kotlin x++/--x as a def (#2195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Kotlin harvester had no postfix_expression/prefix_expression case, so an increment/decrement fell to the default descent and recorded its operand as a use only — never a def. Every sibling harvester (Java/C#/C++/Dart/TS/PHP) models inc/dec, so a Kotlin counting loop (while/for using i++) silently dropped the loop-carried reaching-def of the counter. Add the case: def AND use the operand when it is a plain simple_identifier and the operator is ++/-- (other pre/postfix forms — -x, !x, x!!, x? — stay pure reads, byte-identical to the old descent). Characterization tests for postfix + prefix added; kotlin suite 30 passed, grammar gate + bench --check green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ingestion/cfg/visitors/kotlin-harvest.ts | 23 +++++++++++++++++++ gitnexus/test/unit/cfg/kotlin-visitor.test.ts | 18 +++++++++++++++ 2 files changed, 41 insertions(+) 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', () => {