From ccbfef2eb65087eaecc65ae843582bde2385f129 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 16:42:01 +0000 Subject: [PATCH] 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) --- .../cfg/visitors/call-site-harvest.ts | 59 +++++++++++++++++++ .../ingestion/cfg/visitors/dart-harvest.ts | 53 +---------------- .../ingestion/cfg/visitors/kotlin-harvest.ts | 53 +---------------- .../ingestion/cfg/visitors/python-harvest.ts | 53 +---------------- .../ingestion/cfg/visitors/ruby-harvest.ts | 57 +----------------- .../ingestion/cfg/visitors/rust-harvest.ts | 53 +---------------- .../ingestion/cfg/visitors/swift-harvest.ts | 53 +---------------- 7 files changed, 65 insertions(+), 316 deletions(-) diff --git a/gitnexus/src/core/ingestion/cfg/visitors/call-site-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/call-site-harvest.ts index cb9e4eba9..0ff3313da 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/call-site-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/call-site-harvest.ts @@ -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(); + private readonly useSeen = new Set(); + private readonly mayDefSeen = new Set(); + + 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 diff --git a/gitnexus/src/core/ingestion/cfg/visitors/dart-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/dart-harvest.ts index ca4bb366d..381fe8dca 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/dart-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/dart-harvest.ts @@ -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(); - private readonly useSeen = new Set(); - private readonly mayDefSeen = new Set(); - - 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). */ diff --git a/gitnexus/src/core/ingestion/cfg/visitors/kotlin-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/kotlin-harvest.ts index 52359a8dc..d9c04f82f 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/kotlin-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/kotlin-harvest.ts @@ -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(); - private readonly useSeen = new Set(); - private readonly mayDefSeen = new Set(); - - 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). */ diff --git a/gitnexus/src/core/ingestion/cfg/visitors/python-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/python-harvest.ts index c5dd39e1f..6ea99c883 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/python-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/python-harvest.ts @@ -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(); - private readonly useSeen = new Set(); - private readonly mayDefSeen = new Set(); - - 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). */ diff --git a/gitnexus/src/core/ingestion/cfg/visitors/ruby-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/ruby-harvest.ts index 7adad3443..ec1c5e4bd 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/ruby-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/ruby-harvest.ts @@ -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(); - private readonly useSeen = new Set(); - private readonly mayDefSeen = new Set(); - - 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). */ diff --git a/gitnexus/src/core/ingestion/cfg/visitors/rust-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/rust-harvest.ts index 0610859ea..7f2dd7034 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/rust-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/rust-harvest.ts @@ -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(); - private readonly useSeen = new Set(); - private readonly mayDefSeen = new Set(); - - 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). */ diff --git a/gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts index dce687404..f03f98918 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts @@ -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(); - private readonly useSeen = new Set(); - private readonly mayDefSeen = new Set(); - - 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). */