fix(cfg): harvest C++ structured-binding defs (auto [a,b]=e) (#2195)

The C++ def/use harvester only recorded a def when an init_declarator's
declarator was a plain identifier, so a structured binding (auto [a,b] = mk(),
incl. the auto& reference form whose binding sits under a reference_declarator)
declared only the first name in phase 1 and emitted ZERO defs in phase 2 — a,b
were walked as spurious uses and later use(a)/use(b) resolved to a synthetic
module binding, silently corrupting REACHING_DEF/taint for an idiomatic C++17
shape. Unwrap the structured_binding_declarator in both phases and def every
identifier leaf; result-of-initializer flows to the whole list. Inert for C
(no structured bindings). Characterization tests added (plain + reference form).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 18:39:28 +00:00
parent 213ae2526e
commit 5165d71d7b
2 changed files with 67 additions and 2 deletions

View file

@ -190,13 +190,46 @@ export class CCppHarvester extends ScopeTreeHarvester {
}
}
/**
* The `structured_binding_declarator` a declarator binds (C++17 `auto [a,b]`,
* or `auto& [a,b]` whose binding sits under a `reference_declarator`), or
* undefined. C has no structured bindings, so this is inert for C.
*/
private structuredBinding(declarator: SyntaxNode | null | undefined): SyntaxNode | undefined {
let cur = declarator ?? undefined;
let hops = 4;
while (cur && hops-- > 0) {
if (cur.type === 'structured_binding_declarator') return cur;
if (cur.type !== 'reference_declarator' && cur.type !== 'pointer_declarator') break;
cur = cur.namedChildren.find(
(c) =>
c.type === 'structured_binding_declarator' ||
c.type === 'reference_declarator' ||
c.type === 'pointer_declarator',
);
}
return undefined;
}
/** The identifier leaves a `structured_binding_declarator` binds (`[a, b]`). */
private structuredBindingNames(sbd: SyntaxNode): SyntaxNode[] {
return sbd.namedChildren.filter((c) => c.type === 'identifier');
}
private declareDeclarators(declNode: SyntaxNode, scope: Scope): void {
for (let i = 0; i < declNode.namedChildCount; i++) {
const d = declNode.namedChild(i);
if (!d) continue;
if (d.type === 'init_declarator') {
const name = this.declaratorName(d.childForFieldName('declarator') ?? null);
if (name) this.declare(name, 'var', scope);
const declarator = d.childForFieldName('declarator');
const sbd = this.structuredBinding(declarator);
if (sbd) {
// `auto [a, b] = e;` — every identifier binds a scalar local.
for (const id of this.structuredBindingNames(sbd)) this.declare(id, 'var', scope);
} else {
const name = this.declaratorName(declarator ?? null);
if (name) this.declare(name, 'var', scope);
}
} else if (
d.type === 'identifier' ||
d.type === 'pointer_declarator' ||
@ -301,6 +334,16 @@ export class CCppHarvester extends ScopeTreeHarvester {
if (d?.type !== 'init_declarator') continue;
const declarator = d.childForFieldName('declarator');
const value = d.childForFieldName('value');
const sbd = this.structuredBinding(declarator);
if (sbd && value) {
// `auto [a, b] = e;` — each identifier is a scalar def; the result
// of `e` flows into all of them (resultDefs covers the whole list).
const snap = acc.defSnapshot();
for (const id of this.structuredBindingNames(sbd)) this.def(id, acc);
this.registerResultDefs(value, acc.defsSince(snap));
this.walkValue(value, acc);
continue;
}
const name = declarator ? this.declaratorName(declarator) : undefined;
// Only an INITIALIZED declarator writes (`int x = e;`). A bare
// `int x;` is not a def (it writes nothing at runtime), matching the

View file

@ -231,6 +231,28 @@ describe('C CfgVisitor — def/use harvest', () => {
});
});
describe('C++ CfgVisitor — structured bindings (#2195 P1)', () => {
const isDef = (cfg: FunctionCfg, idx: number): boolean =>
cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx)));
it('auto [a, b] = mk(); defines BOTH a and b (not just the first / neither)', () => {
const cfg = cpp.cfgOf(`void f() { auto [a, b] = mk(); use(a); use(b); }`);
expect(isDef(cfg, bindingIdx(cfg, 'a'))).toBe(true);
expect(isDef(cfg, bindingIdx(cfg, 'b'))).toBe(true);
// a later use(a) must resolve to the SAME binding the declaration defs —
// i.e. `a` is a real local, not a synthetic module binding.
const a = bindingIdx(cfg, 'a');
const usedA = cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(a)));
expect(usedA).toBe(true);
});
it('auto& [a, b] = ref(); (reference structured binding) defines both names', () => {
const cfg = cpp.cfgOf(`void f() { auto& [a, b] = ref(); sink(a, b); }`);
expect(isDef(cfg, bindingIdx(cfg, 'a'))).toBe(true);
expect(isDef(cfg, bindingIdx(cfg, 'b'))).toBe(true);
});
});
describe('C CfgVisitor — functionStartColumn', () => {
it('two same-line functions get distinct functionStartColumn', () => {
const cfgs = c.cfgsOf(`int a(){return 1;} int b(){return 2;}`);