@grafema/core 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Orchestrator.d.ts +3 -1
- package/dist/Orchestrator.d.ts.map +1 -1
- package/dist/Orchestrator.js +25 -8
- package/dist/Orchestrator.js.map +1 -1
- package/dist/PhaseRunner.d.ts +4 -5
- package/dist/PhaseRunner.d.ts.map +1 -1
- package/dist/PhaseRunner.js +31 -20
- package/dist/PhaseRunner.js.map +1 -1
- package/dist/plugins/analysis/DatabaseAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/DatabaseAnalyzer.js +3 -4
- package/dist/plugins/analysis/DatabaseAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/ExpressAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/ExpressAnalyzer.js +1 -0
- package/dist/plugins/analysis/ExpressAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/ExpressResponseAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/ExpressResponseAnalyzer.js +11 -34
- package/dist/plugins/analysis/ExpressResponseAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/ExpressRouteAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/ExpressRouteAnalyzer.js +1 -0
- package/dist/plugins/analysis/ExpressRouteAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/NestJSRouteAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/NestJSRouteAnalyzer.js +1 -0
- package/dist/plugins/analysis/NestJSRouteAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/ReactAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/ReactAnalyzer.js +1 -0
- package/dist/plugins/analysis/ReactAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/SQLiteAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/SQLiteAnalyzer.js +7 -9
- package/dist/plugins/analysis/SQLiteAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/ServiceLayerAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/ServiceLayerAnalyzer.js +7 -9
- package/dist/plugins/analysis/ServiceLayerAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/SocketIOAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/SocketIOAnalyzer.js +1 -0
- package/dist/plugins/analysis/SocketIOAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/ast/GraphBuilder.d.ts +18 -8
- package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/GraphBuilder.js +92 -34
- package/dist/plugins/analysis/ast/GraphBuilder.js.map +1 -1
- package/dist/plugins/enrichment/MethodCallResolver.d.ts.map +1 -1
- package/dist/plugins/enrichment/MethodCallResolver.js +8 -3
- package/dist/plugins/enrichment/MethodCallResolver.js.map +1 -1
- package/dist/plugins/enrichment/method-call/MethodCallIndexers.d.ts +22 -0
- package/dist/plugins/enrichment/method-call/MethodCallIndexers.d.ts.map +1 -1
- package/dist/plugins/enrichment/method-call/MethodCallIndexers.js +138 -0
- package/dist/plugins/enrichment/method-call/MethodCallIndexers.js.map +1 -1
- package/dist/plugins/enrichment/method-call/MethodCallResolution.d.ts +11 -1
- package/dist/plugins/enrichment/method-call/MethodCallResolution.d.ts.map +1 -1
- package/dist/plugins/enrichment/method-call/MethodCallResolution.js +39 -1
- package/dist/plugins/enrichment/method-call/MethodCallResolution.js.map +1 -1
- package/dist/storage/backends/RFDBServerBackend.d.ts +9 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
- package/dist/storage/backends/RFDBServerBackend.js +40 -0
- package/dist/storage/backends/RFDBServerBackend.js.map +1 -1
- package/package.json +3 -3
- package/src/Orchestrator.ts +25 -8
- package/src/PhaseRunner.ts +34 -24
- package/src/plugins/analysis/DatabaseAnalyzer.ts +3 -5
- package/src/plugins/analysis/ExpressAnalyzer.ts +1 -0
- package/src/plugins/analysis/ExpressResponseAnalyzer.ts +11 -36
- package/src/plugins/analysis/ExpressRouteAnalyzer.ts +1 -0
- package/src/plugins/analysis/NestJSRouteAnalyzer.ts +1 -0
- package/src/plugins/analysis/ReactAnalyzer.ts +1 -0
- package/src/plugins/analysis/SQLiteAnalyzer.ts +10 -11
- package/src/plugins/analysis/ServiceLayerAnalyzer.ts +7 -9
- package/src/plugins/analysis/SocketIOAnalyzer.ts +1 -0
- package/src/plugins/analysis/ast/GraphBuilder.ts +96 -35
- package/src/plugins/analysis/ast/builders/types.ts +1 -1
- package/src/plugins/enrichment/MethodCallResolver.ts +17 -3
- package/src/plugins/enrichment/method-call/MethodCallIndexers.ts +159 -0
- package/src/plugins/enrichment/method-call/MethodCallResolution.ts +53 -1
- package/src/storage/backends/RFDBServerBackend.ts +39 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GraphBuilder - orchestrator that delegates to domain-specific builders
|
|
3
|
-
*
|
|
3
|
+
* Writes nodes/edges directly to graph during RFDBClient batch window.
|
|
4
|
+
* Only FUNCTION nodes are deferred (pending metadata mutation by ModuleRuntimeBuilder).
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { GraphBackend, NodeRecord } from '@grafema/types';
|
|
@@ -36,8 +37,19 @@ export class GraphBuilder {
|
|
|
36
37
|
// Track singleton nodes to avoid duplicates (net:stdio, net:request, etc.)
|
|
37
38
|
private _createdSingletons: Set<string> = new Set();
|
|
38
39
|
|
|
39
|
-
//
|
|
40
|
-
private
|
|
40
|
+
// Graph reference for direct writes (set during build(), cleared after)
|
|
41
|
+
private _graph: GraphBackend | null = null;
|
|
42
|
+
|
|
43
|
+
// Pending function nodes (deferred until domain builders can mutate metadata)
|
|
44
|
+
private _pendingFunctions: Map<string, GraphNode> = new Map();
|
|
45
|
+
|
|
46
|
+
// Sync batch mode: push directly to RFDBClient batch arrays (no intermediate buffer)
|
|
47
|
+
private _useSyncBatch: boolean = false;
|
|
48
|
+
private _directNodeCount: number = 0;
|
|
49
|
+
private _directEdgeCount: number = 0;
|
|
50
|
+
|
|
51
|
+
// Fallback buffers (when graph.batchNode is not available)
|
|
52
|
+
private _nodeBuffer: unknown[] = [];
|
|
41
53
|
private _edgeBuffer: GraphEdge[] = [];
|
|
42
54
|
|
|
43
55
|
// Domain builders
|
|
@@ -72,7 +84,7 @@ export class GraphBuilder {
|
|
|
72
84
|
bufferEdge: (edge) => this._bufferEdge(edge),
|
|
73
85
|
isCreated: (key) => this._createdSingletons.has(key),
|
|
74
86
|
markCreated: (key) => { this._createdSingletons.add(key); },
|
|
75
|
-
findBufferedNode: (id) => this.
|
|
87
|
+
findBufferedNode: (id) => this._pendingFunctions.get(id),
|
|
76
88
|
findFunctionByName: (functions, name, file, callScopeId) =>
|
|
77
89
|
this.findFunctionByName(functions, name, file, callScopeId),
|
|
78
90
|
resolveVariableInScope: (name, scopePath, file, variables) =>
|
|
@@ -84,46 +96,79 @@ export class GraphBuilder {
|
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
/**
|
|
87
|
-
* Buffer a node for batched writing
|
|
99
|
+
* Buffer a node for batched writing.
|
|
100
|
+
* INVARIANT: Only FUNCTION nodes are deferred (stored in _pendingFunctions)
|
|
101
|
+
* because ModuleRuntimeBuilder mutates their metadata (rejectionPatterns)
|
|
102
|
+
* after buffering. All other nodes go to sync batch or fallback buffer.
|
|
88
103
|
*/
|
|
89
104
|
private _bufferNode(node: GraphNode): void {
|
|
90
|
-
this.
|
|
105
|
+
if (!this._graph) throw new Error('_bufferNode called outside build() — _graph is null');
|
|
106
|
+
const branded = brandNodeInternal(node as unknown as NodeRecord);
|
|
107
|
+
if ((node as Record<string, unknown>).type === 'FUNCTION') {
|
|
108
|
+
this._pendingFunctions.set(node.id, branded as unknown as GraphNode);
|
|
109
|
+
} else if (this._useSyncBatch) {
|
|
110
|
+
this._graph.batchNode!(branded as unknown as Parameters<NonNullable<GraphBackend['batchNode']>>[0]);
|
|
111
|
+
this._directNodeCount++;
|
|
112
|
+
} else {
|
|
113
|
+
this._nodeBuffer.push(branded);
|
|
114
|
+
}
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
/**
|
|
94
|
-
* Buffer an edge for batched writing
|
|
118
|
+
* Buffer an edge for batched writing.
|
|
95
119
|
*/
|
|
96
120
|
private _bufferEdge(edge: GraphEdge): void {
|
|
97
|
-
this.
|
|
121
|
+
if (!this._graph) throw new Error('_bufferEdge called outside build() — _graph is null');
|
|
122
|
+
if (this._useSyncBatch) {
|
|
123
|
+
this._graph.batchEdge!(edge as unknown as Parameters<NonNullable<GraphBackend['batchEdge']>>[0]);
|
|
124
|
+
this._directEdgeCount++;
|
|
125
|
+
} else {
|
|
126
|
+
this._edgeBuffer.push(edge);
|
|
127
|
+
}
|
|
98
128
|
}
|
|
99
129
|
|
|
100
130
|
/**
|
|
101
|
-
* Flush
|
|
131
|
+
* Flush pending function nodes to the graph.
|
|
132
|
+
* In sync batch mode, pushes each to batchNode. In fallback mode, uses addNodes.
|
|
133
|
+
* Returns count of function nodes flushed.
|
|
102
134
|
*/
|
|
103
|
-
private async
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
private async _flushPendingFunctions(graph: GraphBackend): Promise<number> {
|
|
136
|
+
const pendingFunctions = Array.from(this._pendingFunctions.values());
|
|
137
|
+
if (pendingFunctions.length === 0) return 0;
|
|
138
|
+
|
|
139
|
+
// Nodes already branded in _bufferNode() — no need to brand again
|
|
140
|
+
if (this._useSyncBatch) {
|
|
141
|
+
for (const node of pendingFunctions) {
|
|
142
|
+
graph.batchNode!(node as unknown as Parameters<NonNullable<GraphBackend['batchNode']>>[0]);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
await graph.addNodes(pendingFunctions as unknown as Parameters<GraphBackend['addNodes']>[0]);
|
|
111
146
|
}
|
|
112
|
-
|
|
147
|
+
|
|
148
|
+
const count = pendingFunctions.length;
|
|
149
|
+
this._pendingFunctions.clear();
|
|
150
|
+
return count;
|
|
113
151
|
}
|
|
114
152
|
|
|
115
153
|
/**
|
|
116
|
-
* Flush
|
|
117
|
-
* Note: skip_validation=true because nodes were just flushed
|
|
154
|
+
* Flush fallback buffers (only used when sync batch is not available).
|
|
118
155
|
*/
|
|
119
|
-
private async
|
|
156
|
+
private async _flushFallbackBuffers(graph: GraphBackend): Promise<{ nodes: number; edges: number }> {
|
|
157
|
+
let nodesCreated = 0;
|
|
158
|
+
if (this._nodeBuffer.length > 0) {
|
|
159
|
+
await graph.addNodes(this._nodeBuffer as unknown as Parameters<GraphBackend['addNodes']>[0]);
|
|
160
|
+
nodesCreated = this._nodeBuffer.length;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let edgesCreated = 0;
|
|
120
164
|
if (this._edgeBuffer.length > 0) {
|
|
121
|
-
await (graph as GraphBackend & { addEdges(e: GraphEdge[], skip?: boolean): Promise<void> }).addEdges(this._edgeBuffer, true
|
|
122
|
-
|
|
123
|
-
this._edgeBuffer = [];
|
|
124
|
-
return count;
|
|
165
|
+
await (graph as GraphBackend & { addEdges(e: GraphEdge[], skip?: boolean): Promise<void> }).addEdges(this._edgeBuffer, true);
|
|
166
|
+
edgesCreated = this._edgeBuffer.length;
|
|
125
167
|
}
|
|
126
|
-
|
|
168
|
+
|
|
169
|
+
this._nodeBuffer = [];
|
|
170
|
+
this._edgeBuffer = [];
|
|
171
|
+
return { nodes: nodesCreated, edges: edgesCreated };
|
|
127
172
|
}
|
|
128
173
|
|
|
129
174
|
/**
|
|
@@ -150,7 +195,12 @@ export class GraphBuilder {
|
|
|
150
195
|
hasTopLevelAwait = false
|
|
151
196
|
} = data;
|
|
152
197
|
|
|
153
|
-
// Reset
|
|
198
|
+
// Reset state for this build
|
|
199
|
+
this._graph = graph;
|
|
200
|
+
this._pendingFunctions.clear();
|
|
201
|
+
this._useSyncBatch = typeof graph.batchNode === 'function' && typeof graph.batchEdge === 'function';
|
|
202
|
+
this._directNodeCount = 0;
|
|
203
|
+
this._directEdgeCount = 0;
|
|
154
204
|
this._nodeBuffer = [];
|
|
155
205
|
this._edgeBuffer = [];
|
|
156
206
|
|
|
@@ -275,9 +325,20 @@ export class GraphBuilder {
|
|
|
275
325
|
this._typeSystemBuilder.buffer(module, data);
|
|
276
326
|
this._moduleRuntimeBuilder.buffer(module, data);
|
|
277
327
|
|
|
278
|
-
// FLUSH: Write
|
|
279
|
-
const
|
|
280
|
-
|
|
328
|
+
// FLUSH: Write pending function nodes (after domain builders mutated metadata)
|
|
329
|
+
const functionsCount = await this._flushPendingFunctions(graph);
|
|
330
|
+
|
|
331
|
+
// Flush fallback buffers if not using sync batch
|
|
332
|
+
let fallbackNodes = 0;
|
|
333
|
+
let fallbackEdges = 0;
|
|
334
|
+
if (!this._useSyncBatch) {
|
|
335
|
+
const fallback = await this._flushFallbackBuffers(graph);
|
|
336
|
+
fallbackNodes = fallback.nodes;
|
|
337
|
+
fallbackEdges = fallback.edges;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const nodesCreated = this._useSyncBatch ? this._directNodeCount + functionsCount : fallbackNodes + functionsCount;
|
|
341
|
+
const edgesCreated = this._useSyncBatch ? this._directEdgeCount : fallbackEdges;
|
|
281
342
|
|
|
282
343
|
// Handle async operations for ASSIGNED_FROM with CLASS lookups
|
|
283
344
|
const classAssignmentEdges = await this.createClassAssignmentEdges(variableAssignments, graph);
|
|
@@ -289,6 +350,8 @@ export class GraphBuilder {
|
|
|
289
350
|
// REG-297: Update MODULE node with hasTopLevelAwait metadata
|
|
290
351
|
await this.updateModuleTopLevelAwaitMetadata(module, graph, hasTopLevelAwait);
|
|
291
352
|
|
|
353
|
+
this._graph = null; // release reference
|
|
354
|
+
|
|
292
355
|
return { nodes: nodesCreated, edges: edgesCreated + classAssignmentEdges };
|
|
293
356
|
}
|
|
294
357
|
|
|
@@ -528,11 +591,9 @@ export class GraphBuilder {
|
|
|
528
591
|
const file = parts.length >= 3 ? parts[2] : null;
|
|
529
592
|
|
|
530
593
|
let classNode: { id: string; name: string; file?: string } | null = null;
|
|
531
|
-
for await (const node of graph.queryNodes({ type: 'CLASS' })) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
break;
|
|
535
|
-
}
|
|
594
|
+
for await (const node of graph.queryNodes(file ? { type: 'CLASS', name: className, file } : { type: 'CLASS', name: className })) {
|
|
595
|
+
classNode = node as { id: string; name: string; file?: string };
|
|
596
|
+
break;
|
|
536
597
|
}
|
|
537
598
|
|
|
538
599
|
if (classNode) {
|
|
@@ -28,7 +28,7 @@ export interface BuilderContext {
|
|
|
28
28
|
isCreated(singletonKey: string): boolean;
|
|
29
29
|
markCreated(singletonKey: string): void;
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// Pending function node lookup (for metadata updates by ModuleRuntimeBuilder)
|
|
32
32
|
findBufferedNode(id: string): GraphNode | undefined;
|
|
33
33
|
|
|
34
34
|
// Scope-aware variable/parameter resolution (REG-309)
|
|
@@ -27,7 +27,12 @@ import { StrictModeError } from '../../errors/GrafemaError.js';
|
|
|
27
27
|
import { BUILTIN_PROTOTYPE_METHODS } from './method-call/MethodCallData.js';
|
|
28
28
|
import type { MethodCallNode, LibraryCallStats } from './method-call/MethodCallData.js';
|
|
29
29
|
import { isExternalMethod, isBuiltInObject, trackLibraryCall } from './method-call/MethodCallDetectors.js';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
buildClassMethodIndex,
|
|
32
|
+
buildVariableTypeIndex,
|
|
33
|
+
buildInterfaceMethodIndex,
|
|
34
|
+
buildInterfaceImplementationIndex
|
|
35
|
+
} from './method-call/MethodCallIndexers.js';
|
|
31
36
|
import { resolveMethodCall } from './method-call/MethodCallResolution.js';
|
|
32
37
|
import { analyzeResolutionFailure, generateContextualSuggestion } from './method-call/MethodCallErrorAnalysis.js';
|
|
33
38
|
|
|
@@ -45,7 +50,7 @@ export class MethodCallResolver extends Plugin {
|
|
|
45
50
|
edges: ['CALLS']
|
|
46
51
|
},
|
|
47
52
|
dependencies: ['ImportExportLinker'],
|
|
48
|
-
consumes: ['CONTAINS', 'INSTANCE_OF', 'DERIVES_FROM'],
|
|
53
|
+
consumes: ['CONTAINS', 'INSTANCE_OF', 'DERIVES_FROM', 'IMPLEMENTS', 'EXTENDS'],
|
|
49
54
|
produces: ['CALLS']
|
|
50
55
|
};
|
|
51
56
|
}
|
|
@@ -104,6 +109,13 @@ export class MethodCallResolver extends Plugin {
|
|
|
104
109
|
|
|
105
110
|
const variableTypes = await buildVariableTypeIndex(graph, logger);
|
|
106
111
|
|
|
112
|
+
// Build interface indexes for CHA fallback (REG-485)
|
|
113
|
+
const methodToInterfaces = await buildInterfaceMethodIndex(graph, logger);
|
|
114
|
+
logger.info('Indexed interface methods', { methods: methodToInterfaces.size });
|
|
115
|
+
|
|
116
|
+
const interfaceImpls = await buildInterfaceImplementationIndex(graph, logger);
|
|
117
|
+
logger.info('Indexed interface implementations', { interfaces: interfaceImpls.size });
|
|
118
|
+
|
|
107
119
|
// Cache for containing class lookups (local to this execution)
|
|
108
120
|
const containingClassCache = new Map<string, BaseNodeRecord | null>();
|
|
109
121
|
|
|
@@ -162,7 +174,9 @@ export class MethodCallResolver extends Plugin {
|
|
|
162
174
|
classMethodIndex,
|
|
163
175
|
variableTypes,
|
|
164
176
|
graph,
|
|
165
|
-
containingClassCache
|
|
177
|
+
containingClassCache,
|
|
178
|
+
methodToInterfaces,
|
|
179
|
+
interfaceImpls
|
|
166
180
|
);
|
|
167
181
|
|
|
168
182
|
if (targetMethod) {
|
|
@@ -58,6 +58,165 @@ export async function buildClassMethodIndex(
|
|
|
58
58
|
return index;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Builds an index of interface methods for CHA-based resolution (REG-485).
|
|
63
|
+
* Maps method name to the set of interface names that declare (or inherit) that method.
|
|
64
|
+
*
|
|
65
|
+
* Algorithm:
|
|
66
|
+
* 1. Query all INTERFACE nodes and extract method names from properties
|
|
67
|
+
* 2. Track EXTENDS edges between interfaces
|
|
68
|
+
* 3. Flatten inherited methods (walk EXTENDS chain with cycle protection, max depth 10)
|
|
69
|
+
* 4. Build final map: method name -> set of interface names
|
|
70
|
+
*/
|
|
71
|
+
export async function buildInterfaceMethodIndex(
|
|
72
|
+
graph: PluginContext['graph'],
|
|
73
|
+
logger: Logger
|
|
74
|
+
): Promise<Map<string, Set<string>>> {
|
|
75
|
+
const startTime = Date.now();
|
|
76
|
+
const index = new Map<string, Set<string>>();
|
|
77
|
+
|
|
78
|
+
// Collect direct methods per interface and EXTENDS relationships
|
|
79
|
+
const interfaceMethods = new Map<string, Set<string>>();
|
|
80
|
+
const interfaceExtends = new Map<string, string[]>();
|
|
81
|
+
|
|
82
|
+
for await (const node of graph.queryNodes({ nodeType: 'INTERFACE' })) {
|
|
83
|
+
const interfaceName = node.name as string;
|
|
84
|
+
if (!interfaceName) continue;
|
|
85
|
+
|
|
86
|
+
// Parse properties field (may be JSON string or already parsed array)
|
|
87
|
+
let properties: Array<{ name: string; type?: string }> = [];
|
|
88
|
+
if (node.properties) {
|
|
89
|
+
try {
|
|
90
|
+
properties = typeof node.properties === 'string'
|
|
91
|
+
? JSON.parse(node.properties)
|
|
92
|
+
: node.properties as Array<{ name: string; type?: string }>;
|
|
93
|
+
} catch {
|
|
94
|
+
// Skip unparseable properties
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const methods = new Set<string>();
|
|
99
|
+
if (Array.isArray(properties)) {
|
|
100
|
+
for (const prop of properties) {
|
|
101
|
+
if (prop && typeof prop.name === 'string' && prop.name) {
|
|
102
|
+
methods.add(prop.name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
interfaceMethods.set(interfaceName, methods);
|
|
107
|
+
|
|
108
|
+
// Track EXTENDS edges for inheritance flattening
|
|
109
|
+
const extendsEdges = await graph.getOutgoingEdges(node.id, ['EXTENDS']);
|
|
110
|
+
if (extendsEdges.length > 0) {
|
|
111
|
+
const parentNames: string[] = [];
|
|
112
|
+
for (const edge of extendsEdges) {
|
|
113
|
+
const parentNode = await graph.getNode(edge.dst);
|
|
114
|
+
if (parentNode && parentNode.name) {
|
|
115
|
+
parentNames.push(parentNode.name as string);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (parentNames.length > 0) {
|
|
119
|
+
interfaceExtends.set(interfaceName, parentNames);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Flatten inherited methods with cycle protection
|
|
125
|
+
const resolvedMethods = new Map<string, Set<string>>();
|
|
126
|
+
|
|
127
|
+
function collectMethods(name: string, visited: Set<string>, depth: number): Set<string> {
|
|
128
|
+
if (depth > 10 || visited.has(name)) return new Set();
|
|
129
|
+
if (resolvedMethods.has(name)) return resolvedMethods.get(name)!;
|
|
130
|
+
|
|
131
|
+
visited.add(name);
|
|
132
|
+
const ownMethods = interfaceMethods.get(name) || new Set<string>();
|
|
133
|
+
const allMethods = new Set(ownMethods);
|
|
134
|
+
|
|
135
|
+
const parents = interfaceExtends.get(name);
|
|
136
|
+
if (parents) {
|
|
137
|
+
for (const parent of parents) {
|
|
138
|
+
const parentMethods = collectMethods(parent, visited, depth + 1);
|
|
139
|
+
for (const m of parentMethods) {
|
|
140
|
+
allMethods.add(m);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
resolvedMethods.set(name, allMethods);
|
|
146
|
+
return allMethods;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const name of interfaceMethods.keys()) {
|
|
150
|
+
collectMethods(name, new Set(), 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Build final index: method name -> set of interface names
|
|
154
|
+
for (const [interfaceName, methods] of resolvedMethods) {
|
|
155
|
+
for (const methodName of methods) {
|
|
156
|
+
let interfaces = index.get(methodName);
|
|
157
|
+
if (!interfaces) {
|
|
158
|
+
interfaces = new Set();
|
|
159
|
+
index.set(methodName, interfaces);
|
|
160
|
+
}
|
|
161
|
+
interfaces.add(interfaceName);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
166
|
+
logger.debug('Built interface method index', {
|
|
167
|
+
interfaces: interfaceMethods.size,
|
|
168
|
+
methods: index.size,
|
|
169
|
+
time: `${elapsed}s`
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return index;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Builds an index of interface implementations for CHA-based resolution (REG-485).
|
|
177
|
+
* Maps interface name to the set of class names that implement it.
|
|
178
|
+
*
|
|
179
|
+
* Algorithm:
|
|
180
|
+
* 1. Query all CLASS nodes
|
|
181
|
+
* 2. For each class, get outgoing IMPLEMENTS edges
|
|
182
|
+
* 3. For each IMPLEMENTS edge, get the target INTERFACE node name
|
|
183
|
+
* 4. Accumulate: interface name -> Set of class names
|
|
184
|
+
*/
|
|
185
|
+
export async function buildInterfaceImplementationIndex(
|
|
186
|
+
graph: PluginContext['graph'],
|
|
187
|
+
logger: Logger
|
|
188
|
+
): Promise<Map<string, Set<string>>> {
|
|
189
|
+
const startTime = Date.now();
|
|
190
|
+
const index = new Map<string, Set<string>>();
|
|
191
|
+
|
|
192
|
+
for await (const classNode of graph.queryNodes({ nodeType: 'CLASS' })) {
|
|
193
|
+
const className = classNode.name as string;
|
|
194
|
+
if (!className) continue;
|
|
195
|
+
|
|
196
|
+
const implementsEdges = await graph.getOutgoingEdges(classNode.id, ['IMPLEMENTS']);
|
|
197
|
+
for (const edge of implementsEdges) {
|
|
198
|
+
const interfaceNode = await graph.getNode(edge.dst);
|
|
199
|
+
if (!interfaceNode || !interfaceNode.name) continue;
|
|
200
|
+
|
|
201
|
+
const interfaceName = interfaceNode.name as string;
|
|
202
|
+
let classes = index.get(interfaceName);
|
|
203
|
+
if (!classes) {
|
|
204
|
+
classes = new Set();
|
|
205
|
+
index.set(interfaceName, classes);
|
|
206
|
+
}
|
|
207
|
+
classes.add(className);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
212
|
+
logger.debug('Built interface implementation index', {
|
|
213
|
+
interfaces: index.size,
|
|
214
|
+
time: `${elapsed}s`
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return index;
|
|
218
|
+
}
|
|
219
|
+
|
|
61
220
|
/**
|
|
62
221
|
* Builds an index of variables and their types (from INSTANCE_OF edges).
|
|
63
222
|
*/
|
|
@@ -18,13 +18,16 @@ import type { MethodCallNode, ClassEntry } from './MethodCallData.js';
|
|
|
18
18
|
* 2. Local class in same file
|
|
19
19
|
* 3. "this" reference to containing class
|
|
20
20
|
* 4. Variable type index (INSTANCE_OF)
|
|
21
|
+
* 5. Interface-aware CHA fallback (REG-485)
|
|
21
22
|
*/
|
|
22
23
|
export async function resolveMethodCall(
|
|
23
24
|
methodCall: MethodCallNode,
|
|
24
25
|
classMethodIndex: Map<string, ClassEntry>,
|
|
25
26
|
variableTypes: Map<string, string>,
|
|
26
27
|
graph: PluginContext['graph'],
|
|
27
|
-
containingClassCache: Map<string, BaseNodeRecord | null
|
|
28
|
+
containingClassCache: Map<string, BaseNodeRecord | null>,
|
|
29
|
+
methodToInterfaces?: Map<string, Set<string>>,
|
|
30
|
+
interfaceImpls?: Map<string, Set<string>>
|
|
28
31
|
): Promise<BaseNodeRecord | null> {
|
|
29
32
|
const { object, method, file } = methodCall;
|
|
30
33
|
|
|
@@ -88,6 +91,55 @@ export async function resolveMethodCall(
|
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
// 5. Interface-aware CHA fallback (REG-485)
|
|
95
|
+
if (methodToInterfaces && interfaceImpls) {
|
|
96
|
+
const chaResult = await resolveViaInterfaceCHA(
|
|
97
|
+
method, classMethodIndex, methodToInterfaces, interfaceImpls, graph
|
|
98
|
+
);
|
|
99
|
+
if (chaResult) return chaResult;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Step 5: Interface-aware CHA fallback (REG-485).
|
|
107
|
+
*
|
|
108
|
+
* When steps 1-4 fail, look up method name in interface method index,
|
|
109
|
+
* find implementing classes, and resolve to their method definition.
|
|
110
|
+
* Also checks parent classes via DERIVES_FROM for inherited methods.
|
|
111
|
+
* This enables resolution for calls on variables typed as interfaces.
|
|
112
|
+
*/
|
|
113
|
+
export async function resolveViaInterfaceCHA(
|
|
114
|
+
methodName: string,
|
|
115
|
+
classMethodIndex: Map<string, ClassEntry>,
|
|
116
|
+
methodToInterfaces: Map<string, Set<string>>,
|
|
117
|
+
interfaceImpls: Map<string, Set<string>>,
|
|
118
|
+
graph: PluginContext['graph']
|
|
119
|
+
): Promise<BaseNodeRecord | null> {
|
|
120
|
+
const candidateInterfaces = methodToInterfaces.get(methodName);
|
|
121
|
+
if (!candidateInterfaces || candidateInterfaces.size === 0) return null;
|
|
122
|
+
|
|
123
|
+
for (const interfaceName of candidateInterfaces) {
|
|
124
|
+
const implementingClasses = interfaceImpls.get(interfaceName);
|
|
125
|
+
if (!implementingClasses) continue;
|
|
126
|
+
|
|
127
|
+
for (const className of implementingClasses) {
|
|
128
|
+
const classEntry = classMethodIndex.get(className);
|
|
129
|
+
if (!classEntry) continue;
|
|
130
|
+
|
|
131
|
+
if (classEntry.methods.has(methodName)) {
|
|
132
|
+
return classEntry.methods.get(methodName)!;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check parent classes via DERIVES_FROM chain
|
|
136
|
+
const inherited = await findMethodInParentClasses(
|
|
137
|
+
classEntry.classNode, methodName, classMethodIndex, graph
|
|
138
|
+
);
|
|
139
|
+
if (inherited) return inherited;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
91
143
|
return null;
|
|
92
144
|
}
|
|
93
145
|
|
|
@@ -775,6 +775,45 @@ export class RFDBServerBackend {
|
|
|
775
775
|
return this.client.commitBatch(tags);
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Synchronously batch a node. Must be inside beginBatch/commitBatch.
|
|
780
|
+
* Bypasses async wrapper for direct batch insertion.
|
|
781
|
+
*/
|
|
782
|
+
batchNode(node: InputNode): void {
|
|
783
|
+
if (!this.client) throw new Error('Not connected');
|
|
784
|
+
const { id, type, nodeType, node_type, name, file, exported, ...rest } = node;
|
|
785
|
+
const useV3 = this.protocolVersion >= 3;
|
|
786
|
+
const wire: Record<string, unknown> = {
|
|
787
|
+
id: String(id),
|
|
788
|
+
nodeType: (nodeType || node_type || type || 'UNKNOWN'),
|
|
789
|
+
name: name || '',
|
|
790
|
+
file: file || '',
|
|
791
|
+
exported: exported || false,
|
|
792
|
+
metadata: useV3 ? JSON.stringify(rest) : JSON.stringify({ originalId: String(id), ...rest }),
|
|
793
|
+
};
|
|
794
|
+
if (useV3) {
|
|
795
|
+
wire.semanticId = String(id);
|
|
796
|
+
}
|
|
797
|
+
this.client.batchNode(wire as Parameters<typeof this.client.batchNode>[0]);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Synchronously batch an edge. Must be inside beginBatch/commitBatch.
|
|
802
|
+
*/
|
|
803
|
+
batchEdge(edge: InputEdge): void {
|
|
804
|
+
if (!this.client) throw new Error('Not connected');
|
|
805
|
+
const { src, dst, type, edgeType, edge_type, etype, metadata, ...rest } = edge;
|
|
806
|
+
const edgeTypeStr = edgeType || edge_type || (etype as string) || type;
|
|
807
|
+
if (typeof edgeTypeStr === 'string') this.edgeTypes.add(edgeTypeStr);
|
|
808
|
+
const flatMetadata = { ...rest, ...(typeof metadata === 'object' && metadata !== null ? metadata as Record<string, unknown> : {}) };
|
|
809
|
+
this.client.batchEdge({
|
|
810
|
+
src: String(src),
|
|
811
|
+
dst: String(dst),
|
|
812
|
+
edgeType: (edgeTypeStr || 'UNKNOWN'),
|
|
813
|
+
metadata: JSON.stringify(flatMetadata),
|
|
814
|
+
} as Record<string, unknown>);
|
|
815
|
+
}
|
|
816
|
+
|
|
778
817
|
/**
|
|
779
818
|
* Abort the current batch, discarding all buffered data.
|
|
780
819
|
*/
|