mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
parent
7192036183
commit
7ebd579524
5 changed files with 197 additions and 448 deletions
|
|
@ -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<string, number>;
|
||||
}
|
||||
|
||||
export class CCppHarvester {
|
||||
private readonly bindings: BindingEntry[] = [];
|
||||
private readonly scopeByNode = new Map<number, Scope>();
|
||||
private readonly root: Scope = { parent: null, table: new Map() };
|
||||
private readonly synthetic = new Map<string, number>();
|
||||
private readonly fnId: number;
|
||||
/** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */
|
||||
private readonly nearestScopeCache = new Map<number, Scope>();
|
||||
/** >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<number, number[]>();
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<string, number>;
|
||||
}
|
||||
|
||||
export class CsharpHarvester {
|
||||
private readonly bindings: BindingEntry[] = [];
|
||||
private readonly scopeByNode = new Map<number, Scope>();
|
||||
private readonly root: Scope = { parent: null, table: new Map() };
|
||||
private readonly synthetic = new Map<string, number>();
|
||||
private readonly fnId: number;
|
||||
/** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */
|
||||
private readonly nearestScopeCache = new Map<number, Scope>();
|
||||
/** >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<number, number[]>();
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<string, number>;
|
||||
}
|
||||
|
||||
export class GoHarvester {
|
||||
private readonly bindings: BindingEntry[] = [];
|
||||
private readonly scopeByNode = new Map<number, Scope>();
|
||||
private readonly root: Scope = { parent: null, table: new Map() };
|
||||
private readonly synthetic = new Map<string, number>();
|
||||
private readonly fnId: number;
|
||||
/** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */
|
||||
private readonly nearestScopeCache = new Map<number, Scope>();
|
||||
/** >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<number, number[]>();
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<string, number>;
|
||||
}
|
||||
|
||||
export class JavaHarvester {
|
||||
private readonly bindings: BindingEntry[] = [];
|
||||
private readonly scopeByNode = new Map<number, Scope>();
|
||||
private readonly root: Scope = { parent: null, table: new Map() };
|
||||
private readonly synthetic = new Map<string, number>();
|
||||
private readonly fnId: number;
|
||||
/** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */
|
||||
private readonly nearestScopeCache = new Map<number, Scope>();
|
||||
/** >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<number, number[]>();
|
||||
|
||||
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;
|
||||
|
|
|
|||
171
gitnexus/src/core/ingestion/cfg/visitors/scope-tree-harvest.ts
Normal file
171
gitnexus/src/core/ingestion/cfg/visitors/scope-tree-harvest.ts
Normal file
|
|
@ -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<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<number, Scope>();
|
||||
protected readonly root: Scope = { parent: null, table: new Map() };
|
||||
protected readonly synthetic = new Map<string, number>();
|
||||
protected readonly fnId: number;
|
||||
/** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */
|
||||
protected readonly nearestScopeCache = new Map<number, Scope>();
|
||||
/** >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<number, number[]>();
|
||||
|
||||
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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue