From 8a5478c6f01198b99ae2d242e46b06d3787adad3 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 18:49:14 +0000 Subject: [PATCH] fix(cfg): harvest C# out-var and deconstruction-declaration defs (#2195) Two idiomatic C# write shapes recorded ZERO defs, silently breaking REACHING_DEF/taint: - out-var (G(out var n) / G(out int n)) parses as a declaration_expression; it was neither declared (phase 1) nor def'd (phase 2), so n resolved to a synthetic module binding and the callee-written value had no reaching def. - deconstruction declaration (var (a, b) = T()) has a variable_declarator whose name slot is a tuple_pattern (null name field), so declareVariableDeclaration + the variable_declaration walk skipped it entirely (only the assignment form (a,b)=T() was handled). Both a and b were dropped. Declare + def the declaration_expression's identifier (must-def: out params are definitely-assigned), and route a null-name variable_declarator through the tuple_pattern via the existing declareForeachTarget/defTupleTargets helpers. Characterization tests added; csharp suite 42 passed, grammar gate + bench --check green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ingestion/cfg/visitors/csharp-harvest.ts | 39 ++++++++++++++++++- gitnexus/test/unit/cfg/csharp-visitor.test.ts | 13 +++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts index e03114837..5e0296bf3 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts @@ -155,6 +155,14 @@ export class CsharpHarvester extends ScopeTreeHarvester { if (name) this.declare(name, 'var', childScope); break; } + case 'declaration_expression': { + // `f(out var n)` / `f(out int n)` — `n` is a fresh out-binding written + // by the callee; declare it so its def + later uses resolve to a real + // local rather than a synthetic module binding. + const name = node.childForFieldName('name'); + if (name) this.declare(name, 'var', childScope); + break; + } default: break; } @@ -171,7 +179,15 @@ export class CsharpHarvester extends ScopeTreeHarvester { const d = declNode.namedChild(i); if (d?.type !== 'variable_declarator') continue; const name = d.childForFieldName('name'); - if (name) this.declare(name, 'var', scope); + if (name) { + this.declare(name, 'var', scope); + } else { + // Deconstruction declaration `var (a, b) = …;` — the declarator's name + // slot is a `tuple_pattern`; declare each identifier under it (reusing + // the foreach tuple-target logic). + const tuple = d.namedChildren.find((c) => c.type === 'tuple_pattern'); + if (tuple) this.declareForeachTarget(tuple, scope); + } } } @@ -290,6 +306,20 @@ export class CsharpHarvester extends ScopeTreeHarvester { const d = decl.namedChild(i); if (d?.type !== 'variable_declarator') continue; const name = d.childForFieldName('name'); + if (!name) { + // Deconstruction declaration `var (a, b) = e;` — def each + // identifier in the `tuple_pattern`; the initializer is the + // declarator's non-pattern child. + const tuple = d.namedChildren.find((c) => c.type === 'tuple_pattern'); + const tupleInit = d.namedChildren.find((c) => c.type !== 'tuple_pattern'); + if (tuple) { + const snap = acc.defSnapshot(); + this.defTupleTargets(tuple, acc); + if (tupleInit) this.registerResultDefs(tupleInit, acc.defsSince(snap)); + } + if (tupleInit) this.walkValue(tupleInit, acc); + continue; + } // The initializer (if any) is the LAST named child after `name`. const init = this.declaratorInit(d); if (name && init) { @@ -302,6 +332,13 @@ export class CsharpHarvester extends ScopeTreeHarvester { } return; } + case 'declaration_expression': { + // `out var n` / `out int n` — the callee writes `n` (a must-def: C# + // requires an out parameter to be assigned before the method returns). + const name = node.childForFieldName('name'); + if (name) this.def(name, acc); + return; + } case 'assignment_expression': { const left = node.childForFieldName('left'); const right = node.childForFieldName('right'); diff --git a/gitnexus/test/unit/cfg/csharp-visitor.test.ts b/gitnexus/test/unit/cfg/csharp-visitor.test.ts index dcc74f831..b2241e1de 100644 --- a/gitnexus/test/unit/cfg/csharp-visitor.test.ts +++ b/gitnexus/test/unit/cfg/csharp-visitor.test.ts @@ -360,6 +360,19 @@ describe('C# CfgVisitor — def/use harvest', () => { expect(hasDef(cfg, z)).toBe(true); expect(hasUse(cfg, z)).toBe(true); }); + + it('out var n records n as a callee-written def (#2195 P1)', () => { + const cfg = cs.cfgOf(`class C { void M(string s) { int.TryParse(s, out var n); use(n); } }`); + const n = bindingIdx(cfg, 'n'); + expect(hasDef(cfg, n)).toBe(true); + expect(hasUse(cfg, n)).toBe(true); + }); + + it('var (a, b) = T() deconstruction declaration defines BOTH a and b (#2195 P1)', () => { + const cfg = cs.cfgOf(`class C { void M() { var (a, b) = T(); use(a); use(b); } }`); + expect(hasDef(cfg, bindingIdx(cfg, 'a'))).toBe(true); + expect(hasDef(cfg, bindingIdx(cfg, 'b'))).toBe(true); + }); }); describe('C# CfgVisitor — functionStartColumn', () => {