refactor(cfg): consolidate no-site def/use accumulator into DefUseAccumulator (#2195 U7)

The Kotlin/Python/Ruby/Rust/Dart/Swift harvesters each carried a
byte-identical copy of the no-site def/use accumulator (~270 lines total;
only Ruby's adds the live useCount() emit-guard helper). Extract it as an
exported DefUseAccumulator beside CallSiteFactAccumulator in
call-site-harvest.ts (the PR's own model for the with-site superset); the six
harvesters import it under their existing local FactAccumulator name. Pure
byte-equivalent move, no logic change: cfg unit suite 613 passed, tsc clean,
bench --check fingerprints unchanged (TS/Go paths untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 16:42:01 +00:00
parent 7ebd579524
commit ccbfef2eb6
7 changed files with 65 additions and 316 deletions

View file

@ -57,6 +57,65 @@ interface SiteFrame {
argIdx: number;
}
/**
* Minimal ordered, deduplicating def/use collector for one statement record,
* with NO call-site machinery (#2195 U7). The Kotlin / Python / Ruby / Rust /
* Dart / Swift harvesters each carried a BYTE-IDENTICAL copy of this class:
* those units harvest NO call sites (the taint substrate is a later step), so a
* site-free accumulator keeps their emitted facts free of any `sites` key
* (matching the Python harvester) and byte-identical to one another. This is the
* no-site sibling of {@link CallSiteFactAccumulator}; `finish` omits `sites`
* entirely. `useCount` is live (Ruby's emit guard is `defCount() ||
* useCount()`).
*/
export class DefUseAccumulator {
private readonly defs: number[] = [];
private readonly uses: number[] = [];
private readonly mayDefs: number[] = [];
private readonly defSeen = new Set<number>();
private readonly useSeen = new Set<number>();
private readonly mayDefSeen = new Set<number>();
constructor(private readonly line: number) {}
addDef(idx: number): void {
if (this.defSeen.has(idx)) return;
this.defSeen.add(idx);
this.defs.push(idx);
}
/** A def that may not execute (conditional context) — gen without kill. */
addMayDef(idx: number): void {
if (this.mayDefSeen.has(idx)) return;
this.mayDefSeen.add(idx);
this.mayDefs.push(idx);
}
addUse(idx: number): void {
if (this.useSeen.has(idx)) return;
this.useSeen.add(idx);
this.uses.push(idx);
}
defCount(): number {
return this.defs.length + this.mayDefs.length;
}
useCount(): number {
return this.uses.length;
}
finish(): StatementFacts {
return {
line: this.line,
defs: this.defs,
uses: this.uses,
// Stay absent when empty — keeps the serialized side-channel payload lean.
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
};
}
}
/**
* Ordered, deduplicating def/use collector for one statement record, PLUS the
* call-site harvest machinery (#2195 U6). A drop-in superset of the simple

View file

@ -78,6 +78,7 @@
*/
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { DefUseAccumulator as FactAccumulator } from './call-site-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set(['function_expression', 'function_body']);
@ -86,58 +87,6 @@ const COMMENT_TYPES = new Set(['comment', 'documentation_comment']);
const FUNCTION_VALUE_TYPES = new Set(['function_expression']);
/**
* Minimal ordered, deduplicating def/use collector for one statement record.
* Deliberately NOT the shared {@link import('./call-site-harvest.js')
* CallSiteFactAccumulator} — this unit harvests NO call sites (taint substrate is
* a later step), so a local accumulator with only the def/use/may-def machinery
* keeps `dart-harvest.ts` free of site logic and guarantees the emitted facts
* carry no `sites` key (mirrors the Kotlin / Swift / Python / Rust harvesters).
*/
class FactAccumulator {
private readonly defs: number[] = [];
private readonly uses: number[] = [];
private readonly mayDefs: number[] = [];
private readonly defSeen = new Set<number>();
private readonly useSeen = new Set<number>();
private readonly mayDefSeen = new Set<number>();
constructor(private readonly line: number) {}
addDef(idx: number): void {
if (this.defSeen.has(idx)) return;
this.defSeen.add(idx);
this.defs.push(idx);
}
/** A def that may not execute (conditional context) — gen without kill. */
addMayDef(idx: number): void {
if (this.mayDefSeen.has(idx)) return;
this.mayDefSeen.add(idx);
this.mayDefs.push(idx);
}
addUse(idx: number): void {
if (this.useSeen.has(idx)) return;
this.useSeen.add(idx);
this.uses.push(idx);
}
defCount(): number {
return this.defs.length + this.mayDefs.length;
}
finish(): StatementFacts {
return {
line: this.line,
defs: this.defs,
uses: this.uses,
// Stay absent when empty — keeps the serialized side-channel payload lean.
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
};
}
}
export class DartHarvester {
private readonly bindings: BindingEntry[] = [];
/** Single function-scope name → binding index (v1: no block scope). */

View file

@ -80,6 +80,7 @@
*/
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { DefUseAccumulator as FactAccumulator } from './call-site-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set([
@ -90,58 +91,6 @@ const NESTED_FUNCTION_TYPES = new Set([
const COMMENT_TYPES = new Set(['line_comment', 'multiline_comment', 'shebang_line']);
/**
* Minimal ordered, deduplicating def/use collector for one statement record.
* Deliberately NOT the shared {@link import('./call-site-harvest.js')
* CallSiteFactAccumulator} — this unit harvests NO call sites (taint substrate is
* a later step), so a local accumulator with only the def/use/may-def machinery
* keeps `kotlin-harvest.ts` free of site logic and guarantees the emitted facts
* carry no `sites` key (mirrors the Swift / Python / Rust harvesters).
*/
class FactAccumulator {
private readonly defs: number[] = [];
private readonly uses: number[] = [];
private readonly mayDefs: number[] = [];
private readonly defSeen = new Set<number>();
private readonly useSeen = new Set<number>();
private readonly mayDefSeen = new Set<number>();
constructor(private readonly line: number) {}
addDef(idx: number): void {
if (this.defSeen.has(idx)) return;
this.defSeen.add(idx);
this.defs.push(idx);
}
/** A def that may not execute (conditional context) — gen without kill. */
addMayDef(idx: number): void {
if (this.mayDefSeen.has(idx)) return;
this.mayDefSeen.add(idx);
this.mayDefs.push(idx);
}
addUse(idx: number): void {
if (this.useSeen.has(idx)) return;
this.useSeen.add(idx);
this.uses.push(idx);
}
defCount(): number {
return this.defs.length + this.mayDefs.length;
}
finish(): StatementFacts {
return {
line: this.line,
defs: this.defs,
uses: this.uses,
// Stay absent when empty — keeps the serialized side-channel payload lean.
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
};
}
}
export class KotlinHarvester {
private readonly bindings: BindingEntry[] = [];
/** Single function-scope name → binding index (v1: no block scope). */

View file

@ -92,6 +92,7 @@
*/
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { DefUseAccumulator as FactAccumulator } from './call-site-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set(['function_definition', 'lambda']);
@ -99,58 +100,6 @@ const NESTED_FUNCTION_TYPES = new Set(['function_definition', 'lambda']);
/** LHS pattern containers whose identifier/splat leaves are assignment targets. */
const PATTERN_LIST_TYPES = new Set(['pattern_list', 'tuple_pattern', 'list_pattern']);
/**
* Minimal ordered, deduplicating def/use collector for one statement record.
* Deliberately NOT the shared {@link import('./call-site-harvest.js')
* CallSiteFactAccumulator} — this unit harvests NO call sites (taint substrate
* is a later step), so a local accumulator with only the def/use/may-def
* machinery keeps `python-harvest.ts` free of site logic and guarantees the
* emitted facts carry no `sites` key.
*/
class FactAccumulator {
private readonly defs: number[] = [];
private readonly uses: number[] = [];
private readonly mayDefs: number[] = [];
private readonly defSeen = new Set<number>();
private readonly useSeen = new Set<number>();
private readonly mayDefSeen = new Set<number>();
constructor(private readonly line: number) {}
addDef(idx: number): void {
if (this.defSeen.has(idx)) return;
this.defSeen.add(idx);
this.defs.push(idx);
}
/** A def that may not execute (conditional context) — gen without kill. */
addMayDef(idx: number): void {
if (this.mayDefSeen.has(idx)) return;
this.mayDefSeen.add(idx);
this.mayDefs.push(idx);
}
addUse(idx: number): void {
if (this.useSeen.has(idx)) return;
this.useSeen.add(idx);
this.uses.push(idx);
}
defCount(): number {
return this.defs.length + this.mayDefs.length;
}
finish(): StatementFacts {
return {
line: this.line,
defs: this.defs,
uses: this.uses,
// Stay absent when empty — keeps the serialized side-channel payload lean.
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
};
}
}
export class PythonHarvester {
private readonly bindings: BindingEntry[] = [];
/** Single function-scope name → binding index (Python has no block scope). */

View file

@ -76,6 +76,7 @@
*/
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { DefUseAccumulator as FactAccumulator } from './call-site-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set([
@ -102,62 +103,6 @@ const NAMED_PARAM_TYPES = new Set([
'keyword_parameter',
]);
/**
* Minimal ordered, deduplicating def/use collector for one statement record.
* Deliberately NOT the shared {@link import('./call-site-harvest.js')
* CallSiteFactAccumulator} — this unit harvests NO call sites (taint substrate
* is a later step), so a local accumulator with only the def/use/may-def
* machinery keeps `ruby-harvest.ts` free of site logic and guarantees the
* emitted facts carry no `sites` key (matching the Python harvester).
*/
class FactAccumulator {
private readonly defs: number[] = [];
private readonly uses: number[] = [];
private readonly mayDefs: number[] = [];
private readonly defSeen = new Set<number>();
private readonly useSeen = new Set<number>();
private readonly mayDefSeen = new Set<number>();
constructor(private readonly line: number) {}
addDef(idx: number): void {
if (this.defSeen.has(idx)) return;
this.defSeen.add(idx);
this.defs.push(idx);
}
/** A def that may not execute (conditional context) — gen without kill. */
addMayDef(idx: number): void {
if (this.mayDefSeen.has(idx)) return;
this.mayDefSeen.add(idx);
this.mayDefs.push(idx);
}
addUse(idx: number): void {
if (this.useSeen.has(idx)) return;
this.useSeen.add(idx);
this.uses.push(idx);
}
defCount(): number {
return this.defs.length + this.mayDefs.length;
}
useCount(): number {
return this.uses.length;
}
finish(): StatementFacts {
return {
line: this.line,
defs: this.defs,
uses: this.uses,
// Stay absent when empty — keeps the serialized side-channel payload lean.
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
};
}
}
export class RubyHarvester {
private readonly bindings: BindingEntry[] = [];
/** Single function-scope name → binding index (documented over-approximation). */

View file

@ -80,6 +80,7 @@
*/
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { DefUseAccumulator as FactAccumulator } from './call-site-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set(['function_item', 'closure_expression']);
@ -92,58 +93,6 @@ const PATTERN_CONTAINER_TYPES = new Set([
'reference_pattern',
]);
/**
* Minimal ordered, deduplicating def/use collector for one statement record.
* Deliberately NOT the shared {@link import('./call-site-harvest.js')
* CallSiteFactAccumulator} — this unit harvests NO call sites (taint substrate
* is a later step), so a local accumulator with only the def/use/may-def
* machinery keeps `rust-harvest.ts` free of site logic and guarantees the
* emitted facts carry no `sites` key (mirrors the Python harvester).
*/
class FactAccumulator {
private readonly defs: number[] = [];
private readonly uses: number[] = [];
private readonly mayDefs: number[] = [];
private readonly defSeen = new Set<number>();
private readonly useSeen = new Set<number>();
private readonly mayDefSeen = new Set<number>();
constructor(private readonly line: number) {}
addDef(idx: number): void {
if (this.defSeen.has(idx)) return;
this.defSeen.add(idx);
this.defs.push(idx);
}
/** A def that may not execute (conditional context) — gen without kill. */
addMayDef(idx: number): void {
if (this.mayDefSeen.has(idx)) return;
this.mayDefSeen.add(idx);
this.mayDefs.push(idx);
}
addUse(idx: number): void {
if (this.useSeen.has(idx)) return;
this.useSeen.add(idx);
this.uses.push(idx);
}
defCount(): number {
return this.defs.length + this.mayDefs.length;
}
finish(): StatementFacts {
return {
line: this.line,
defs: this.defs,
uses: this.uses,
// Stay absent when empty — keeps the serialized side-channel payload lean.
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
};
}
}
export class RustHarvester {
private readonly bindings: BindingEntry[] = [];
/** Single function-scope name → binding index (v1: no block scope). */

View file

@ -78,6 +78,7 @@
*/
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { DefUseAccumulator as FactAccumulator } from './call-site-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set([
@ -87,58 +88,6 @@ const NESTED_FUNCTION_TYPES = new Set([
'lambda_literal',
]);
/**
* Minimal ordered, deduplicating def/use collector for one statement record.
* Deliberately NOT the shared {@link import('./call-site-harvest.js')
* CallSiteFactAccumulator} — this unit harvests NO call sites (taint substrate
* is a later step), so a local accumulator with only the def/use/may-def
* machinery keeps `swift-harvest.ts` free of site logic and guarantees the
* emitted facts carry no `sites` key (mirrors the Python / Rust harvesters).
*/
class FactAccumulator {
private readonly defs: number[] = [];
private readonly uses: number[] = [];
private readonly mayDefs: number[] = [];
private readonly defSeen = new Set<number>();
private readonly useSeen = new Set<number>();
private readonly mayDefSeen = new Set<number>();
constructor(private readonly line: number) {}
addDef(idx: number): void {
if (this.defSeen.has(idx)) return;
this.defSeen.add(idx);
this.defs.push(idx);
}
/** A def that may not execute (conditional context) — gen without kill. */
addMayDef(idx: number): void {
if (this.mayDefSeen.has(idx)) return;
this.mayDefSeen.add(idx);
this.mayDefs.push(idx);
}
addUse(idx: number): void {
if (this.useSeen.has(idx)) return;
this.useSeen.add(idx);
this.uses.push(idx);
}
defCount(): number {
return this.defs.length + this.mayDefs.length;
}
finish(): StatementFacts {
return {
line: this.line,
defs: this.defs,
uses: this.uses,
// Stay absent when empty — keeps the serialized side-channel payload lean.
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
};
}
}
export class SwiftHarvester {
private readonly bindings: BindingEntry[] = [];
/** Single function-scope name → binding index (v1: no block scope). */