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) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 18:49:14 +00:00
parent a654c1c764
commit 8a5478c6f0
2 changed files with 51 additions and 1 deletions

View file

@ -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');

View file

@ -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', () => {