From 7ebd579524308a19f599fa8679a042153f8afa45 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 16:32:24 +0000 Subject: [PATCH] refactor(cfg): consolidate scope-tree substrate into ScopeTreeHarvester (#2195 U6) The Go/Java/C#/C-C++ def/use harvesters each carried a byte-identical copy of the lexical scope-tree machinery (Scope record, two-phase resolution cache, openScope/nearestScopeOf/resolve/def/use/conditional/bindingTable, ~270 lines total). Extract it into an abstract ScopeTreeHarvester base; the four harvesters now extend it and supply only their genuine per-language variation (the prescan switch, plus Go's _-blank-identifier overrides of declare/def/use). Net -422 lines. Mechanical and byte-equivalent: cfg unit suite 613 passed, bench --check fingerprints unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ingestion/cfg/visitors/c-cpp-harvest.ts | 123 +------------ .../ingestion/cfg/visitors/csharp-harvest.ts | 121 +------------ .../core/ingestion/cfg/visitors/go-harvest.ts | 109 ++--------- .../ingestion/cfg/visitors/java-harvest.ts | 121 +------------ .../cfg/visitors/scope-tree-harvest.ts | 171 ++++++++++++++++++ 5 files changed, 197 insertions(+), 448 deletions(-) create mode 100644 gitnexus/src/core/ingestion/cfg/visitors/scope-tree-harvest.ts diff --git a/gitnexus/src/core/ingestion/cfg/visitors/c-cpp-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/c-cpp-harvest.ts index af8eb432f..5556616a9 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/c-cpp-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/c-cpp-harvest.ts @@ -52,13 +52,7 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js'; import type { BindingEntry, StatementFacts } from '../types.js'; import { CallSiteFactAccumulator } from './call-site-harvest.js'; - -/** - * The per-statement def/use + call-site collector. Aliased to the shared - * {@link CallSiteFactAccumulator} so the harvester references one name for both - * the value (constructor) and the type. - */ -type FactAccumulator = CallSiteFactAccumulator; +import { ScopeTreeHarvester, type Scope, type FactAccumulator } from './scope-tree-harvest.js'; /** Node types that own a nested CFG — their subtrees are opaque to harvesting. */ const NESTED_FUNCTION_TYPES = new Set(['lambda_expression', 'function_definition']); @@ -83,43 +77,14 @@ const TYPE_CONTEXT_TYPES = new Set([ 'primitive_type', ]); -interface Scope { - readonly parent: Scope | null; - /** name → binding index */ - readonly table: Map; -} - -export class CCppHarvester { - private readonly bindings: BindingEntry[] = []; - private readonly scopeByNode = new Map(); - private readonly root: Scope = { parent: null, table: new Map() }; - private readonly synthetic = new Map(); - private readonly fnId: number; - /** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */ - private readonly nearestScopeCache = new Map(); - /** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */ - private conditionalDepth = 0; - /** - * Call/new node id → bindings whose declarator/assignment VALUE is exactly - * that call (#2195 U6). Registered by the declaration/assignment handlers - * BEFORE the value walk, consumed by {@link visitCall} when it reaches the - * node (mirrors the TS harvester's `resultDefTargets`). - */ - private readonly resultDefTargets = new Map(); - - constructor(private readonly fnNode: SyntaxNode) { - this.fnId = fnNode.id; - this.scopeByNode.set(fnNode.id, this.root); +export class CCppHarvester extends ScopeTreeHarvester { + constructor(fnNode: SyntaxNode) { + super(fnNode); this.declareParams(fnNode); const body = this.bodyOf(fnNode); if (body) this.prescan(body, this.openScope(body)); } - /** The completed binding table — pass to `CfgBuilder.finish`. */ - bindingTable(): readonly BindingEntry[] { - return this.bindings; - } - /** The function/lambda body block (`compound_statement`). */ private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined { const body = fnNode.childForFieldName('body'); @@ -129,35 +94,6 @@ export class CCppHarvester { // ── phase 1: declaration pre-scan ──────────────────────────────────────── - private openScope(node: SyntaxNode): Scope { - const existing = this.scopeByNode.get(node.id); - if (existing) return existing; - const scope: Scope = { parent: this.nearestScopeOf(node), table: new Map() }; - this.scopeByNode.set(node.id, scope); - return scope; - } - - private nearestScopeOf(node: SyntaxNode): Scope { - for (let p = node.parent; p; p = p.parent) { - const s = this.scopeByNode.get(p.id); - if (s) return s; - if (p.id === this.fnId) break; - } - return this.root; - } - - private declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void { - const name = nameNode.text; - if (!name || scope.table.has(name)) return; - scope.table.set(name, this.bindings.length); - this.bindings.push({ - name, - declLine: nameNode.startPosition.row + 1, - declColumn: nameNode.startPosition.column, - kind, - }); - } - /** The bare identifier a declarator chain ultimately names (or undefined). */ private declaratorName(node: SyntaxNode | null): SyntaxNode | undefined { let cur: SyntaxNode | null = node; @@ -210,7 +146,7 @@ export class CCppHarvester { return undefined; } - private prescan(node: SyntaxNode, scope: Scope): void { + protected prescan(node: SyntaxNode, scope: Scope): void { this.nearestScopeCache.set(node.id, scope); const t = node.type; if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) { @@ -333,55 +269,6 @@ export class CCppHarvester { return acc.defCount() ? acc.finish() : undefined; } - private resolve(nameNode: SyntaxNode): number { - const name = nameNode.text; - const cached = this.nearestScopeCache.get(nameNode.id); - let startScope: Scope | null = cached ?? null; - if (!startScope) { - for (let p: SyntaxNode | null = nameNode; p; p = p.parent) { - const scope = this.scopeByNode.get(p.id) ?? this.nearestScopeCache.get(p.id); - if (scope) { - startScope = scope; - break; - } - if (p.id === this.fnId) { - startScope = this.root; - break; - } - } - } - for (let s: Scope | null = startScope; s; s = s.parent) { - const idx = s.table.get(name); - if (idx !== undefined) return idx; - } - let idx = this.synthetic.get(name); - if (idx === undefined) { - idx = this.bindings.length; - this.synthetic.set(name, idx); - this.bindings.push({ name, declLine: 0, declColumn: 0, kind: 'module', synthetic: true }); - } - return idx; - } - - private def(nameNode: SyntaxNode, acc: FactAccumulator): void { - if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode)); - else acc.addDef(this.resolve(nameNode)); - } - - private use(nameNode: SyntaxNode, acc: FactAccumulator): void { - acc.addUse(this.resolve(nameNode)); - } - - /** Run `fn` with defs demoted to may-defs (conditionally-evaluated context). */ - private conditional(fn: () => void): void { - this.conditionalDepth++; - try { - fn(); - } finally { - this.conditionalDepth--; - } - } - /** Strip parenthesized wrappers around an lvalue (`(x) = 1`). */ private unwrapLvalue(node: SyntaxNode): SyntaxNode { let n = node; diff --git a/gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts index 860808100..e03114837 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts @@ -47,12 +47,7 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js'; import type { BindingEntry, StatementFacts } from '../types.js'; import { CallSiteFactAccumulator } from './call-site-harvest.js'; - -/** - * The per-statement def/use + call-site collector, aliased to the shared - * {@link CallSiteFactAccumulator} (one name for the value and the type). - */ -type FactAccumulator = CallSiteFactAccumulator; +import { ScopeTreeHarvester, type Scope, type FactAccumulator } from './scope-tree-harvest.js'; /** Node types that own a nested CFG — their subtrees are opaque to harvesting. */ const NESTED_FUNCTION_TYPES = new Set([ @@ -80,42 +75,14 @@ const SCOPE_TYPES = new Set([ 'switch_expression_arm', ]); -interface Scope { - readonly parent: Scope | null; - /** name → binding index */ - readonly table: Map; -} - -export class CsharpHarvester { - private readonly bindings: BindingEntry[] = []; - private readonly scopeByNode = new Map(); - private readonly root: Scope = { parent: null, table: new Map() }; - private readonly synthetic = new Map(); - private readonly fnId: number; - /** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */ - private readonly nearestScopeCache = new Map(); - /** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */ - private conditionalDepth = 0; - /** - * Call/new node id → bindings whose declarator/assignment VALUE is exactly - * that call (#2195 U6). Registered before the value walk, consumed by - * {@link visitCall} (mirrors the TS harvester's `resultDefTargets`). - */ - private readonly resultDefTargets = new Map(); - - constructor(private readonly fnNode: SyntaxNode) { - this.fnId = fnNode.id; - this.scopeByNode.set(fnNode.id, this.root); +export class CsharpHarvester extends ScopeTreeHarvester { + constructor(fnNode: SyntaxNode) { + super(fnNode); this.declareParams(fnNode); const body = this.bodyOf(fnNode); if (body) this.prescan(body, this.openScope(body)); } - /** The completed binding table — pass to `CfgBuilder.finish`. */ - bindingTable(): readonly BindingEntry[] { - return this.bindings; - } - /** The function/lambda body node (a `block` or an expression for `=> expr`). */ private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined { const body = fnNode.childForFieldName('body'); @@ -126,35 +93,6 @@ export class CsharpHarvester { // ── phase 1: declaration pre-scan ──────────────────────────────────────── - private openScope(node: SyntaxNode): Scope { - const existing = this.scopeByNode.get(node.id); - if (existing) return existing; - const scope: Scope = { parent: this.nearestScopeOf(node), table: new Map() }; - this.scopeByNode.set(node.id, scope); - return scope; - } - - private nearestScopeOf(node: SyntaxNode): Scope { - for (let p = node.parent; p; p = p.parent) { - const s = this.scopeByNode.get(p.id); - if (s) return s; - if (p.id === this.fnId) break; - } - return this.root; - } - - private declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void { - const name = nameNode.text; - if (!name || scope.table.has(name)) return; - scope.table.set(name, this.bindings.length); - this.bindings.push({ - name, - declLine: nameNode.startPosition.row + 1, - declColumn: nameNode.startPosition.column, - kind, - }); - } - private declareParams(fnNode: SyntaxNode): void { const params = fnNode.childForFieldName('parameters') ?? @@ -175,7 +113,7 @@ export class CsharpHarvester { } } - private prescan(node: SyntaxNode, scope: Scope): void { + protected prescan(node: SyntaxNode, scope: Scope): void { this.nearestScopeCache.set(node.id, scope); const t = node.type; if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) { @@ -308,55 +246,6 @@ export class CsharpHarvester { return acc.defCount() ? acc.finish() : undefined; } - private resolve(nameNode: SyntaxNode): number { - const name = nameNode.text; - const cached = this.nearestScopeCache.get(nameNode.id); - let startScope: Scope | null = cached ?? null; - if (!startScope) { - for (let p: SyntaxNode | null = nameNode; p; p = p.parent) { - const scope = this.scopeByNode.get(p.id) ?? this.nearestScopeCache.get(p.id); - if (scope) { - startScope = scope; - break; - } - if (p.id === this.fnId) { - startScope = this.root; - break; - } - } - } - for (let s: Scope | null = startScope; s; s = s.parent) { - const idx = s.table.get(name); - if (idx !== undefined) return idx; - } - let idx = this.synthetic.get(name); - if (idx === undefined) { - idx = this.bindings.length; - this.synthetic.set(name, idx); - this.bindings.push({ name, declLine: 0, declColumn: 0, kind: 'module', synthetic: true }); - } - return idx; - } - - private def(nameNode: SyntaxNode, acc: FactAccumulator): void { - if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode)); - else acc.addDef(this.resolve(nameNode)); - } - - private use(nameNode: SyntaxNode, acc: FactAccumulator): void { - acc.addUse(this.resolve(nameNode)); - } - - /** Run `fn` with defs demoted to may-defs (conditionally-evaluated context). */ - private conditional(fn: () => void): void { - this.conditionalDepth++; - try { - fn(); - } finally { - this.conditionalDepth--; - } - } - /** Strip parenthesized wrappers around an lvalue (`(x) = 1`). */ private unwrapLvalue(node: SyntaxNode): SyntaxNode { let n = node; diff --git a/gitnexus/src/core/ingestion/cfg/visitors/go-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/go-harvest.ts index a00a7cc18..b409931aa 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/go-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/go-harvest.ts @@ -68,12 +68,7 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js'; import type { BindingEntry, StatementFacts } from '../types.js'; import { CallSiteFactAccumulator } from './call-site-harvest.js'; - -/** - * The per-statement def/use + call-site collector, aliased to the shared - * {@link CallSiteFactAccumulator} (one name for the value and the type). - */ -type FactAccumulator = CallSiteFactAccumulator; +import { ScopeTreeHarvester, type Scope, type FactAccumulator } from './scope-tree-harvest.js'; /** Node types that own a nested CFG — their subtrees are opaque to harvesting. */ const NESTED_FUNCTION_TYPES = new Set([ @@ -100,43 +95,15 @@ const SCOPE_TYPES = new Set([ 'communication_case', ]); -interface Scope { - readonly parent: Scope | null; - /** name → binding index */ - readonly table: Map; -} - -export class GoHarvester { - private readonly bindings: BindingEntry[] = []; - private readonly scopeByNode = new Map(); - private readonly root: Scope = { parent: null, table: new Map() }; - private readonly synthetic = new Map(); - private readonly fnId: number; - /** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */ - private readonly nearestScopeCache = new Map(); - /** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */ - private conditionalDepth = 0; - /** - * Call node id → bindings whose declaration/assignment VALUE is exactly that - * call (#2195 U6). Registered before the value walk, consumed by - * {@link visitCall} (mirrors the TS harvester's `resultDefTargets`). - */ - private readonly resultDefTargets = new Map(); - - constructor(private readonly fnNode: SyntaxNode) { - this.fnId = fnNode.id; - this.scopeByNode.set(fnNode.id, this.root); +export class GoHarvester extends ScopeTreeHarvester { + constructor(fnNode: SyntaxNode) { + super(fnNode); this.declareReceiver(fnNode); this.declareParams(fnNode); const body = this.bodyOf(fnNode); if (body && body.type === 'block') this.prescan(body, this.openScope(body)); } - /** The completed binding table — pass to `CfgBuilder.finish`. */ - bindingTable(): readonly BindingEntry[] { - return this.bindings; - } - /** The function/method/literal body node (always a `block` in Go). */ private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined { return fnNode.childForFieldName('body') ?? undefined; @@ -144,24 +111,8 @@ export class GoHarvester { // ── phase 1: declaration pre-scan ──────────────────────────────────────── - private openScope(node: SyntaxNode): Scope { - const existing = this.scopeByNode.get(node.id); - if (existing) return existing; - const scope: Scope = { parent: this.nearestScopeOf(node), table: new Map() }; - this.scopeByNode.set(node.id, scope); - return scope; - } - - private nearestScopeOf(node: SyntaxNode): Scope { - for (let p = node.parent; p; p = p.parent) { - const s = this.scopeByNode.get(p.id); - if (s) return s; - if (p.id === this.fnId) break; - } - return this.root; - } - - private declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void { + /** Go override: `_` is the blank identifier and binds nothing. */ + protected override declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void { const name = nameNode.text; if (!name || name === '_' || scope.table.has(name)) return; // `_` is the blank identifier scope.table.set(name, this.bindings.length); @@ -201,7 +152,7 @@ export class GoHarvester { } } - private prescan(node: SyntaxNode, scope: Scope): void { + protected prescan(node: SyntaxNode, scope: Scope): void { this.nearestScopeCache.set(node.id, scope); const t = node.type; if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) { @@ -393,57 +344,19 @@ export class GoHarvester { return acc.defCount() ? acc.finish() : undefined; } - private resolve(nameNode: SyntaxNode): number { - const name = nameNode.text; - const cached = this.nearestScopeCache.get(nameNode.id); - let startScope: Scope | null = cached ?? null; - if (!startScope) { - for (let p: SyntaxNode | null = nameNode; p; p = p.parent) { - const scope = this.scopeByNode.get(p.id) ?? this.nearestScopeCache.get(p.id); - if (scope) { - startScope = scope; - break; - } - if (p.id === this.fnId) { - startScope = this.root; - break; - } - } - } - for (let s: Scope | null = startScope; s; s = s.parent) { - const idx = s.table.get(name); - if (idx !== undefined) return idx; - } - let idx = this.synthetic.get(name); - if (idx === undefined) { - idx = this.bindings.length; - this.synthetic.set(name, idx); - this.bindings.push({ name, declLine: 0, declColumn: 0, kind: 'module', synthetic: true }); - } - return idx; - } - - private def(nameNode: SyntaxNode, acc: FactAccumulator): void { + /** Go override: the blank identifier (`_`) defines nothing. */ + protected override def(nameNode: SyntaxNode, acc: FactAccumulator): void { if (nameNode.text === '_') return; // blank identifier defines nothing if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode)); else acc.addDef(this.resolve(nameNode)); } - private use(nameNode: SyntaxNode, acc: FactAccumulator): void { + /** Go override: the blank identifier (`_`) is read of nothing. */ + protected override use(nameNode: SyntaxNode, acc: FactAccumulator): void { if (nameNode.text === '_') return; acc.addUse(this.resolve(nameNode)); } - /** Run `fn` with defs demoted to may-defs (conditionally-evaluated context). */ - private conditional(fn: () => void): void { - this.conditionalDepth++; - try { - fn(); - } finally { - this.conditionalDepth--; - } - } - /** Strip parenthesized wrappers around an lvalue (`(x) = 1`). */ private unwrapLvalue(node: SyntaxNode): SyntaxNode { let n = node; diff --git a/gitnexus/src/core/ingestion/cfg/visitors/java-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/java-harvest.ts index f23dfd769..549d212f7 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/java-harvest.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/java-harvest.ts @@ -44,12 +44,7 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js'; import type { BindingEntry, StatementFacts } from '../types.js'; import { CallSiteFactAccumulator } from './call-site-harvest.js'; - -/** - * The per-statement def/use + call-site collector, aliased to the shared - * {@link CallSiteFactAccumulator} (one name for the value and the type). - */ -type FactAccumulator = CallSiteFactAccumulator; +import { ScopeTreeHarvester, type Scope, type FactAccumulator } from './scope-tree-harvest.js'; /** Node types that own a nested CFG — their subtrees are opaque to harvesting. */ const NESTED_FUNCTION_TYPES = new Set([ @@ -80,42 +75,14 @@ const SCOPE_TYPES = new Set([ /** Comment node types tree-sitter-java surfaces (NOT `comment`). */ const COMMENT_TYPES = new Set(['line_comment', 'block_comment']); -interface Scope { - readonly parent: Scope | null; - /** name → binding index */ - readonly table: Map; -} - -export class JavaHarvester { - private readonly bindings: BindingEntry[] = []; - private readonly scopeByNode = new Map(); - private readonly root: Scope = { parent: null, table: new Map() }; - private readonly synthetic = new Map(); - private readonly fnId: number; - /** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */ - private readonly nearestScopeCache = new Map(); - /** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */ - private conditionalDepth = 0; - /** - * Call/new node id → bindings whose declarator/assignment VALUE is exactly - * that call (#2195 U6). Registered before the value walk, consumed by - * {@link visitCall} (mirrors the TS harvester's `resultDefTargets`). - */ - private readonly resultDefTargets = new Map(); - - constructor(private readonly fnNode: SyntaxNode) { - this.fnId = fnNode.id; - this.scopeByNode.set(fnNode.id, this.root); +export class JavaHarvester extends ScopeTreeHarvester { + constructor(fnNode: SyntaxNode) { + super(fnNode); this.declareParams(fnNode); const body = this.bodyOf(fnNode); if (body && body.type === 'block') this.prescan(body, this.openScope(body)); } - /** The completed binding table — pass to `CfgBuilder.finish`. */ - bindingTable(): readonly BindingEntry[] { - return this.bindings; - } - /** The function/lambda body node (a `block`, or an expression for `x -> expr`). */ private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined { const body = fnNode.childForFieldName('body'); @@ -125,35 +92,6 @@ export class JavaHarvester { // ── phase 1: declaration pre-scan ──────────────────────────────────────── - private openScope(node: SyntaxNode): Scope { - const existing = this.scopeByNode.get(node.id); - if (existing) return existing; - const scope: Scope = { parent: this.nearestScopeOf(node), table: new Map() }; - this.scopeByNode.set(node.id, scope); - return scope; - } - - private nearestScopeOf(node: SyntaxNode): Scope { - for (let p = node.parent; p; p = p.parent) { - const s = this.scopeByNode.get(p.id); - if (s) return s; - if (p.id === this.fnId) break; - } - return this.root; - } - - private declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void { - const name = nameNode.text; - if (!name || scope.table.has(name)) return; - scope.table.set(name, this.bindings.length); - this.bindings.push({ - name, - declLine: nameNode.startPosition.row + 1, - declColumn: nameNode.startPosition.column, - kind, - }); - } - private declareParams(fnNode: SyntaxNode): void { const params = fnNode.childForFieldName('parameters'); if (!params) { @@ -190,7 +128,7 @@ export class JavaHarvester { return declarator?.childForFieldName('name'); } - private prescan(node: SyntaxNode, scope: Scope): void { + protected prescan(node: SyntaxNode, scope: Scope): void { this.nearestScopeCache.set(node.id, scope); const t = node.type; if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) { @@ -315,55 +253,6 @@ export class JavaHarvester { return acc.defCount() ? acc.finish() : undefined; } - private resolve(nameNode: SyntaxNode): number { - const name = nameNode.text; - const cached = this.nearestScopeCache.get(nameNode.id); - let startScope: Scope | null = cached ?? null; - if (!startScope) { - for (let p: SyntaxNode | null = nameNode; p; p = p.parent) { - const scope = this.scopeByNode.get(p.id) ?? this.nearestScopeCache.get(p.id); - if (scope) { - startScope = scope; - break; - } - if (p.id === this.fnId) { - startScope = this.root; - break; - } - } - } - for (let s: Scope | null = startScope; s; s = s.parent) { - const idx = s.table.get(name); - if (idx !== undefined) return idx; - } - let idx = this.synthetic.get(name); - if (idx === undefined) { - idx = this.bindings.length; - this.synthetic.set(name, idx); - this.bindings.push({ name, declLine: 0, declColumn: 0, kind: 'module', synthetic: true }); - } - return idx; - } - - private def(nameNode: SyntaxNode, acc: FactAccumulator): void { - if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode)); - else acc.addDef(this.resolve(nameNode)); - } - - private use(nameNode: SyntaxNode, acc: FactAccumulator): void { - acc.addUse(this.resolve(nameNode)); - } - - /** Run `fn` with defs demoted to may-defs (conditionally-evaluated context). */ - private conditional(fn: () => void): void { - this.conditionalDepth++; - try { - fn(); - } finally { - this.conditionalDepth--; - } - } - /** Strip parenthesized wrappers around an lvalue (`(x) = 1`). */ private unwrapLvalue(node: SyntaxNode): SyntaxNode { let n = node; diff --git a/gitnexus/src/core/ingestion/cfg/visitors/scope-tree-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/scope-tree-harvest.ts new file mode 100644 index 000000000..1822e58ae --- /dev/null +++ b/gitnexus/src/core/ingestion/cfg/visitors/scope-tree-harvest.ts @@ -0,0 +1,171 @@ +/** + * Shared scope-tree substrate for the C-family / Go / Java / C# def/use + * harvesters (#2197 U6, plan KTD4 — a byte-equivalent consolidation). + * + * The Go ({@link import('./go-harvest.js').GoHarvester}), Java ({@link + * import('./java-harvest.js').JavaHarvester}), C# ({@link + * import('./csharp-harvest.js').CsharpHarvester}) and C/C++ ({@link + * import('./c-cpp-harvest.js').CCppHarvester}) harvesters each carried a + * BYTE-IDENTICAL copy of the lexical scope tree machinery: the {@link Scope} + * record, the binding/scope/synthetic state, the two-phase resolution cache, and + * the `openScope` / `nearestScopeOf` / `resolve` / `def` / `use` / `conditional` + * / `bindingTable` methods. This base holds that one copy; the four harvesters + * extend it and supply ONLY their genuine per-language variation — the + * `prescan` switch (abstract) and, for Go, the blank-identifier (`_`) overrides + * of `declare` / `def` / `use`. + * + * TWO-PHASE, ORDER-INDEPENDENT (load-bearing): the CFG walk is NOT source-order + * (`visitFor` builds the init block after the body, `visitDoWhile` the condition + * before the body), so resolving names against a scope stack populated *during* + * the walk would mis-resolve. Phase 1 (`prescan`, per-language) pre-scans the + * whole function subtree once into a completed lexical scope tree; phase 2 + * (`resolve`) resolves defs/uses against that finished tree from any walk order. + * + * Identifiers with no in-function declaration (globals, fields, imports, …) + * resolve to a SYNTHETIC module-level binding (`name@module`), created on first + * reference and applied identically by def and use harvesting. + * + * NOTE: nothing serialized via the harvested bindings/facts may carry a field + * named `nodeId` — the durable parsedfile-store reviver dedups objects keyed on + * that field name. + */ +import type { SyntaxNode } from '../../utils/ast-helpers.js'; +import type { BindingEntry } from '../types.js'; +import { CallSiteFactAccumulator } from './call-site-harvest.js'; + +/** + * The per-statement def/use + call-site collector, aliased to the shared + * {@link CallSiteFactAccumulator} (one name for the value and the type). + */ +export type FactAccumulator = CallSiteFactAccumulator; + +export interface Scope { + readonly parent: Scope | null; + /** name → binding index */ + readonly table: Map; +} + +/** + * Abstract base owning the lexical scope tree + the two-phase resolution + * substrate. Subclasses provide the per-language constructor wiring (param / + * receiver declaration + the body `prescan` kick-off) and the abstract + * `prescan`; Go additionally overrides `declare` / `def` / `use` for its `_` + * blank-identifier semantics. + */ +export abstract class ScopeTreeHarvester { + protected readonly bindings: BindingEntry[] = []; + protected readonly scopeByNode = new Map(); + protected readonly root: Scope = { parent: null, table: new Map() }; + protected readonly synthetic = new Map(); + protected readonly fnId: number; + /** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */ + protected readonly nearestScopeCache = new Map(); + /** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */ + protected conditionalDepth = 0; + /** + * Call/new node id → bindings whose declaration/assignment VALUE is exactly + * that call (#2195 U6). Registered before the value walk, consumed by the + * language harvester's `visitCall` (mirrors the TS harvester's + * `resultDefTargets`). + */ + protected readonly resultDefTargets = new Map(); + + constructor(protected readonly fnNode: SyntaxNode) { + this.fnId = fnNode.id; + this.scopeByNode.set(fnNode.id, this.root); + } + + /** The completed binding table — pass to `CfgBuilder.finish`. */ + bindingTable(): readonly BindingEntry[] { + return this.bindings; + } + + // ── phase 1: declaration pre-scan ──────────────────────────────────────── + + protected openScope(node: SyntaxNode): Scope { + const existing = this.scopeByNode.get(node.id); + if (existing) return existing; + const scope: Scope = { parent: this.nearestScopeOf(node), table: new Map() }; + this.scopeByNode.set(node.id, scope); + return scope; + } + + protected nearestScopeOf(node: SyntaxNode): Scope { + for (let p = node.parent; p; p = p.parent) { + const s = this.scopeByNode.get(p.id); + if (s) return s; + if (p.id === this.fnId) break; + } + return this.root; + } + + protected declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void { + const name = nameNode.text; + if (!name || scope.table.has(name)) return; + scope.table.set(name, this.bindings.length); + this.bindings.push({ + name, + declLine: nameNode.startPosition.row + 1, + declColumn: nameNode.startPosition.column, + kind, + }); + } + + /** + * Phase-1 declaration pre-scan — the only genuine per-language variation (each + * grammar has a distinct declaration-node taxonomy). Walks the function + * subtree once, filling `nearestScopeCache` and the scope tables. + */ + protected abstract prescan(node: SyntaxNode, scope: Scope): void; + + // ── phase 2: per-statement fact extraction ─────────────────────────────── + + protected resolve(nameNode: SyntaxNode): number { + const name = nameNode.text; + const cached = this.nearestScopeCache.get(nameNode.id); + let startScope: Scope | null = cached ?? null; + if (!startScope) { + for (let p: SyntaxNode | null = nameNode; p; p = p.parent) { + const scope = this.scopeByNode.get(p.id) ?? this.nearestScopeCache.get(p.id); + if (scope) { + startScope = scope; + break; + } + if (p.id === this.fnId) { + startScope = this.root; + break; + } + } + } + for (let s: Scope | null = startScope; s; s = s.parent) { + const idx = s.table.get(name); + if (idx !== undefined) return idx; + } + let idx = this.synthetic.get(name); + if (idx === undefined) { + idx = this.bindings.length; + this.synthetic.set(name, idx); + this.bindings.push({ name, declLine: 0, declColumn: 0, kind: 'module', synthetic: true }); + } + return idx; + } + + protected def(nameNode: SyntaxNode, acc: FactAccumulator): void { + if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode)); + else acc.addDef(this.resolve(nameNode)); + } + + protected use(nameNode: SyntaxNode, acc: FactAccumulator): void { + acc.addUse(this.resolve(nameNode)); + } + + /** Run `fn` with defs demoted to may-defs (conditionally-evaluated context). */ + protected conditional(fn: () => void): void { + this.conditionalDepth++; + try { + fn(); + } finally { + this.conditionalDepth--; + } + } +}