fix(cfg): harvest all names of a Dart multi-variable declaration (#2195)

`var a = 1, b = 2;` is one initialized_variable_definition whose first binding
is the name/value field pair and whose subsequent bindings are trailing
initialized_identifier children. Both prescan (declareInitializedVar) and the
walkValue case read only the name/value fields, so every name after the first
was never declared or def'd — `b` resolved to a synthetic module binding and
its REACHING_DEF/taint flow was lost. Iterate the trailing initialized_identifier
nodes in both phases. (Dart-3 record/list pattern declarations `var (a,b)=pair`
remain a separate follow-up.) dart suite 35 passed, tsc + grammar gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 19:33:45 +00:00
parent 31936a0929
commit 324bde0368
2 changed files with 28 additions and 1 deletions

View file

@ -212,10 +212,18 @@ export class DartHarvester {
}
}
/** Declare the `name:identifier` of an `initialized_variable_definition`. */
/** Declare every name of an `initialized_variable_definition` (`var a = 1, b = 2`). */
private declareInitializedVar(node: SyntaxNode, kind: BindingEntry['kind']): void {
const name = node.childForFieldName('name');
if (name) this.declare(name, kind);
// Trailing comma-separated bindings: each `initialized_identifier` (`b = 2`)
// names another local that the `name` field alone misses.
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (c?.type !== 'initialized_identifier') continue;
const id = c.namedChildren.find((g) => g.type === 'identifier');
if (id) this.declare(id, kind);
}
}
/**
@ -367,6 +375,16 @@ export class DartHarvester {
if (value) this.walkValue(value, acc);
const name = node.childForFieldName('name');
if (name) this.def(name, acc);
// Trailing comma-separated bindings (`var a = 1, b = 2;`): each
// `initialized_identifier` is an `identifier` + its own value expr.
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (c?.type !== 'initialized_identifier') continue;
const id = c.namedChildren.find((g) => g.type === 'identifier');
const val = c.namedChildren.find((g) => g.type !== 'identifier');
if (val) this.walkValue(val, acc);
if (id) this.def(id, acc);
}
return;
}
case 'assignment_expression': {

View file

@ -387,6 +387,15 @@ describe('Dart CfgVisitor — def/use harvest', () => {
// `obj` is used (it is the assignment target's root); no scalar `x` binding.
expect(usesBinding(cfg, bindingIdx(cfg, 'obj'))).toBe(true);
});
it('multi-variable declaration `var a = 1, b = 2;` defines BOTH names (#2195 P2)', () => {
const cfg = dart.cfgOf(`void f() { var a = 1, b = 2; use(a); use(b); }`);
// The bug dropped every name after the first — `b` became a synthetic
// global. Both must be real locals defined by the declaration.
expect(definesBinding(cfg, bindingIdx(cfg, 'a'))).toBe(true);
expect(definesBinding(cfg, bindingIdx(cfg, 'b'))).toBe(true);
expect(usesBinding(cfg, bindingIdx(cfg, 'b'))).toBe(true); // use(b)
});
});
describe('Dart CfgVisitor — functionStartColumn', () => {