fix(cfg): harvest Go select channel-receive binding as a def (#2195)

walkValue had no receive_statement case, so a select receive (case v := <-ch:)
fell to the default descent: v was recorded as a USE of an uninitialized var
and the channel-sourced definition was invisible to REACHING_DEF/taint —
channels are a primary taint source in Go. Add the case mirroring
short_var_declaration: def each left identifier, use the <-ch right, attach
resultDefs for the := short form. prescan already declared the binding; this
completes the phase-2 fact. go:branchy bench fingerprint unchanged; go suite
40 passed, 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:31:20 +00:00
parent 33446d61c3
commit 31936a0929
2 changed files with 22 additions and 0 deletions

View file

@ -438,6 +438,18 @@ export class GoHarvester extends ScopeTreeHarvester {
if (left) this.defLeftList(left, acc, op !== '=');
return;
}
case 'receive_statement': {
// `select { case v := <-ch: }` / `case v = <-ch:` — the left
// identifier(s) are DEFS of the channel-received value; `<-ch` (right)
// is a use of the channel. A bare `case <-ch:` has no left (uses only).
const left = node.childForFieldName('left');
const right = node.childForFieldName('right');
const isShort = node.children.some((c) => !c.isNamed && c.text === ':=');
if (isShort && left && right) this.registerListResultDefs(left, right);
if (right) this.walkValue(right, acc);
if (left) this.defLeftList(left, acc, false);
return;
}
case 'inc_statement':
case 'dec_statement': {
// `x++` / `x--` — def AND use the lvalue when it's a plain identifier.

View file

@ -407,6 +407,16 @@ describe('Go CfgVisitor — def/use harvest', () => {
expect(hasUse(cfg, bindingIdx(cfg, 'k'))).toBe(true);
void hasMayDef; // may-def path covered structurally by the conditional walk
});
it('select receive `case v := <-ch` defines v, not a use-only (#2195 P2)', () => {
const cfg = go.cfgOf(pkg(`func f(ch chan int) { select { case v := <-ch: use(v) } }`));
const v = bindingIdx(cfg, 'v');
// the channel-received binding is a DEF (the channel-sourced value flows
// into v) — the bug recorded it as a use of an uninitialized var instead.
expect(hasDef(cfg, v)).toBe(true);
// and the channel `ch` is read.
expect(hasUse(cfg, bindingIdx(cfg, 'ch'))).toBe(true);
});
});
describe('Go CfgVisitor — functionStartColumn', () => {