code-graph-context 2.0.0 → 2.2.0
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/README.md +156 -2
- package/dist/constants.js +167 -0
- package/dist/core/config/fairsquare-framework-schema.js +9 -7
- package/dist/core/config/nestjs-framework-schema.js +60 -43
- package/dist/core/config/schema.js +41 -2
- package/dist/core/embeddings/natural-language-to-cypher.service.js +166 -110
- package/dist/core/parsers/typescript-parser.js +1043 -747
- package/dist/core/parsers/workspace-parser.js +177 -194
- package/dist/core/utils/code-normalizer.js +299 -0
- package/dist/core/utils/file-change-detection.js +17 -2
- package/dist/core/utils/file-utils.js +40 -5
- package/dist/core/utils/graph-factory.js +161 -0
- package/dist/core/utils/shared-utils.js +79 -0
- package/dist/core/workspace/workspace-detector.js +59 -5
- package/dist/mcp/constants.js +141 -8
- package/dist/mcp/handlers/graph-generator.handler.js +1 -0
- package/dist/mcp/handlers/incremental-parse.handler.js +3 -6
- package/dist/mcp/handlers/parallel-import.handler.js +136 -0
- package/dist/mcp/handlers/streaming-import.handler.js +14 -59
- package/dist/mcp/mcp.server.js +1 -1
- package/dist/mcp/services/job-manager.js +5 -8
- package/dist/mcp/services/watch-manager.js +7 -18
- package/dist/mcp/tools/detect-dead-code.tool.js +413 -0
- package/dist/mcp/tools/detect-duplicate-code.tool.js +450 -0
- package/dist/mcp/tools/impact-analysis.tool.js +20 -4
- package/dist/mcp/tools/index.js +4 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +15 -14
- package/dist/mcp/workers/chunk-worker-pool.js +196 -0
- package/dist/mcp/workers/chunk-worker.types.js +4 -0
- package/dist/mcp/workers/chunk.worker.js +89 -0
- package/dist/mcp/workers/parse-coordinator.js +183 -0
- package/dist/mcp/workers/worker.pool.js +54 -0
- package/dist/storage/neo4j/neo4j.service.js +190 -10
- package/package.json +1 -1
|
@@ -1,36 +1,18 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import crypto from 'crypto';
|
|
3
2
|
import fs from 'fs/promises';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import { glob } from 'glob';
|
|
6
5
|
import { minimatch } from 'minimatch';
|
|
7
6
|
import { Project, Node } from 'ts-morph';
|
|
8
|
-
import {
|
|
9
|
-
/**
|
|
10
|
-
* Generate a deterministic node ID based on stable properties.
|
|
11
|
-
* This ensures the same node gets the same ID across reparses.
|
|
12
|
-
*
|
|
13
|
-
* Identity is based on: projectId + coreType + filePath + name (+ parentId for nested nodes)
|
|
14
|
-
* This is stable because when it matters (one side of edge not reparsed),
|
|
15
|
-
* names are guaranteed unchanged (or imports would break, triggering reparse).
|
|
16
|
-
*
|
|
17
|
-
* Including projectId ensures nodes from different projects have unique IDs
|
|
18
|
-
* even if they have identical file paths and names.
|
|
19
|
-
*/
|
|
20
|
-
const generateDeterministicId = (projectId, coreType, filePath, name, parentId) => {
|
|
21
|
-
const parts = parentId ? [projectId, coreType, filePath, parentId, name] : [projectId, coreType, filePath, name];
|
|
22
|
-
const identity = parts.join('::');
|
|
23
|
-
const hash = crypto.createHash('sha256').update(identity).digest('hex').substring(0, 16);
|
|
24
|
-
return `${projectId}:${coreType}:${hash}`;
|
|
25
|
-
};
|
|
26
|
-
import { debugLog, hashFile } from '../utils/file-utils.js';
|
|
7
|
+
import { EXCLUDE_PATTERNS_GLOB, BUILT_IN_FUNCTIONS, BUILT_IN_METHODS, BUILT_IN_CLASSES } from '../../constants.js';
|
|
27
8
|
import { NESTJS_FRAMEWORK_SCHEMA } from '../config/nestjs-framework-schema.js';
|
|
28
9
|
import { CoreNodeType, CORE_TYPESCRIPT_SCHEMA, DEFAULT_PARSE_OPTIONS, CoreEdgeType, } from '../config/schema.js';
|
|
10
|
+
import { normalizeCode } from '../utils/code-normalizer.js';
|
|
11
|
+
import { debugLog, hashFile, matchesPattern, cleanTypeName } from '../utils/file-utils.js';
|
|
12
|
+
import { createFrameworkEdgeData, createCoreEdge as createCoreEdgeFactory, createCallsEdge as createCallsEdgeFactory, toNeo4jNode, toNeo4jEdge, toParsedEdge, generateDeterministicId, } from '../utils/graph-factory.js';
|
|
29
13
|
import { resolveProjectId } from '../utils/project-id.js';
|
|
30
14
|
export class TypeScriptParser {
|
|
31
|
-
|
|
32
|
-
tsConfigPath;
|
|
33
|
-
project;
|
|
15
|
+
project; // initialized in constructor, undefined in resolver-only mode
|
|
34
16
|
coreSchema;
|
|
35
17
|
parseConfig;
|
|
36
18
|
frameworkSchemas;
|
|
@@ -43,17 +25,50 @@ export class TypeScriptParser {
|
|
|
43
25
|
lazyLoad; // Whether to use lazy file loading for large projects
|
|
44
26
|
discoveredFiles = null; // Cached file discovery results
|
|
45
27
|
deferEdgeEnhancements = false; // When true, skip edge enhancements (parent will handle)
|
|
28
|
+
// Lookup indexes for efficient CALLS edge resolution
|
|
29
|
+
methodsByClass = new Map(); // className -> methodName -> node
|
|
30
|
+
functionsByName = new Map(); // functionName -> node
|
|
31
|
+
constructorsByClass = new Map(); // className -> constructor node
|
|
32
|
+
// Track already exported items to avoid returning duplicates in streaming mode
|
|
33
|
+
exportedNodeIds = new Set();
|
|
34
|
+
exportedEdgeIds = new Set();
|
|
35
|
+
workspacePath;
|
|
36
|
+
/**
|
|
37
|
+
* Create a resolver-only instance for edge resolution without parsing.
|
|
38
|
+
* This mode doesn't initialize ts-morph and can only resolve edges from externally-added nodes.
|
|
39
|
+
*/
|
|
40
|
+
static createResolver(projectId) {
|
|
41
|
+
const instance = Object.create(TypeScriptParser.prototype);
|
|
42
|
+
// project is intentionally not initialized - resolver mode doesn't need ts-morph
|
|
43
|
+
instance.coreSchema = CORE_TYPESCRIPT_SCHEMA;
|
|
44
|
+
instance.parseConfig = DEFAULT_PARSE_OPTIONS;
|
|
45
|
+
instance.frameworkSchemas = [];
|
|
46
|
+
instance.parsedNodes = new Map();
|
|
47
|
+
instance.parsedEdges = new Map();
|
|
48
|
+
instance.existingNodes = new Map();
|
|
49
|
+
instance.deferredEdges = [];
|
|
50
|
+
instance.sharedContext = new Map();
|
|
51
|
+
instance.projectId = projectId;
|
|
52
|
+
instance.lazyLoad = true;
|
|
53
|
+
instance.discoveredFiles = null;
|
|
54
|
+
instance.deferEdgeEnhancements = false;
|
|
55
|
+
instance.methodsByClass = new Map();
|
|
56
|
+
instance.functionsByName = new Map();
|
|
57
|
+
instance.constructorsByClass = new Map();
|
|
58
|
+
instance.exportedNodeIds = new Set();
|
|
59
|
+
instance.exportedEdgeIds = new Set();
|
|
60
|
+
instance.workspacePath = '';
|
|
61
|
+
return instance;
|
|
62
|
+
}
|
|
46
63
|
constructor(workspacePath, tsConfigPath = 'tsconfig.json', coreSchema = CORE_TYPESCRIPT_SCHEMA, frameworkSchemas = [NESTJS_FRAMEWORK_SCHEMA], parseConfig = DEFAULT_PARSE_OPTIONS, projectId, // Optional - derived from workspacePath if not provided
|
|
47
64
|
lazyLoad = false) {
|
|
48
65
|
this.workspacePath = workspacePath;
|
|
49
|
-
this.tsConfigPath = tsConfigPath;
|
|
50
66
|
this.coreSchema = coreSchema;
|
|
51
67
|
this.frameworkSchemas = frameworkSchemas;
|
|
52
68
|
this.parseConfig = parseConfig;
|
|
53
69
|
this.projectId = resolveProjectId(workspacePath, projectId);
|
|
54
70
|
this.lazyLoad = lazyLoad;
|
|
55
|
-
|
|
56
|
-
console.log(`📂 Lazy loading: ${lazyLoad ? 'enabled' : 'disabled'}`);
|
|
71
|
+
debugLog('Parser initialized', { projectId: this.projectId, lazyLoad });
|
|
57
72
|
if (lazyLoad) {
|
|
58
73
|
// Lazy mode: create Project without loading any files
|
|
59
74
|
// Files will be added just-in-time during parseChunk()
|
|
@@ -97,6 +112,35 @@ export class TypeScriptParser {
|
|
|
97
112
|
getProjectId() {
|
|
98
113
|
return this.projectId;
|
|
99
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Get all parsed nodes (for cross-parser edge resolution).
|
|
117
|
+
* Returns the internal Map of ParsedNodes.
|
|
118
|
+
*/
|
|
119
|
+
getParsedNodes() {
|
|
120
|
+
return this.parsedNodes;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get the framework schemas used by this parser.
|
|
124
|
+
* Useful for WorkspaceParser to apply cross-package edge enhancements.
|
|
125
|
+
*/
|
|
126
|
+
getFrameworkSchemas() {
|
|
127
|
+
return this.frameworkSchemas;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get the shared context from this parser.
|
|
131
|
+
* Useful for aggregating context across multiple parsers.
|
|
132
|
+
*/
|
|
133
|
+
getSharedContext() {
|
|
134
|
+
return this.sharedContext;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Set the shared context for this parser.
|
|
138
|
+
* Use this to share context across multiple parsers (e.g., in WorkspaceParser).
|
|
139
|
+
* @param context The shared context map to use
|
|
140
|
+
*/
|
|
141
|
+
setSharedContext(context) {
|
|
142
|
+
this.sharedContext = context;
|
|
143
|
+
}
|
|
100
144
|
/**
|
|
101
145
|
* Set existing nodes from Neo4j for edge target matching during incremental parsing.
|
|
102
146
|
* These nodes will be available as targets for edge detection but won't be exported.
|
|
@@ -122,63 +166,204 @@ export class TypeScriptParser {
|
|
|
122
166
|
};
|
|
123
167
|
this.existingNodes.set(node.id, parsedNode);
|
|
124
168
|
}
|
|
125
|
-
|
|
169
|
+
debugLog('Loaded existing nodes for edge detection', { count: nodes.length });
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Defer edge enhancements to a parent parser (e.g., WorkspaceParser).
|
|
173
|
+
* When true, parseChunk() will skip applyEdgeEnhancements().
|
|
174
|
+
* The parent is responsible for calling applyEdgeEnhancementsManually() at the end.
|
|
175
|
+
*/
|
|
176
|
+
setDeferEdgeEnhancements(defer) {
|
|
177
|
+
this.deferEdgeEnhancements = defer;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Load framework schemas for a specific project type.
|
|
181
|
+
* No-op for TypeScriptParser since schemas are loaded in constructor.
|
|
182
|
+
*/
|
|
183
|
+
loadFrameworkSchemasForType(_projectType) {
|
|
184
|
+
// TypeScriptParser already has schemas loaded via constructor/ParserFactory
|
|
126
185
|
}
|
|
127
186
|
async parseWorkspace(filesToParse) {
|
|
128
187
|
let sourceFiles;
|
|
188
|
+
// Determine which files to parse
|
|
189
|
+
let filePaths;
|
|
129
190
|
if (filesToParse && filesToParse.length > 0) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return existing;
|
|
136
|
-
// Add file to project if not already loaded (lazy mode)
|
|
137
|
-
try {
|
|
138
|
-
return this.project.addSourceFileAtPath(filePath);
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
})
|
|
144
|
-
.filter((sf) => sf !== undefined);
|
|
191
|
+
filePaths = filesToParse;
|
|
192
|
+
}
|
|
193
|
+
else if (this.lazyLoad) {
|
|
194
|
+
// In lazy mode, use glob-based discovery (consistent with detectChangedFiles)
|
|
195
|
+
filePaths = await this.discoverSourceFiles();
|
|
145
196
|
}
|
|
146
197
|
else {
|
|
198
|
+
// Eager mode - files already loaded from tsconfig
|
|
147
199
|
sourceFiles = this.project.getSourceFiles();
|
|
200
|
+
for (const sourceFile of sourceFiles) {
|
|
201
|
+
if (this.shouldSkipFile(sourceFile))
|
|
202
|
+
continue;
|
|
203
|
+
await this.parseCoreTypeScript(sourceFile);
|
|
204
|
+
}
|
|
205
|
+
return this.finishParsing();
|
|
148
206
|
}
|
|
207
|
+
// Load files into project (for lazy mode or explicit file list)
|
|
208
|
+
sourceFiles = filePaths
|
|
209
|
+
.map((filePath) => {
|
|
210
|
+
const existing = this.project.getSourceFile(filePath);
|
|
211
|
+
if (existing)
|
|
212
|
+
return existing;
|
|
213
|
+
try {
|
|
214
|
+
return this.project.addSourceFileAtPath(filePath);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
.filter((sf) => sf !== undefined);
|
|
149
221
|
for (const sourceFile of sourceFiles) {
|
|
150
222
|
if (this.shouldSkipFile(sourceFile))
|
|
151
223
|
continue;
|
|
152
|
-
await this.
|
|
153
|
-
}
|
|
154
|
-
await this.resolveDeferredEdges();
|
|
155
|
-
await this.applyContextExtractors();
|
|
156
|
-
if (this.frameworkSchemas.length > 0) {
|
|
157
|
-
await this.applyFrameworkEnhancements();
|
|
224
|
+
await this.parseCoreTypeScript(sourceFile);
|
|
158
225
|
}
|
|
159
|
-
|
|
160
|
-
const neo4jNodes = Array.from(this.parsedNodes.values()).map(this.toNeo4jNode);
|
|
161
|
-
const neo4jEdges = Array.from(this.parsedEdges.values()).map(this.toNeo4jEdge);
|
|
162
|
-
return { nodes: neo4jNodes, edges: neo4jEdges };
|
|
226
|
+
return this.finishParsing();
|
|
163
227
|
}
|
|
164
228
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
229
|
+
* Parse a chunk of files without resolving deferred edges.
|
|
230
|
+
* Use this for streaming parsing where edges are resolved after all chunks.
|
|
231
|
+
* In lazy mode, files are added to the project just-in-time and removed after parsing.
|
|
232
|
+
* @param filePaths Specific file paths to parse
|
|
233
|
+
* @param skipEdgeResolution If true, deferred edges are not resolved (default: false)
|
|
167
234
|
*/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
235
|
+
async parseChunk(filePaths, skipEdgeResolution = false) {
|
|
236
|
+
// Declare sourceFiles outside try so it's available in finally
|
|
237
|
+
const sourceFiles = [];
|
|
238
|
+
try {
|
|
239
|
+
if (this.lazyLoad) {
|
|
240
|
+
// Lazy mode: add files to project just-in-time
|
|
241
|
+
for (const filePath of filePaths) {
|
|
242
|
+
try {
|
|
243
|
+
// Check if file already exists in project (shouldn't happen in lazy mode)
|
|
244
|
+
// Add the file to the project if not already present
|
|
245
|
+
const sourceFile = this.project.getSourceFile(filePath) ?? this.project.addSourceFileAtPath(filePath);
|
|
246
|
+
sourceFiles.push(sourceFile);
|
|
175
247
|
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
console.warn(`Failed to add source file ${filePath}:`, error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Eager mode: files are already loaded
|
|
255
|
+
const loadedFiles = filePaths
|
|
256
|
+
.map((filePath) => this.project.getSourceFile(filePath))
|
|
257
|
+
.filter((sf) => sf !== undefined);
|
|
258
|
+
sourceFiles.push(...loadedFiles);
|
|
259
|
+
}
|
|
260
|
+
for (const sourceFile of sourceFiles) {
|
|
261
|
+
if (this.shouldSkipFile(sourceFile))
|
|
262
|
+
continue;
|
|
263
|
+
await this.parseCoreTypeScript(sourceFile);
|
|
264
|
+
}
|
|
265
|
+
// Only resolve edges if not skipping
|
|
266
|
+
if (!skipEdgeResolution) {
|
|
267
|
+
await this.resolveDeferredEdges();
|
|
268
|
+
}
|
|
269
|
+
await this.applyContextExtractors();
|
|
270
|
+
if (this.frameworkSchemas.length > 0) {
|
|
271
|
+
await this.applyFrameworkEnhancements();
|
|
272
|
+
}
|
|
273
|
+
// Apply edge enhancements unless deferred to parent (e.g., WorkspaceParser)
|
|
274
|
+
// When deferred, parent will call applyEdgeEnhancementsManually() at the end
|
|
275
|
+
// with all accumulated nodes for cross-package edge detection
|
|
276
|
+
if (!this.deferEdgeEnhancements) {
|
|
277
|
+
await this.applyEdgeEnhancements();
|
|
278
|
+
}
|
|
279
|
+
// Only return nodes/edges that haven't been exported yet (prevents duplicate imports in streaming mode)
|
|
280
|
+
const newNodes = [];
|
|
281
|
+
const newEdges = [];
|
|
282
|
+
for (const node of this.parsedNodes.values()) {
|
|
283
|
+
if (!this.exportedNodeIds.has(node.id)) {
|
|
284
|
+
newNodes.push(toNeo4jNode(node));
|
|
285
|
+
this.exportedNodeIds.add(node.id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const edge of this.parsedEdges.values()) {
|
|
289
|
+
if (!this.exportedEdgeIds.has(edge.id)) {
|
|
290
|
+
newEdges.push(toNeo4jEdge(edge));
|
|
291
|
+
this.exportedEdgeIds.add(edge.id);
|
|
176
292
|
}
|
|
177
293
|
}
|
|
294
|
+
return { nodes: newNodes, edges: newEdges };
|
|
178
295
|
}
|
|
179
|
-
|
|
296
|
+
finally {
|
|
297
|
+
// Always clean up in lazy mode to prevent memory leaks
|
|
298
|
+
if (this.lazyLoad) {
|
|
299
|
+
for (const sourceFile of sourceFiles) {
|
|
300
|
+
try {
|
|
301
|
+
this.project.removeSourceFile(sourceFile);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// Ignore errors when removing files
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get list of source files in the project.
|
|
312
|
+
* In lazy mode, uses glob to discover files without loading them into memory.
|
|
313
|
+
* Useful for determining total work and creating chunks.
|
|
314
|
+
*/
|
|
315
|
+
async discoverSourceFiles() {
|
|
316
|
+
if (this.discoveredFiles !== null) {
|
|
317
|
+
return this.discoveredFiles;
|
|
318
|
+
}
|
|
319
|
+
if (this.lazyLoad) {
|
|
320
|
+
// Use glob to find files without loading them into ts-morph
|
|
321
|
+
// Use EXCLUDE_PATTERNS_GLOB for consistency with detectChangedFiles
|
|
322
|
+
const pattern = path.join(this.workspacePath, '**/*.{ts,tsx}');
|
|
323
|
+
this.discoveredFiles = await glob(pattern, {
|
|
324
|
+
ignore: EXCLUDE_PATTERNS_GLOB,
|
|
325
|
+
absolute: true,
|
|
326
|
+
});
|
|
327
|
+
debugLog('Discovered TypeScript files (lazy mode)', { count: this.discoveredFiles.length });
|
|
328
|
+
return this.discoveredFiles;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Eager mode - files are already loaded
|
|
332
|
+
this.discoveredFiles = this.project
|
|
333
|
+
.getSourceFiles()
|
|
334
|
+
.filter((sf) => !this.shouldSkipFile(sf))
|
|
335
|
+
.map((sf) => sf.getFilePath());
|
|
336
|
+
return this.discoveredFiles;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* @deprecated Use discoverSourceFiles() instead for async file discovery
|
|
341
|
+
*/
|
|
342
|
+
getSourceFilePaths() {
|
|
343
|
+
if (this.lazyLoad) {
|
|
344
|
+
throw new Error('getSourceFilePaths() is not supported in lazy mode. Use discoverSourceFiles() instead.');
|
|
345
|
+
}
|
|
346
|
+
return this.project
|
|
347
|
+
.getSourceFiles()
|
|
348
|
+
.filter((sf) => !this.shouldSkipFile(sf))
|
|
349
|
+
.map((sf) => sf.getFilePath());
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Complete parsing by resolving edges and applying enhancements.
|
|
353
|
+
* Called after all source files have been parsed.
|
|
354
|
+
*/
|
|
355
|
+
async finishParsing() {
|
|
356
|
+
await this.resolveDeferredEdges();
|
|
357
|
+
await this.applyContextExtractors();
|
|
358
|
+
if (this.frameworkSchemas.length > 0) {
|
|
359
|
+
await this.applyFrameworkEnhancements();
|
|
360
|
+
}
|
|
361
|
+
await this.applyEdgeEnhancements();
|
|
362
|
+
const neo4jNodes = Array.from(this.parsedNodes.values()).map(toNeo4jNode);
|
|
363
|
+
const neo4jEdges = Array.from(this.parsedEdges.values()).map(toNeo4jEdge);
|
|
364
|
+
return { nodes: neo4jNodes, edges: neo4jEdges };
|
|
180
365
|
}
|
|
181
|
-
async
|
|
366
|
+
async parseCoreTypeScript(sourceFile) {
|
|
182
367
|
const filePath = sourceFile.getFilePath();
|
|
183
368
|
const stats = await fs.stat(filePath);
|
|
184
369
|
const fileTrackingProperties = {
|
|
@@ -221,16 +406,17 @@ export class TypeScriptParser {
|
|
|
221
406
|
});
|
|
222
407
|
}
|
|
223
408
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
409
|
+
for (const varStatement of sourceFile.getVariableStatements()) {
|
|
410
|
+
const isExported = varStatement.isExported();
|
|
411
|
+
if (!isExported && !this.shouldParseVariables(sourceFile.getFilePath()))
|
|
412
|
+
continue;
|
|
413
|
+
for (const varDecl of varStatement.getDeclarations()) {
|
|
414
|
+
if (this.shouldSkipChildNode(varDecl))
|
|
415
|
+
continue;
|
|
416
|
+
const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, { isExported }, sourceFileNode.id);
|
|
417
|
+
this.addNode(variableNode);
|
|
418
|
+
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
|
|
419
|
+
this.addEdge(containsEdge);
|
|
234
420
|
}
|
|
235
421
|
}
|
|
236
422
|
}
|
|
@@ -257,7 +443,15 @@ export class TypeScriptParser {
|
|
|
257
443
|
for (const child of children) {
|
|
258
444
|
if (this.shouldSkipChildNode(child))
|
|
259
445
|
continue;
|
|
260
|
-
|
|
446
|
+
// Track parent class name for methods, properties, and constructors
|
|
447
|
+
const extraProperties = {};
|
|
448
|
+
if (parentNode.coreType === CoreNodeType.CLASS_DECLARATION &&
|
|
449
|
+
(type === CoreNodeType.METHOD_DECLARATION ||
|
|
450
|
+
type === CoreNodeType.PROPERTY_DECLARATION ||
|
|
451
|
+
type === CoreNodeType.CONSTRUCTOR_DECLARATION)) {
|
|
452
|
+
extraProperties.parentClassName = parentNode.properties.name;
|
|
453
|
+
}
|
|
454
|
+
const coreNode = this.createCoreNode(child, type, extraProperties, parentNode.id);
|
|
261
455
|
this.addNode(coreNode);
|
|
262
456
|
const coreEdge = this.createCoreEdge(edgeType, parentNode.id, coreNode.id);
|
|
263
457
|
this.addEdge(coreEdge);
|
|
@@ -269,6 +463,15 @@ export class TypeScriptParser {
|
|
|
269
463
|
if (SKELETONIZE_TYPES.has(type)) {
|
|
270
464
|
this.skeletonizeChildInParent(parentNode, coreNode);
|
|
271
465
|
}
|
|
466
|
+
// Extract CALLS edges from method/function/constructor bodies
|
|
467
|
+
const CALL_EXTRACTION_TYPES = new Set([
|
|
468
|
+
CoreNodeType.METHOD_DECLARATION,
|
|
469
|
+
CoreNodeType.FUNCTION_DECLARATION,
|
|
470
|
+
CoreNodeType.CONSTRUCTOR_DECLARATION,
|
|
471
|
+
]);
|
|
472
|
+
if (CALL_EXTRACTION_TYPES.has(type)) {
|
|
473
|
+
this.extractCallsFromBody(coreNode, child);
|
|
474
|
+
}
|
|
272
475
|
const childNodeConfig = this.coreSchema.nodeTypes[type];
|
|
273
476
|
if (childNodeConfig) {
|
|
274
477
|
this.queueRelationshipNodes(childNodeConfig, coreNode, child);
|
|
@@ -286,87 +489,542 @@ export class TypeScriptParser {
|
|
|
286
489
|
parent.properties.sourceCode = parent.properties.sourceCode.replace(childText, placeholder);
|
|
287
490
|
}
|
|
288
491
|
}
|
|
289
|
-
|
|
290
|
-
* Queue relationship edges for deferred processing
|
|
291
|
-
* These are resolved after all nodes are parsed since the target may not exist yet
|
|
292
|
-
*/
|
|
293
|
-
queueRelationshipNodes(nodeConfig, parsedNode, astNode) {
|
|
294
|
-
if (!nodeConfig.relationships || nodeConfig.relationships.length === 0)
|
|
295
|
-
return;
|
|
296
|
-
for (const relationship of nodeConfig.relationships) {
|
|
297
|
-
const { edgeType, method, cardinality, targetNodeType } = relationship;
|
|
298
|
-
const astGetter = astNode[method];
|
|
299
|
-
if (typeof astGetter !== 'function')
|
|
300
|
-
continue;
|
|
301
|
-
const result = astGetter.call(astNode);
|
|
302
|
-
if (!result)
|
|
303
|
-
continue;
|
|
304
|
-
const targets = cardinality === 'single' ? [result] : result;
|
|
305
|
-
for (const target of targets) {
|
|
306
|
-
if (!target)
|
|
307
|
-
continue;
|
|
308
|
-
const targetName = this.extractRelationshipTargetName(target);
|
|
309
|
-
if (!targetName)
|
|
310
|
-
continue;
|
|
311
|
-
// For EXTENDS/IMPLEMENTS, try to get the file path from the resolved declaration
|
|
312
|
-
let targetFilePath;
|
|
313
|
-
if (edgeType === CoreEdgeType.EXTENDS || edgeType === CoreEdgeType.IMPLEMENTS) {
|
|
314
|
-
targetFilePath = this.extractTargetFilePath(target);
|
|
315
|
-
}
|
|
316
|
-
this.deferredEdges.push({
|
|
317
|
-
edgeType: edgeType,
|
|
318
|
-
sourceNodeId: parsedNode.id,
|
|
319
|
-
targetName,
|
|
320
|
-
targetType: targetNodeType,
|
|
321
|
-
targetFilePath,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Extract the file path from a resolved target declaration.
|
|
328
|
-
* Used for EXTENDS/IMPLEMENTS to enable precise matching.
|
|
329
|
-
*/
|
|
330
|
-
extractTargetFilePath(target) {
|
|
492
|
+
extractNodeName(astNode, coreType) {
|
|
331
493
|
try {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
494
|
+
switch (coreType) {
|
|
495
|
+
case CoreNodeType.SOURCE_FILE:
|
|
496
|
+
if (Node.isSourceFile(astNode)) {
|
|
497
|
+
return astNode.getBaseName();
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
case CoreNodeType.CLASS_DECLARATION:
|
|
501
|
+
if (Node.isClassDeclaration(astNode)) {
|
|
502
|
+
return astNode.getName() ?? 'AnonymousClass';
|
|
503
|
+
}
|
|
504
|
+
break;
|
|
505
|
+
case CoreNodeType.METHOD_DECLARATION:
|
|
506
|
+
if (Node.isMethodDeclaration(astNode)) {
|
|
507
|
+
return astNode.getName();
|
|
508
|
+
}
|
|
509
|
+
break;
|
|
510
|
+
case CoreNodeType.FUNCTION_DECLARATION:
|
|
511
|
+
if (Node.isFunctionDeclaration(astNode)) {
|
|
512
|
+
return astNode.getName() ?? 'AnonymousFunction';
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
case CoreNodeType.INTERFACE_DECLARATION:
|
|
516
|
+
if (Node.isInterfaceDeclaration(astNode)) {
|
|
517
|
+
return astNode.getName();
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
case CoreNodeType.PROPERTY_DECLARATION:
|
|
521
|
+
if (Node.isPropertyDeclaration(astNode)) {
|
|
522
|
+
return astNode.getName();
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
case CoreNodeType.PARAMETER_DECLARATION:
|
|
526
|
+
if (Node.isParameterDeclaration(astNode)) {
|
|
527
|
+
return astNode.getName();
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
case CoreNodeType.IMPORT_DECLARATION:
|
|
531
|
+
if (Node.isImportDeclaration(astNode)) {
|
|
532
|
+
return astNode.getModuleSpecifierValue();
|
|
533
|
+
}
|
|
534
|
+
break;
|
|
535
|
+
case CoreNodeType.DECORATOR:
|
|
536
|
+
if (Node.isDecorator(astNode)) {
|
|
537
|
+
return astNode.getName();
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
case CoreNodeType.TYPE_ALIAS:
|
|
541
|
+
if (Node.isTypeAliasDeclaration(astNode)) {
|
|
542
|
+
return astNode.getName();
|
|
543
|
+
}
|
|
544
|
+
break;
|
|
545
|
+
default:
|
|
546
|
+
return astNode.getKindName();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
console.warn(`Error extracting name for ${coreType}:`, error);
|
|
551
|
+
}
|
|
552
|
+
return 'Unknown';
|
|
553
|
+
}
|
|
554
|
+
extractProperty(astNode, propDef) {
|
|
555
|
+
const { method, source, defaultValue } = propDef.extraction;
|
|
556
|
+
try {
|
|
557
|
+
switch (method) {
|
|
558
|
+
case 'ast':
|
|
559
|
+
if (typeof source === 'string') {
|
|
560
|
+
const fn = astNode[source];
|
|
561
|
+
return typeof fn === 'function' ? fn.call(astNode) : defaultValue;
|
|
562
|
+
}
|
|
563
|
+
return defaultValue;
|
|
564
|
+
case 'function':
|
|
565
|
+
if (typeof source === 'function') {
|
|
566
|
+
return source(astNode);
|
|
567
|
+
}
|
|
568
|
+
return defaultValue;
|
|
569
|
+
case 'static':
|
|
570
|
+
return defaultValue;
|
|
571
|
+
case 'context':
|
|
572
|
+
// Context properties are handled by context extractors
|
|
573
|
+
return undefined;
|
|
574
|
+
default:
|
|
575
|
+
return defaultValue;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
console.warn(`Failed to extract property ${propDef.name}:`, error);
|
|
580
|
+
return defaultValue;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Extract the target name from an AST node returned by relationship methods
|
|
585
|
+
*/
|
|
586
|
+
extractRelationshipTargetName(target) {
|
|
587
|
+
if (Node.isClassDeclaration(target))
|
|
588
|
+
return target.getName();
|
|
589
|
+
if (Node.isInterfaceDeclaration(target))
|
|
590
|
+
return target.getName();
|
|
591
|
+
if (Node.isExpressionWithTypeArguments(target)) {
|
|
592
|
+
const expression = target.getExpression();
|
|
593
|
+
const text = expression.getText();
|
|
594
|
+
const genericIndex = text.indexOf('<');
|
|
595
|
+
return genericIndex > 0 ? text.substring(0, genericIndex) : text;
|
|
596
|
+
}
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Extract the file path from a resolved target declaration.
|
|
601
|
+
* Used for EXTENDS/IMPLEMENTS to enable precise matching.
|
|
602
|
+
*/
|
|
603
|
+
extractTargetFilePath(target) {
|
|
604
|
+
try {
|
|
605
|
+
// If target is already a ClassDeclaration or InterfaceDeclaration, get its source file
|
|
606
|
+
if (Node.isClassDeclaration(target) || Node.isInterfaceDeclaration(target)) {
|
|
607
|
+
return target.getSourceFile().getFilePath();
|
|
608
|
+
}
|
|
609
|
+
// If target is ExpressionWithTypeArguments (e.g., extends Foo<T>), resolve the type
|
|
610
|
+
if (Node.isExpressionWithTypeArguments(target)) {
|
|
611
|
+
const expression = target.getExpression();
|
|
612
|
+
if (Node.isIdentifier(expression)) {
|
|
613
|
+
// Try to get the definition of the type
|
|
614
|
+
const definitions = expression.getDefinitionNodes();
|
|
615
|
+
for (const def of definitions) {
|
|
616
|
+
if (Node.isClassDeclaration(def) || Node.isInterfaceDeclaration(def)) {
|
|
617
|
+
return def.getSourceFile().getFilePath();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
349
622
|
}
|
|
350
623
|
catch {
|
|
351
624
|
// If resolution fails (e.g., external type), return undefined
|
|
352
625
|
}
|
|
353
|
-
return undefined;
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Extract method/function calls from the body of a method or function.
|
|
630
|
+
* Creates deferred CALLS edges for resolution after all nodes are parsed.
|
|
631
|
+
*/
|
|
632
|
+
extractCallsFromBody(callerNode, astNode) {
|
|
633
|
+
// Get the body of the method/function
|
|
634
|
+
let body;
|
|
635
|
+
if (Node.isMethodDeclaration(astNode)) {
|
|
636
|
+
body = astNode.getBody();
|
|
637
|
+
}
|
|
638
|
+
else if (Node.isFunctionDeclaration(astNode)) {
|
|
639
|
+
body = astNode.getBody();
|
|
640
|
+
}
|
|
641
|
+
else if (Node.isConstructorDeclaration(astNode)) {
|
|
642
|
+
body = astNode.getBody();
|
|
643
|
+
}
|
|
644
|
+
if (!body)
|
|
645
|
+
return;
|
|
646
|
+
// Skip very short method bodies (likely simple getters/setters)
|
|
647
|
+
const bodyText = body.getText();
|
|
648
|
+
if (bodyText.length < 15)
|
|
649
|
+
return;
|
|
650
|
+
// Track unique calls to avoid duplicates (same method called multiple times)
|
|
651
|
+
const seenCalls = new Set();
|
|
652
|
+
// Traverse all descendants looking for call expressions
|
|
653
|
+
body.forEachDescendant((descendant) => {
|
|
654
|
+
// Method/function calls: foo(), this.foo(), obj.foo()
|
|
655
|
+
if (Node.isCallExpression(descendant)) {
|
|
656
|
+
const callInfo = this.extractCallInfo(descendant, callerNode);
|
|
657
|
+
if (callInfo) {
|
|
658
|
+
const callKey = `${callInfo.targetName}:${callInfo.receiverType ?? 'unknown'}`;
|
|
659
|
+
if (!seenCalls.has(callKey)) {
|
|
660
|
+
seenCalls.add(callKey);
|
|
661
|
+
this.queueCallEdge(callerNode.id, callInfo);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Constructor calls: new ClassName()
|
|
666
|
+
if (Node.isNewExpression(descendant)) {
|
|
667
|
+
const callInfo = this.extractConstructorCallInfo(descendant);
|
|
668
|
+
if (callInfo) {
|
|
669
|
+
const callKey = `constructor:${callInfo.targetClassName}`;
|
|
670
|
+
if (!seenCalls.has(callKey)) {
|
|
671
|
+
seenCalls.add(callKey);
|
|
672
|
+
this.queueCallEdge(callerNode.id, callInfo);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Extract call information from a CallExpression.
|
|
680
|
+
*/
|
|
681
|
+
extractCallInfo(callExpr, callerNode) {
|
|
682
|
+
if (!Node.isCallExpression(callExpr))
|
|
683
|
+
return null;
|
|
684
|
+
const expression = callExpr.getExpression();
|
|
685
|
+
const lineNumber = callExpr.getStartLineNumber();
|
|
686
|
+
const argumentCount = callExpr.getArguments().length;
|
|
687
|
+
// Check if this call is awaited
|
|
688
|
+
const parent = callExpr.getParent();
|
|
689
|
+
const isAsync = parent !== undefined && Node.isAwaitExpression(parent);
|
|
690
|
+
// Case 1: Direct function call - functionName()
|
|
691
|
+
if (Node.isIdentifier(expression)) {
|
|
692
|
+
const targetName = expression.getText();
|
|
693
|
+
// Skip built-in functions and common utilities
|
|
694
|
+
if (BUILT_IN_FUNCTIONS.has(targetName))
|
|
695
|
+
return null;
|
|
696
|
+
return {
|
|
697
|
+
targetName,
|
|
698
|
+
targetType: CoreNodeType.FUNCTION_DECLARATION,
|
|
699
|
+
lineNumber,
|
|
700
|
+
isAsync,
|
|
701
|
+
argumentCount,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
// Case 2: Method call - obj.method() or this.method() or this.service.method()
|
|
705
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
706
|
+
const methodName = expression.getName();
|
|
707
|
+
const receiver = expression.getExpression();
|
|
708
|
+
// Skip common built-in method calls
|
|
709
|
+
if (BUILT_IN_METHODS.has(methodName))
|
|
710
|
+
return null;
|
|
711
|
+
// this.method() - internal class method call
|
|
712
|
+
if (Node.isThisExpression(receiver)) {
|
|
713
|
+
return {
|
|
714
|
+
targetName: methodName,
|
|
715
|
+
targetType: CoreNodeType.METHOD_DECLARATION,
|
|
716
|
+
receiverExpression: 'this',
|
|
717
|
+
lineNumber,
|
|
718
|
+
isAsync,
|
|
719
|
+
argumentCount,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
// this.service.method() - dependency injection call
|
|
723
|
+
if (Node.isPropertyAccessExpression(receiver)) {
|
|
724
|
+
const innerReceiver = receiver.getExpression();
|
|
725
|
+
if (Node.isThisExpression(innerReceiver)) {
|
|
726
|
+
const propertyName = receiver.getName();
|
|
727
|
+
// Try to resolve the type from constructor parameters
|
|
728
|
+
const receiverType = this.resolvePropertyType(callerNode, propertyName);
|
|
729
|
+
return {
|
|
730
|
+
targetName: methodName,
|
|
731
|
+
targetType: CoreNodeType.METHOD_DECLARATION,
|
|
732
|
+
receiverExpression: `this.${propertyName}`,
|
|
733
|
+
receiverPropertyName: propertyName,
|
|
734
|
+
receiverType,
|
|
735
|
+
lineNumber,
|
|
736
|
+
isAsync,
|
|
737
|
+
argumentCount,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// variable.method() - method call on local variable
|
|
742
|
+
if (Node.isIdentifier(receiver)) {
|
|
743
|
+
const varName = receiver.getText();
|
|
744
|
+
// Try to get the type of the variable
|
|
745
|
+
let receiverType;
|
|
746
|
+
try {
|
|
747
|
+
const typeText = receiver.getType().getText();
|
|
748
|
+
// Clean up type (remove generics, imports)
|
|
749
|
+
receiverType = cleanTypeName(typeText);
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
// Type resolution failed
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
targetName: methodName,
|
|
756
|
+
targetType: CoreNodeType.METHOD_DECLARATION,
|
|
757
|
+
receiverExpression: varName,
|
|
758
|
+
receiverType,
|
|
759
|
+
lineNumber,
|
|
760
|
+
isAsync,
|
|
761
|
+
argumentCount,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Extract constructor call information from a NewExpression.
|
|
769
|
+
*/
|
|
770
|
+
extractConstructorCallInfo(newExpr) {
|
|
771
|
+
if (!Node.isNewExpression(newExpr))
|
|
772
|
+
return null;
|
|
773
|
+
const expression = newExpr.getExpression();
|
|
774
|
+
const lineNumber = newExpr.getStartLineNumber();
|
|
775
|
+
const argumentCount = newExpr.getArguments().length;
|
|
776
|
+
// new ClassName()
|
|
777
|
+
if (Node.isIdentifier(expression)) {
|
|
778
|
+
const className = expression.getText();
|
|
779
|
+
// Skip built-in constructors
|
|
780
|
+
if (BUILT_IN_CLASSES.has(className))
|
|
781
|
+
return null;
|
|
782
|
+
return {
|
|
783
|
+
targetName: 'constructor',
|
|
784
|
+
targetType: CoreNodeType.CONSTRUCTOR_DECLARATION,
|
|
785
|
+
targetClassName: className,
|
|
786
|
+
lineNumber,
|
|
787
|
+
isAsync: false,
|
|
788
|
+
argumentCount,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Queue a CALLS edge for deferred resolution.
|
|
795
|
+
*/
|
|
796
|
+
queueCallEdge(sourceNodeId, callInfo) {
|
|
797
|
+
// For constructor calls, use class name as target
|
|
798
|
+
const targetName = callInfo.targetClassName ?? callInfo.targetName;
|
|
799
|
+
this.deferredEdges.push({
|
|
800
|
+
edgeType: CoreEdgeType.CALLS,
|
|
801
|
+
sourceNodeId,
|
|
802
|
+
targetName,
|
|
803
|
+
targetType: callInfo.targetType,
|
|
804
|
+
callContext: {
|
|
805
|
+
receiverExpression: callInfo.receiverExpression,
|
|
806
|
+
receiverType: callInfo.receiverType,
|
|
807
|
+
receiverPropertyName: callInfo.receiverPropertyName,
|
|
808
|
+
lineNumber: callInfo.lineNumber,
|
|
809
|
+
isAsync: callInfo.isAsync,
|
|
810
|
+
argumentCount: callInfo.argumentCount,
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Resolve the type of a class property from constructor parameters.
|
|
816
|
+
* Used for NestJS dependency injection pattern.
|
|
817
|
+
*/
|
|
818
|
+
resolvePropertyType(node, propertyName) {
|
|
819
|
+
// Look for constructor parameter types in the node's context
|
|
820
|
+
const context = node.properties.context;
|
|
821
|
+
if (context?.constructorParamTypes) {
|
|
822
|
+
const paramTypes = context.constructorParamTypes;
|
|
823
|
+
// Constructor params with 'private' or 'public' become properties
|
|
824
|
+
// Try to find a matching parameter by name
|
|
825
|
+
// The context extractor stores types in order, we need to match by name
|
|
826
|
+
// For now, use a heuristic: look for type that matches property name
|
|
827
|
+
for (const paramType of paramTypes) {
|
|
828
|
+
// Check if the type name matches (case-insensitive, removing 'Service', 'Repository' etc.)
|
|
829
|
+
const normalizedProp = propertyName.toLowerCase().replace(/service|repository|provider/gi, '');
|
|
830
|
+
const normalizedType = paramType.toLowerCase().replace(/service|repository|provider/gi, '');
|
|
831
|
+
if (normalizedType.includes(normalizedProp) || normalizedProp.includes(normalizedType)) {
|
|
832
|
+
return paramType;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// Also check the class node's parent for constructor info
|
|
837
|
+
const classNode = this.findParentClassNode(node);
|
|
838
|
+
if (classNode?.properties.context?.constructorParamTypes) {
|
|
839
|
+
const paramTypes = classNode.properties.context.constructorParamTypes;
|
|
840
|
+
for (const paramType of paramTypes) {
|
|
841
|
+
const normalizedProp = propertyName.toLowerCase().replace(/service|repository|provider/gi, '');
|
|
842
|
+
const normalizedType = paramType.toLowerCase().replace(/service|repository|provider/gi, '');
|
|
843
|
+
if (normalizedType.includes(normalizedProp) || normalizedProp.includes(normalizedType)) {
|
|
844
|
+
return paramType;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
createCoreNode(astNode, coreType, baseProperties = {}, parentId) {
|
|
851
|
+
const name = this.extractNodeName(astNode, coreType);
|
|
852
|
+
const filePath = astNode.getSourceFile().getFilePath();
|
|
853
|
+
const nodeId = generateDeterministicId(this.projectId, coreType, filePath, name, parentId);
|
|
854
|
+
// Extract base properties using schema
|
|
855
|
+
const properties = {
|
|
856
|
+
id: nodeId,
|
|
857
|
+
projectId: this.projectId,
|
|
858
|
+
name,
|
|
859
|
+
coreType,
|
|
860
|
+
filePath,
|
|
861
|
+
startLine: astNode.getStartLineNumber(),
|
|
862
|
+
endLine: astNode.getEndLineNumber(),
|
|
863
|
+
sourceCode: astNode.getText(),
|
|
864
|
+
createdAt: new Date().toISOString(),
|
|
865
|
+
...baseProperties,
|
|
866
|
+
};
|
|
867
|
+
// Extract schema-defined properties
|
|
868
|
+
const coreNodeDef = this.coreSchema.nodeTypes[coreType];
|
|
869
|
+
if (coreNodeDef) {
|
|
870
|
+
for (const propDef of coreNodeDef.properties) {
|
|
871
|
+
try {
|
|
872
|
+
const value = this.extractProperty(astNode, propDef);
|
|
873
|
+
if (value !== undefined && propDef.name !== 'context') {
|
|
874
|
+
properties[propDef.name] = value;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
console.warn(`Failed to extract core property ${propDef.name}:`, error);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// Compute normalizedHash for duplicate detection (methods, functions, constructors)
|
|
883
|
+
if (coreType === CoreNodeType.METHOD_DECLARATION ||
|
|
884
|
+
coreType === CoreNodeType.FUNCTION_DECLARATION ||
|
|
885
|
+
coreType === CoreNodeType.CONSTRUCTOR_DECLARATION) {
|
|
886
|
+
try {
|
|
887
|
+
const { normalizedHash } = normalizeCode(properties.sourceCode);
|
|
888
|
+
if (normalizedHash) {
|
|
889
|
+
properties.normalizedHash = normalizedHash;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
console.warn(`Failed to compute normalizedHash for ${nodeId}:`, error);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
id: nodeId,
|
|
898
|
+
coreType,
|
|
899
|
+
labels: [...(coreNodeDef?.neo4j.labels || [])],
|
|
900
|
+
properties,
|
|
901
|
+
sourceNode: astNode,
|
|
902
|
+
skipEmbedding: coreNodeDef?.neo4j.skipEmbedding ?? false,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
createCoreEdge(relationshipType, sourceNodeId, targetNodeId) {
|
|
906
|
+
return toParsedEdge(createCoreEdgeFactory({
|
|
907
|
+
edgeType: relationshipType,
|
|
908
|
+
sourceNodeId,
|
|
909
|
+
targetNodeId,
|
|
910
|
+
projectId: this.projectId,
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
createCallsEdge(sourceNodeId, targetNodeId, callContext) {
|
|
914
|
+
return toParsedEdge(createCallsEdgeFactory({
|
|
915
|
+
sourceNodeId,
|
|
916
|
+
targetNodeId,
|
|
917
|
+
projectId: this.projectId,
|
|
918
|
+
callContext,
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
createFrameworkEdge(semanticType, relationshipType, sourceNodeId, targetNodeId, context = {}, relationshipWeight = 0.5) {
|
|
922
|
+
const { id, properties } = createFrameworkEdgeData({
|
|
923
|
+
semanticType,
|
|
924
|
+
sourceNodeId,
|
|
925
|
+
targetNodeId,
|
|
926
|
+
projectId: this.projectId,
|
|
927
|
+
context,
|
|
928
|
+
relationshipWeight,
|
|
929
|
+
});
|
|
930
|
+
return {
|
|
931
|
+
id,
|
|
932
|
+
relationshipType,
|
|
933
|
+
sourceNodeId,
|
|
934
|
+
targetNodeId,
|
|
935
|
+
properties,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Queue relationship edges for deferred processing
|
|
940
|
+
* These are resolved after all nodes are parsed since the target may not exist yet
|
|
941
|
+
*/
|
|
942
|
+
queueRelationshipNodes(nodeConfig, parsedNode, astNode) {
|
|
943
|
+
if (!nodeConfig.relationships || nodeConfig.relationships.length === 0)
|
|
944
|
+
return;
|
|
945
|
+
for (const relationship of nodeConfig.relationships) {
|
|
946
|
+
const { edgeType, method, cardinality, targetNodeType } = relationship;
|
|
947
|
+
const astGetter = astNode[method];
|
|
948
|
+
if (typeof astGetter !== 'function')
|
|
949
|
+
continue;
|
|
950
|
+
const result = astGetter.call(astNode);
|
|
951
|
+
if (!result)
|
|
952
|
+
continue;
|
|
953
|
+
const targets = cardinality === 'single' ? [result] : result;
|
|
954
|
+
for (const target of targets) {
|
|
955
|
+
if (!target)
|
|
956
|
+
continue;
|
|
957
|
+
const targetName = this.extractRelationshipTargetName(target);
|
|
958
|
+
if (!targetName)
|
|
959
|
+
continue;
|
|
960
|
+
// For EXTENDS/IMPLEMENTS, try to get the file path from the resolved declaration
|
|
961
|
+
let targetFilePath;
|
|
962
|
+
if (edgeType === CoreEdgeType.EXTENDS || edgeType === CoreEdgeType.IMPLEMENTS) {
|
|
963
|
+
targetFilePath = this.extractTargetFilePath(target);
|
|
964
|
+
}
|
|
965
|
+
this.deferredEdges.push({
|
|
966
|
+
edgeType: edgeType,
|
|
967
|
+
sourceNodeId: parsedNode.id,
|
|
968
|
+
targetName,
|
|
969
|
+
targetType: targetNodeType,
|
|
970
|
+
targetFilePath,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}
|
|
354
974
|
}
|
|
355
975
|
/**
|
|
356
|
-
*
|
|
976
|
+
* Resolve the target node for a CALLS edge.
|
|
977
|
+
* Uses special resolution logic based on call context.
|
|
357
978
|
*/
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
979
|
+
resolveCallTarget(deferred) {
|
|
980
|
+
const { targetName, targetType, sourceNodeId, callContext } = deferred;
|
|
981
|
+
// Case 1: this.method() - internal class method call
|
|
982
|
+
if (callContext?.receiverExpression === 'this') {
|
|
983
|
+
// Find the caller's class, then look for method in same class
|
|
984
|
+
const sourceNode = this.parsedNodes.get(sourceNodeId) ?? this.existingNodes.get(sourceNodeId);
|
|
985
|
+
if (sourceNode) {
|
|
986
|
+
const className = this.getClassNameForNode(sourceNode);
|
|
987
|
+
if (className && this.methodsByClass.has(className)) {
|
|
988
|
+
const method = this.methodsByClass.get(className).get(targetName);
|
|
989
|
+
if (method)
|
|
990
|
+
return method;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
368
993
|
}
|
|
369
|
-
|
|
994
|
+
// Case 2: this.service.method() - dependency injection call
|
|
995
|
+
if (callContext?.receiverType) {
|
|
996
|
+
// Look for method in the receiver's class
|
|
997
|
+
const className = callContext.receiverType;
|
|
998
|
+
if (this.methodsByClass.has(className)) {
|
|
999
|
+
const method = this.methodsByClass.get(className).get(targetName);
|
|
1000
|
+
if (method)
|
|
1001
|
+
return method;
|
|
1002
|
+
}
|
|
1003
|
+
// Fallback: search all nodes for a method with this name in a class matching the type
|
|
1004
|
+
for (const [, node] of this.parsedNodes) {
|
|
1005
|
+
if (node.coreType === CoreNodeType.METHOD_DECLARATION && node.properties.name === targetName) {
|
|
1006
|
+
// Check if parent class matches the receiver type
|
|
1007
|
+
const parentClass = this.findParentClassNode(node);
|
|
1008
|
+
if (parentClass?.properties.name === className) {
|
|
1009
|
+
return node;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// Case 3: Constructor call - find the constructor in the target class
|
|
1015
|
+
if (targetType === CoreNodeType.CONSTRUCTOR_DECLARATION) {
|
|
1016
|
+
const constructor = this.constructorsByClass.get(targetName);
|
|
1017
|
+
if (constructor)
|
|
1018
|
+
return constructor;
|
|
1019
|
+
}
|
|
1020
|
+
// Case 4: Standalone function call
|
|
1021
|
+
if (targetType === CoreNodeType.FUNCTION_DECLARATION) {
|
|
1022
|
+
const func = this.functionsByName.get(targetName);
|
|
1023
|
+
if (func)
|
|
1024
|
+
return func;
|
|
1025
|
+
}
|
|
1026
|
+
// Fallback: generic name matching
|
|
1027
|
+
return this.findNodeByNameAndType(targetName, targetType);
|
|
370
1028
|
}
|
|
371
1029
|
/**
|
|
372
1030
|
* Find a parsed node by name and core type
|
|
@@ -464,26 +1122,49 @@ export class TypeScriptParser {
|
|
|
464
1122
|
return undefined;
|
|
465
1123
|
}
|
|
466
1124
|
/**
|
|
467
|
-
* Resolve deferred edges
|
|
1125
|
+
* Resolve deferred edges against both parsed nodes and existing nodes.
|
|
1126
|
+
* Call this after all chunks have been parsed.
|
|
1127
|
+
* @returns Resolved edges
|
|
468
1128
|
*/
|
|
469
1129
|
async resolveDeferredEdges() {
|
|
1130
|
+
const resolvedEdges = [];
|
|
470
1131
|
// Count edges by type for logging
|
|
471
1132
|
const importsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.IMPORTS).length;
|
|
472
1133
|
const extendsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.EXTENDS).length;
|
|
473
1134
|
const implementsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.IMPLEMENTS).length;
|
|
1135
|
+
const callsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.CALLS).length;
|
|
474
1136
|
let importsResolved = 0;
|
|
475
1137
|
let extendsResolved = 0;
|
|
476
1138
|
let implementsResolved = 0;
|
|
1139
|
+
let callsResolved = 0;
|
|
477
1140
|
const unresolvedImports = [];
|
|
478
1141
|
const unresolvedExtends = [];
|
|
479
1142
|
const unresolvedImplements = [];
|
|
1143
|
+
const unresolvedCalls = [];
|
|
480
1144
|
for (const deferred of this.deferredEdges) {
|
|
1145
|
+
// Special handling for CALLS edges - uses resolveCallTarget for proper resolution
|
|
1146
|
+
if (deferred.edgeType === CoreEdgeType.CALLS) {
|
|
1147
|
+
const targetNode = this.resolveCallTarget(deferred);
|
|
1148
|
+
if (targetNode) {
|
|
1149
|
+
const edge = this.createCallsEdge(deferred.sourceNodeId, targetNode.id, deferred.callContext);
|
|
1150
|
+
resolvedEdges.push(edge);
|
|
1151
|
+
this.addEdge(edge);
|
|
1152
|
+
callsResolved++;
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
const callDesc = deferred.callContext?.receiverType
|
|
1156
|
+
? `${deferred.callContext.receiverType}.${deferred.targetName}`
|
|
1157
|
+
: deferred.targetName;
|
|
1158
|
+
unresolvedCalls.push(callDesc);
|
|
1159
|
+
}
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
481
1162
|
// Pass filePath for precise matching (especially important for EXTENDS/IMPLEMENTS)
|
|
482
1163
|
const targetNode = this.findNodeByNameAndType(deferred.targetName, deferred.targetType, deferred.targetFilePath);
|
|
483
1164
|
if (targetNode) {
|
|
484
1165
|
const edge = this.createCoreEdge(deferred.edgeType, deferred.sourceNodeId, targetNode.id);
|
|
1166
|
+
resolvedEdges.push(edge);
|
|
485
1167
|
this.addEdge(edge);
|
|
486
|
-
// Track resolution by type
|
|
487
1168
|
if (deferred.edgeType === CoreEdgeType.IMPORTS) {
|
|
488
1169
|
importsResolved++;
|
|
489
1170
|
}
|
|
@@ -495,7 +1176,6 @@ export class TypeScriptParser {
|
|
|
495
1176
|
}
|
|
496
1177
|
}
|
|
497
1178
|
else {
|
|
498
|
-
// Track unresolved by type
|
|
499
1179
|
if (deferred.edgeType === CoreEdgeType.IMPORTS) {
|
|
500
1180
|
unresolvedImports.push(deferred.targetName);
|
|
501
1181
|
}
|
|
@@ -507,208 +1187,40 @@ export class TypeScriptParser {
|
|
|
507
1187
|
}
|
|
508
1188
|
}
|
|
509
1189
|
}
|
|
510
|
-
// Log
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1190
|
+
// Log edge resolution stats
|
|
1191
|
+
await debugLog('Edge resolution', {
|
|
1192
|
+
totalDeferredEdges: this.deferredEdges.length,
|
|
1193
|
+
totalNodesAvailable: this.parsedNodes.size + this.existingNodes.size,
|
|
1194
|
+
imports: {
|
|
1195
|
+
queued: importsCount,
|
|
514
1196
|
resolved: importsResolved,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1197
|
+
unresolved: unresolvedImports.length,
|
|
1198
|
+
sample: unresolvedImports.slice(0, 10),
|
|
1199
|
+
},
|
|
1200
|
+
extends: {
|
|
1201
|
+
queued: extendsCount,
|
|
1202
|
+
resolved: extendsResolved,
|
|
1203
|
+
unresolved: unresolvedExtends.length,
|
|
1204
|
+
sample: unresolvedExtends.slice(0, 10),
|
|
1205
|
+
},
|
|
1206
|
+
implements: {
|
|
1207
|
+
queued: implementsCount,
|
|
1208
|
+
resolved: implementsResolved,
|
|
1209
|
+
unresolved: unresolvedImplements.length,
|
|
1210
|
+
sample: unresolvedImplements.slice(0, 10),
|
|
1211
|
+
},
|
|
1212
|
+
calls: {
|
|
1213
|
+
queued: callsCount,
|
|
1214
|
+
resolved: callsResolved,
|
|
1215
|
+
unresolved: unresolvedCalls.length,
|
|
1216
|
+
sample: unresolvedCalls.slice(0, 10),
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
532
1219
|
this.deferredEdges = [];
|
|
533
|
-
|
|
534
|
-
async parseCoreTypeScript(sourceFile) {
|
|
535
|
-
try {
|
|
536
|
-
// Create source file node
|
|
537
|
-
const sourceFileNode = this.createCoreNode(sourceFile, CoreNodeType.SOURCE_FILE);
|
|
538
|
-
this.addNode(sourceFileNode);
|
|
539
|
-
// Parse classes
|
|
540
|
-
for (const classDecl of sourceFile.getClasses()) {
|
|
541
|
-
const classNode = this.createCoreNode(classDecl, CoreNodeType.CLASS_DECLARATION, {}, sourceFileNode.id);
|
|
542
|
-
this.addNode(classNode);
|
|
543
|
-
// File contains class relationship
|
|
544
|
-
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, classNode.id);
|
|
545
|
-
this.addEdge(containsEdge);
|
|
546
|
-
// Parse class decorators
|
|
547
|
-
for (const decorator of classDecl.getDecorators()) {
|
|
548
|
-
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, classNode.id);
|
|
549
|
-
this.addNode(decoratorNode);
|
|
550
|
-
// Class decorated with decorator relationship
|
|
551
|
-
const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, classNode.id, decoratorNode.id);
|
|
552
|
-
this.addEdge(decoratedEdge);
|
|
553
|
-
}
|
|
554
|
-
// Parse methods
|
|
555
|
-
for (const method of classDecl.getMethods()) {
|
|
556
|
-
const methodNode = this.createCoreNode(method, CoreNodeType.METHOD_DECLARATION, {}, classNode.id);
|
|
557
|
-
this.addNode(methodNode);
|
|
558
|
-
// Class has method relationship
|
|
559
|
-
const hasMethodEdge = this.createCoreEdge(CoreEdgeType.HAS_MEMBER, classNode.id, methodNode.id);
|
|
560
|
-
this.addEdge(hasMethodEdge);
|
|
561
|
-
// Parse method decorators
|
|
562
|
-
for (const decorator of method.getDecorators()) {
|
|
563
|
-
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, methodNode.id);
|
|
564
|
-
this.addNode(decoratorNode);
|
|
565
|
-
// Method decorated with decorator relationship
|
|
566
|
-
const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, methodNode.id, decoratorNode.id);
|
|
567
|
-
this.addEdge(decoratedEdge);
|
|
568
|
-
}
|
|
569
|
-
// Parse method parameters
|
|
570
|
-
for (const param of method.getParameters()) {
|
|
571
|
-
const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION, {}, methodNode.id);
|
|
572
|
-
this.addNode(paramNode);
|
|
573
|
-
// Method has parameter relationship
|
|
574
|
-
const hasParamEdge = this.createCoreEdge(CoreEdgeType.HAS_PARAMETER, methodNode.id, paramNode.id);
|
|
575
|
-
this.addEdge(hasParamEdge);
|
|
576
|
-
// Parse parameter decorators
|
|
577
|
-
for (const decorator of param.getDecorators()) {
|
|
578
|
-
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, paramNode.id);
|
|
579
|
-
this.addNode(decoratorNode);
|
|
580
|
-
// Parameter decorated with decorator relationship
|
|
581
|
-
const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, paramNode.id, decoratorNode.id);
|
|
582
|
-
this.addEdge(decoratedEdge);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
// Parse properties
|
|
587
|
-
for (const property of classDecl.getProperties()) {
|
|
588
|
-
const propertyNode = this.createCoreNode(property, CoreNodeType.PROPERTY_DECLARATION, {}, classNode.id);
|
|
589
|
-
this.addNode(propertyNode);
|
|
590
|
-
// Class has property relationship
|
|
591
|
-
const hasPropertyEdge = this.createCoreEdge(CoreEdgeType.HAS_MEMBER, classNode.id, propertyNode.id);
|
|
592
|
-
this.addEdge(hasPropertyEdge);
|
|
593
|
-
// Parse property decorators
|
|
594
|
-
for (const decorator of property.getDecorators()) {
|
|
595
|
-
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, propertyNode.id);
|
|
596
|
-
this.addNode(decoratorNode);
|
|
597
|
-
// Property decorated with decorator relationship
|
|
598
|
-
const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, propertyNode.id, decoratorNode.id);
|
|
599
|
-
this.addEdge(decoratedEdge);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
// Parse interfaces
|
|
604
|
-
for (const interfaceDecl of sourceFile.getInterfaces()) {
|
|
605
|
-
const interfaceNode = this.createCoreNode(interfaceDecl, CoreNodeType.INTERFACE_DECLARATION, {}, sourceFileNode.id);
|
|
606
|
-
this.addNode(interfaceNode);
|
|
607
|
-
// File contains interface relationship
|
|
608
|
-
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, interfaceNode.id);
|
|
609
|
-
this.addEdge(containsEdge);
|
|
610
|
-
}
|
|
611
|
-
// Parse functions
|
|
612
|
-
for (const funcDecl of sourceFile.getFunctions()) {
|
|
613
|
-
const functionNode = this.createCoreNode(funcDecl, CoreNodeType.FUNCTION_DECLARATION, {}, sourceFileNode.id);
|
|
614
|
-
this.addNode(functionNode);
|
|
615
|
-
// File contains function relationship
|
|
616
|
-
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, functionNode.id);
|
|
617
|
-
this.addEdge(containsEdge);
|
|
618
|
-
// Parse function parameters
|
|
619
|
-
for (const param of funcDecl.getParameters()) {
|
|
620
|
-
const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION, {}, functionNode.id);
|
|
621
|
-
this.addNode(paramNode);
|
|
622
|
-
// Function has parameter relationship
|
|
623
|
-
const hasParamEdge = this.createCoreEdge(CoreEdgeType.HAS_PARAMETER, functionNode.id, paramNode.id);
|
|
624
|
-
this.addEdge(hasParamEdge);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
// Parse imports
|
|
628
|
-
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
629
|
-
const importNode = this.createCoreNode(importDecl, CoreNodeType.IMPORT_DECLARATION, {}, sourceFileNode.id);
|
|
630
|
-
this.addNode(importNode);
|
|
631
|
-
// File contains import relationship
|
|
632
|
-
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, importNode.id);
|
|
633
|
-
this.addEdge(containsEdge);
|
|
634
|
-
// Try to resolve import to create SourceFile -> SourceFile IMPORTS edge
|
|
635
|
-
try {
|
|
636
|
-
const targetSourceFile = importDecl.getModuleSpecifierSourceFile();
|
|
637
|
-
if (targetSourceFile) {
|
|
638
|
-
const targetFilePath = targetSourceFile.getFilePath();
|
|
639
|
-
// Queue deferred edge - will be resolved after all files are parsed
|
|
640
|
-
this.deferredEdges.push({
|
|
641
|
-
edgeType: CoreEdgeType.IMPORTS,
|
|
642
|
-
sourceNodeId: sourceFileNode.id,
|
|
643
|
-
targetName: targetFilePath, // Use file path as "name" for SourceFiles
|
|
644
|
-
targetType: CoreNodeType.SOURCE_FILE,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
catch {
|
|
649
|
-
// Module resolution failed - external dependency, skip
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
// Parse variable declarations if framework schema specifies this file should have them parsed
|
|
653
|
-
if (this.shouldParseVariables(sourceFile.getFilePath())) {
|
|
654
|
-
for (const varStatement of sourceFile.getVariableStatements()) {
|
|
655
|
-
for (const varDecl of varStatement.getDeclarations()) {
|
|
656
|
-
const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, {}, sourceFileNode.id);
|
|
657
|
-
this.addNode(variableNode);
|
|
658
|
-
// File contains variable relationship
|
|
659
|
-
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
|
|
660
|
-
this.addEdge(containsEdge);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
catch (error) {
|
|
666
|
-
console.error(`Error parsing file ${sourceFile.getFilePath()}:`, error);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
createCoreNode(astNode, coreType, baseProperties = {}, parentId) {
|
|
670
|
-
const name = this.extractNodeName(astNode, coreType);
|
|
671
|
-
const filePath = astNode.getSourceFile().getFilePath();
|
|
672
|
-
const nodeId = generateDeterministicId(this.projectId, coreType, filePath, name, parentId);
|
|
673
|
-
// Extract base properties using schema
|
|
674
|
-
const properties = {
|
|
675
|
-
id: nodeId,
|
|
676
|
-
projectId: this.projectId,
|
|
677
|
-
name,
|
|
678
|
-
coreType,
|
|
679
|
-
filePath,
|
|
680
|
-
startLine: astNode.getStartLineNumber(),
|
|
681
|
-
endLine: astNode.getEndLineNumber(),
|
|
682
|
-
sourceCode: astNode.getText(),
|
|
683
|
-
createdAt: new Date().toISOString(),
|
|
684
|
-
...baseProperties,
|
|
685
|
-
};
|
|
686
|
-
// Extract schema-defined properties
|
|
687
|
-
const coreNodeDef = this.coreSchema.nodeTypes[coreType];
|
|
688
|
-
if (coreNodeDef) {
|
|
689
|
-
for (const propDef of coreNodeDef.properties) {
|
|
690
|
-
try {
|
|
691
|
-
const value = this.extractProperty(astNode, propDef);
|
|
692
|
-
if (value !== undefined && propDef.name !== 'context') {
|
|
693
|
-
properties[propDef.name] = value;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
catch (error) {
|
|
697
|
-
console.warn(`Failed to extract core property ${propDef.name}:`, error);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
return {
|
|
702
|
-
id: nodeId,
|
|
703
|
-
coreType,
|
|
704
|
-
labels: [...(coreNodeDef?.neo4j.labels || [])],
|
|
705
|
-
properties,
|
|
706
|
-
sourceNode: astNode,
|
|
707
|
-
skipEmbedding: coreNodeDef?.neo4j.skipEmbedding ?? false,
|
|
708
|
-
};
|
|
1220
|
+
return resolvedEdges.map(toNeo4jEdge);
|
|
709
1221
|
}
|
|
710
1222
|
async applyContextExtractors() {
|
|
711
|
-
|
|
1223
|
+
debugLog('Applying context extractors');
|
|
712
1224
|
// Apply global context extractors from framework schemas
|
|
713
1225
|
for (const frameworkSchema of this.frameworkSchemas) {
|
|
714
1226
|
for (const extractor of frameworkSchema.contextExtractors) {
|
|
@@ -736,37 +1248,11 @@ export class TypeScriptParser {
|
|
|
736
1248
|
}
|
|
737
1249
|
}
|
|
738
1250
|
}
|
|
739
|
-
createCoreEdge(relationshipType, sourceNodeId, targetNodeId) {
|
|
740
|
-
// Get the weight from the core schema
|
|
741
|
-
const coreEdgeSchema = CORE_TYPESCRIPT_SCHEMA.edgeTypes[relationshipType];
|
|
742
|
-
const relationshipWeight = coreEdgeSchema?.relationshipWeight ?? 0.5;
|
|
743
|
-
// Generate deterministic edge ID based on type + source + target
|
|
744
|
-
const edgeIdentity = `${relationshipType}::${sourceNodeId}::${targetNodeId}`;
|
|
745
|
-
const edgeHash = crypto.createHash('sha256').update(edgeIdentity).digest('hex').substring(0, 16);
|
|
746
|
-
const edgeId = `${relationshipType}:${edgeHash}`;
|
|
747
|
-
return {
|
|
748
|
-
id: edgeId,
|
|
749
|
-
relationshipType,
|
|
750
|
-
sourceNodeId,
|
|
751
|
-
targetNodeId,
|
|
752
|
-
properties: {
|
|
753
|
-
coreType: relationshipType,
|
|
754
|
-
projectId: this.projectId,
|
|
755
|
-
source: 'ast',
|
|
756
|
-
confidence: 1.0,
|
|
757
|
-
relationshipWeight,
|
|
758
|
-
filePath: '',
|
|
759
|
-
createdAt: new Date().toISOString(),
|
|
760
|
-
},
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
1251
|
async applyFrameworkEnhancements() {
|
|
764
|
-
|
|
1252
|
+
await debugLog('Applying framework enhancements', { schemas: this.frameworkSchemas.map((s) => s.name) });
|
|
765
1253
|
for (const frameworkSchema of this.frameworkSchemas) {
|
|
766
|
-
console.log(`📦 Applying framework schema: ${frameworkSchema.name}`);
|
|
767
1254
|
await this.applyFrameworkSchema(frameworkSchema);
|
|
768
1255
|
}
|
|
769
|
-
console.log('✅ Framework enhancements complete');
|
|
770
1256
|
}
|
|
771
1257
|
async applyFrameworkSchema(schema) {
|
|
772
1258
|
// Sort enhancements by priority (highest first)
|
|
@@ -798,7 +1284,11 @@ export class TypeScriptParser {
|
|
|
798
1284
|
}
|
|
799
1285
|
case 'function':
|
|
800
1286
|
if (typeof pattern.pattern === 'function') {
|
|
801
|
-
|
|
1287
|
+
// Pass the AST sourceNode to pattern functions, not the ParsedNode wrapper
|
|
1288
|
+
const astNode = node.sourceNode;
|
|
1289
|
+
if (!astNode)
|
|
1290
|
+
return false;
|
|
1291
|
+
return pattern.pattern(astNode);
|
|
802
1292
|
}
|
|
803
1293
|
return false;
|
|
804
1294
|
case 'classname':
|
|
@@ -867,7 +1357,7 @@ export class TypeScriptParser {
|
|
|
867
1357
|
}
|
|
868
1358
|
}
|
|
869
1359
|
async applyEdgeEnhancements() {
|
|
870
|
-
|
|
1360
|
+
await debugLog('Applying edge enhancements');
|
|
871
1361
|
for (const frameworkSchema of this.frameworkSchemas) {
|
|
872
1362
|
for (const edgeEnhancement of Object.values(frameworkSchema.edgeEnhancements)) {
|
|
873
1363
|
await this.applyEdgeEnhancement(edgeEnhancement);
|
|
@@ -876,13 +1366,12 @@ export class TypeScriptParser {
|
|
|
876
1366
|
}
|
|
877
1367
|
async applyEdgeEnhancement(edgeEnhancement) {
|
|
878
1368
|
try {
|
|
879
|
-
// Combine parsed nodes and existing nodes for
|
|
880
|
-
//
|
|
1369
|
+
// Combine parsed nodes and existing nodes for edge matching
|
|
1370
|
+
// Detection patterns should use pre-extracted context, not raw AST
|
|
881
1371
|
const allTargetNodes = new Map([...this.parsedNodes, ...this.existingNodes]);
|
|
882
1372
|
for (const [sourceId, sourceNode] of this.parsedNodes) {
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
continue;
|
|
1373
|
+
// Note: sourceNode.sourceNode may be undefined after AST cleanup
|
|
1374
|
+
// Detection patterns should use pre-extracted context from node.properties.context
|
|
886
1375
|
for (const [targetId, targetNode] of allTargetNodes) {
|
|
887
1376
|
if (sourceId === targetId)
|
|
888
1377
|
continue;
|
|
@@ -902,165 +1391,110 @@ export class TypeScriptParser {
|
|
|
902
1391
|
console.error(`Error applying edge enhancement ${edgeEnhancement.name}:`, error);
|
|
903
1392
|
}
|
|
904
1393
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
};
|
|
1394
|
+
/**
|
|
1395
|
+
* Apply edge enhancements on all accumulated nodes.
|
|
1396
|
+
* Call this after all chunks have been parsed for streaming mode.
|
|
1397
|
+
* This allows context-dependent edges (like INTERNAL_API_CALL) to be detected
|
|
1398
|
+
* after all nodes and their context have been collected.
|
|
1399
|
+
* @returns New edges created by edge enhancements
|
|
1400
|
+
*/
|
|
1401
|
+
async applyEdgeEnhancementsManually() {
|
|
1402
|
+
const edgeCountBefore = this.parsedEdges.size;
|
|
1403
|
+
await this.applyEdgeEnhancements();
|
|
1404
|
+
const newEdgeCount = this.parsedEdges.size - edgeCountBefore;
|
|
1405
|
+
await debugLog('Edge enhancements applied', { nodeCount: this.parsedNodes.size, newEdges: newEdgeCount });
|
|
1406
|
+
// Return only the new edges (those created by edge enhancements)
|
|
1407
|
+
const allEdges = Array.from(this.parsedEdges.values()).map(toNeo4jEdge);
|
|
1408
|
+
return allEdges.slice(edgeCountBefore);
|
|
921
1409
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return defaultValue;
|
|
939
|
-
case 'context':
|
|
940
|
-
// Context properties are handled by context extractors
|
|
941
|
-
return undefined;
|
|
942
|
-
default:
|
|
943
|
-
return defaultValue;
|
|
1410
|
+
addNode(node) {
|
|
1411
|
+
this.parsedNodes.set(node.id, node);
|
|
1412
|
+
this.indexNodeForCallsResolution(node);
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Build lookup indexes for efficient CALLS edge resolution.
|
|
1416
|
+
* Called when adding nodes during parsing or from external sources.
|
|
1417
|
+
*/
|
|
1418
|
+
indexNodeForCallsResolution(node) {
|
|
1419
|
+
if (node.coreType === CoreNodeType.METHOD_DECLARATION) {
|
|
1420
|
+
const className = this.getClassNameForNode(node);
|
|
1421
|
+
if (className) {
|
|
1422
|
+
if (!this.methodsByClass.has(className)) {
|
|
1423
|
+
this.methodsByClass.set(className, new Map());
|
|
1424
|
+
}
|
|
1425
|
+
this.methodsByClass.get(className).set(node.properties.name, node);
|
|
944
1426
|
}
|
|
945
1427
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
return defaultValue;
|
|
1428
|
+
else if (node.coreType === CoreNodeType.FUNCTION_DECLARATION) {
|
|
1429
|
+
this.functionsByName.set(node.properties.name, node);
|
|
949
1430
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
case CoreNodeType.SOURCE_FILE:
|
|
955
|
-
if (Node.isSourceFile(astNode)) {
|
|
956
|
-
return astNode.getBaseName();
|
|
957
|
-
}
|
|
958
|
-
break;
|
|
959
|
-
case CoreNodeType.CLASS_DECLARATION:
|
|
960
|
-
if (Node.isClassDeclaration(astNode)) {
|
|
961
|
-
return astNode.getName() ?? 'AnonymousClass';
|
|
962
|
-
}
|
|
963
|
-
break;
|
|
964
|
-
case CoreNodeType.METHOD_DECLARATION:
|
|
965
|
-
if (Node.isMethodDeclaration(astNode)) {
|
|
966
|
-
return astNode.getName();
|
|
967
|
-
}
|
|
968
|
-
break;
|
|
969
|
-
case CoreNodeType.FUNCTION_DECLARATION:
|
|
970
|
-
if (Node.isFunctionDeclaration(astNode)) {
|
|
971
|
-
return astNode.getName() ?? 'AnonymousFunction';
|
|
972
|
-
}
|
|
973
|
-
break;
|
|
974
|
-
case CoreNodeType.INTERFACE_DECLARATION:
|
|
975
|
-
if (Node.isInterfaceDeclaration(astNode)) {
|
|
976
|
-
return astNode.getName();
|
|
977
|
-
}
|
|
978
|
-
break;
|
|
979
|
-
case CoreNodeType.PROPERTY_DECLARATION:
|
|
980
|
-
if (Node.isPropertyDeclaration(astNode)) {
|
|
981
|
-
return astNode.getName();
|
|
982
|
-
}
|
|
983
|
-
break;
|
|
984
|
-
case CoreNodeType.PARAMETER_DECLARATION:
|
|
985
|
-
if (Node.isParameterDeclaration(astNode)) {
|
|
986
|
-
return astNode.getName();
|
|
987
|
-
}
|
|
988
|
-
break;
|
|
989
|
-
case CoreNodeType.IMPORT_DECLARATION:
|
|
990
|
-
if (Node.isImportDeclaration(astNode)) {
|
|
991
|
-
return astNode.getModuleSpecifierValue();
|
|
992
|
-
}
|
|
993
|
-
break;
|
|
994
|
-
case CoreNodeType.DECORATOR:
|
|
995
|
-
if (Node.isDecorator(astNode)) {
|
|
996
|
-
return astNode.getName();
|
|
997
|
-
}
|
|
998
|
-
break;
|
|
999
|
-
default:
|
|
1000
|
-
return astNode.getKindName();
|
|
1431
|
+
else if (node.coreType === CoreNodeType.CONSTRUCTOR_DECLARATION) {
|
|
1432
|
+
const className = this.getClassNameForNode(node);
|
|
1433
|
+
if (className) {
|
|
1434
|
+
this.constructorsByClass.set(className, node);
|
|
1001
1435
|
}
|
|
1002
1436
|
}
|
|
1003
|
-
catch (error) {
|
|
1004
|
-
console.warn(`Error extracting name for ${coreType}:`, error);
|
|
1005
|
-
}
|
|
1006
|
-
return 'Unknown';
|
|
1007
1437
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
return excludedNodeTypes.includes(node.getKindName());
|
|
1438
|
+
addEdge(edge) {
|
|
1439
|
+
this.parsedEdges.set(edge.id, edge);
|
|
1011
1440
|
}
|
|
1012
1441
|
/**
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1442
|
+
* Get the class name for a node.
|
|
1443
|
+
* Uses the parentClassName property tracked during parsing.
|
|
1015
1444
|
*/
|
|
1016
|
-
|
|
1017
|
-
//
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
return
|
|
1445
|
+
getClassNameForNode(node) {
|
|
1446
|
+
// Use the parentClassName that was tracked during parseChildNodes
|
|
1447
|
+
return node.properties.parentClassName;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Find the parent class node for a method/property node.
|
|
1451
|
+
* Uses the parentClassName property tracked during parsing.
|
|
1452
|
+
*/
|
|
1453
|
+
findParentClassNode(node) {
|
|
1454
|
+
const parentClassName = node.properties.parentClassName;
|
|
1455
|
+
if (!parentClassName)
|
|
1456
|
+
return undefined;
|
|
1457
|
+
// Find the class node by name and file path
|
|
1458
|
+
for (const [, classNode] of this.parsedNodes) {
|
|
1459
|
+
if (classNode.coreType === CoreNodeType.CLASS_DECLARATION &&
|
|
1460
|
+
classNode.properties.name === parentClassName &&
|
|
1461
|
+
classNode.properties.filePath === node.properties.filePath) {
|
|
1462
|
+
return classNode;
|
|
1463
|
+
}
|
|
1028
1464
|
}
|
|
1465
|
+
return undefined;
|
|
1029
1466
|
}
|
|
1030
1467
|
shouldSkipFile(sourceFile) {
|
|
1031
1468
|
const filePath = sourceFile.getFilePath();
|
|
1032
1469
|
const excludedPatterns = this.parseConfig.excludePatterns ?? [];
|
|
1033
1470
|
for (const pattern of excludedPatterns) {
|
|
1034
|
-
if (
|
|
1471
|
+
if (matchesPattern(filePath, pattern)) {
|
|
1035
1472
|
return true;
|
|
1036
1473
|
}
|
|
1037
1474
|
}
|
|
1038
1475
|
return false;
|
|
1039
1476
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
labels: parsedNode.labels,
|
|
1044
|
-
properties: parsedNode.properties,
|
|
1045
|
-
skipEmbedding: parsedNode.skipEmbedding ?? false,
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
toNeo4jEdge(parsedEdge) {
|
|
1049
|
-
return {
|
|
1050
|
-
id: parsedEdge.id,
|
|
1051
|
-
type: parsedEdge.relationshipType,
|
|
1052
|
-
startNodeId: parsedEdge.sourceNodeId,
|
|
1053
|
-
endNodeId: parsedEdge.targetNodeId,
|
|
1054
|
-
properties: parsedEdge.properties,
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
addNode(node) {
|
|
1058
|
-
this.parsedNodes.set(node.id, node);
|
|
1477
|
+
shouldSkipChildNode(node) {
|
|
1478
|
+
const excludedNodeTypes = this.parseConfig.excludedNodeTypes ?? [];
|
|
1479
|
+
return excludedNodeTypes.includes(node.getKindName());
|
|
1059
1480
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1481
|
+
/**
|
|
1482
|
+
* Check if variable declarations should be parsed for this file
|
|
1483
|
+
* based on framework schema configurations
|
|
1484
|
+
*/
|
|
1485
|
+
shouldParseVariables(filePath) {
|
|
1486
|
+
for (const schema of this.frameworkSchemas) {
|
|
1487
|
+
const parsePatterns = schema.metadata.parseVariablesFrom;
|
|
1488
|
+
if (parsePatterns) {
|
|
1489
|
+
for (const pattern of parsePatterns) {
|
|
1490
|
+
if (minimatch(filePath, pattern)) {
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return false;
|
|
1062
1497
|
}
|
|
1063
|
-
// Helper methods for statistics and debugging
|
|
1064
1498
|
getStats() {
|
|
1065
1499
|
const nodesByType = {};
|
|
1066
1500
|
const nodesBySemanticType = {};
|
|
@@ -1093,17 +1527,24 @@ export class TypeScriptParser {
|
|
|
1093
1527
|
}));
|
|
1094
1528
|
return { nodes, edges };
|
|
1095
1529
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1530
|
+
/**
|
|
1531
|
+
* Get count of currently parsed nodes and edges.
|
|
1532
|
+
* Useful for progress reporting.
|
|
1533
|
+
*/
|
|
1534
|
+
getCurrentCounts() {
|
|
1535
|
+
return {
|
|
1536
|
+
nodes: this.parsedNodes.size,
|
|
1537
|
+
edges: this.parsedEdges.size,
|
|
1538
|
+
deferredEdges: this.deferredEdges.length,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1100
1541
|
/**
|
|
1101
1542
|
* Export current chunk results without clearing internal state.
|
|
1102
1543
|
* Use this when importing chunks incrementally.
|
|
1103
1544
|
*/
|
|
1104
1545
|
exportChunkResults() {
|
|
1105
|
-
const nodes = Array.from(this.parsedNodes.values()).map(
|
|
1106
|
-
const edges = Array.from(this.parsedEdges.values()).map(
|
|
1546
|
+
const nodes = Array.from(this.parsedNodes.values()).map(toNeo4jNode);
|
|
1547
|
+
const edges = Array.from(this.parsedEdges.values()).map(toNeo4jEdge);
|
|
1107
1548
|
return {
|
|
1108
1549
|
nodes,
|
|
1109
1550
|
edges,
|
|
@@ -1118,259 +1559,114 @@ export class TypeScriptParser {
|
|
|
1118
1559
|
this.parsedNodes.clear();
|
|
1119
1560
|
this.parsedEdges.clear();
|
|
1120
1561
|
this.deferredEdges = [];
|
|
1562
|
+
// Clear CALLS edge lookup indexes
|
|
1563
|
+
this.methodsByClass.clear();
|
|
1564
|
+
this.functionsByName.clear();
|
|
1565
|
+
this.constructorsByClass.clear();
|
|
1566
|
+
this.exportedNodeIds.clear();
|
|
1567
|
+
this.exportedEdgeIds.clear();
|
|
1121
1568
|
}
|
|
1122
1569
|
/**
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
*/
|
|
1126
|
-
getCurrentCounts() {
|
|
1127
|
-
return {
|
|
1128
|
-
nodes: this.parsedNodes.size,
|
|
1129
|
-
edges: this.parsedEdges.size,
|
|
1130
|
-
deferredEdges: this.deferredEdges.length,
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* Set the shared context for this parser.
|
|
1135
|
-
* Use this to share context across multiple parsers (e.g., in WorkspaceParser).
|
|
1136
|
-
* @param context The shared context map to use
|
|
1137
|
-
*/
|
|
1138
|
-
setSharedContext(context) {
|
|
1139
|
-
this.sharedContext = context;
|
|
1140
|
-
}
|
|
1141
|
-
/**
|
|
1142
|
-
* Get the shared context from this parser.
|
|
1143
|
-
* Useful for aggregating context across multiple parsers.
|
|
1144
|
-
*/
|
|
1145
|
-
getSharedContext() {
|
|
1146
|
-
return this.sharedContext;
|
|
1147
|
-
}
|
|
1148
|
-
/**
|
|
1149
|
-
* Get all parsed nodes (for cross-parser edge resolution).
|
|
1150
|
-
* Returns the internal Map of ParsedNodes.
|
|
1151
|
-
*/
|
|
1152
|
-
getParsedNodes() {
|
|
1153
|
-
return this.parsedNodes;
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Get the framework schemas used by this parser.
|
|
1157
|
-
* Useful for WorkspaceParser to apply cross-package edge enhancements.
|
|
1158
|
-
*/
|
|
1159
|
-
getFrameworkSchemas() {
|
|
1160
|
-
return this.frameworkSchemas;
|
|
1161
|
-
}
|
|
1162
|
-
/**
|
|
1163
|
-
* Defer edge enhancements to a parent parser (e.g., WorkspaceParser).
|
|
1164
|
-
* When true, parseChunk() will skip applyEdgeEnhancements().
|
|
1165
|
-
* The parent is responsible for calling applyEdgeEnhancementsManually() at the end.
|
|
1166
|
-
*/
|
|
1167
|
-
setDeferEdgeEnhancements(defer) {
|
|
1168
|
-
this.deferEdgeEnhancements = defer;
|
|
1169
|
-
}
|
|
1170
|
-
/**
|
|
1171
|
-
* Get list of source files in the project.
|
|
1172
|
-
* In lazy mode, uses glob to discover files without loading them into memory.
|
|
1173
|
-
* Useful for determining total work and creating chunks.
|
|
1570
|
+
* Add nodes to the existing nodes map for cross-chunk edge resolution.
|
|
1571
|
+
* These nodes are considered as potential edge targets but won't be exported.
|
|
1174
1572
|
*/
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
});
|
|
1187
|
-
// Apply exclude patterns from parseConfig
|
|
1188
|
-
const excludedPatterns = this.parseConfig.excludePatterns ?? [];
|
|
1189
|
-
this.discoveredFiles = allFiles.filter((filePath) => {
|
|
1190
|
-
for (const excludePattern of excludedPatterns) {
|
|
1191
|
-
if (this.matchesPattern(filePath, excludePattern)) {
|
|
1192
|
-
return false;
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
return true;
|
|
1196
|
-
});
|
|
1197
|
-
console.log(`🔍 Discovered ${this.discoveredFiles.length} TypeScript files (lazy mode)`);
|
|
1198
|
-
return this.discoveredFiles;
|
|
1199
|
-
}
|
|
1200
|
-
else {
|
|
1201
|
-
// Eager mode - files are already loaded
|
|
1202
|
-
this.discoveredFiles = this.project
|
|
1203
|
-
.getSourceFiles()
|
|
1204
|
-
.filter((sf) => !this.shouldSkipFile(sf))
|
|
1205
|
-
.map((sf) => sf.getFilePath());
|
|
1206
|
-
return this.discoveredFiles;
|
|
1573
|
+
addExistingNodesFromChunk(nodes) {
|
|
1574
|
+
for (const node of nodes) {
|
|
1575
|
+
const parsedNode = {
|
|
1576
|
+
id: node.id,
|
|
1577
|
+
coreType: node.properties.coreType,
|
|
1578
|
+
semanticType: node.properties.semanticType,
|
|
1579
|
+
labels: node.labels,
|
|
1580
|
+
properties: node.properties,
|
|
1581
|
+
};
|
|
1582
|
+
this.existingNodes.set(node.id, parsedNode);
|
|
1583
|
+
this.indexNodeForCallsResolution(parsedNode);
|
|
1207
1584
|
}
|
|
1208
1585
|
}
|
|
1209
1586
|
/**
|
|
1210
|
-
*
|
|
1587
|
+
* Add nodes to parsedNodes for edge enhancement.
|
|
1588
|
+
* Use this when coordinator needs to run edge enhancements on accumulated chunk results.
|
|
1589
|
+
* Unlike addExistingNodesFromChunk, these nodes ARE used as edge sources.
|
|
1211
1590
|
*/
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1591
|
+
addParsedNodesFromChunk(nodes) {
|
|
1592
|
+
for (const node of nodes) {
|
|
1593
|
+
const parsedNode = {
|
|
1594
|
+
id: node.id,
|
|
1595
|
+
coreType: node.properties.coreType,
|
|
1596
|
+
semanticType: node.properties.semanticType,
|
|
1597
|
+
labels: node.labels,
|
|
1598
|
+
properties: node.properties,
|
|
1599
|
+
};
|
|
1600
|
+
this.parsedNodes.set(node.id, parsedNode);
|
|
1601
|
+
this.indexNodeForCallsResolution(parsedNode);
|
|
1215
1602
|
}
|
|
1216
|
-
return this.project
|
|
1217
|
-
.getSourceFiles()
|
|
1218
|
-
.filter((sf) => !this.shouldSkipFile(sf))
|
|
1219
|
-
.map((sf) => sf.getFilePath());
|
|
1220
1603
|
}
|
|
1221
1604
|
/**
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
* In lazy mode, files are added to the project just-in-time and removed after parsing.
|
|
1225
|
-
* @param filePaths Specific file paths to parse
|
|
1226
|
-
* @param skipEdgeResolution If true, deferred edges are not resolved (default: false)
|
|
1605
|
+
* Get serialized shared context for parallel parsing.
|
|
1606
|
+
* Converts Maps to arrays for structured clone compatibility.
|
|
1227
1607
|
*/
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
if (
|
|
1233
|
-
|
|
1234
|
-
for (const filePath of filePaths) {
|
|
1235
|
-
try {
|
|
1236
|
-
// Check if file already exists in project (shouldn't happen in lazy mode)
|
|
1237
|
-
// Add the file to the project if not already present
|
|
1238
|
-
const sourceFile = this.project.getSourceFile(filePath) ?? this.project.addSourceFileAtPath(filePath);
|
|
1239
|
-
sourceFiles.push(sourceFile);
|
|
1240
|
-
}
|
|
1241
|
-
catch (error) {
|
|
1242
|
-
console.warn(`Failed to add source file ${filePath}:`, error);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1608
|
+
getSerializedSharedContext() {
|
|
1609
|
+
const serialized = [];
|
|
1610
|
+
for (const [key, value] of this.sharedContext) {
|
|
1611
|
+
// Convert nested Maps to arrays
|
|
1612
|
+
if (value instanceof Map) {
|
|
1613
|
+
serialized.push([key, Array.from(value.entries())]);
|
|
1245
1614
|
}
|
|
1246
1615
|
else {
|
|
1247
|
-
|
|
1248
|
-
const loadedFiles = filePaths
|
|
1249
|
-
.map((filePath) => this.project.getSourceFile(filePath))
|
|
1250
|
-
.filter((sf) => sf !== undefined);
|
|
1251
|
-
sourceFiles.push(...loadedFiles);
|
|
1252
|
-
}
|
|
1253
|
-
for (const sourceFile of sourceFiles) {
|
|
1254
|
-
if (this.shouldSkipFile(sourceFile))
|
|
1255
|
-
continue;
|
|
1256
|
-
await this.parseCoreTypeScriptV2(sourceFile);
|
|
1257
|
-
}
|
|
1258
|
-
// Only resolve edges if not skipping
|
|
1259
|
-
if (!skipEdgeResolution) {
|
|
1260
|
-
await this.resolveDeferredEdges();
|
|
1261
|
-
}
|
|
1262
|
-
await this.applyContextExtractors();
|
|
1263
|
-
if (this.frameworkSchemas.length > 0) {
|
|
1264
|
-
await this.applyFrameworkEnhancements();
|
|
1265
|
-
}
|
|
1266
|
-
// Apply edge enhancements unless deferred to parent (e.g., WorkspaceParser)
|
|
1267
|
-
// When deferred, parent will call applyEdgeEnhancementsManually() at the end
|
|
1268
|
-
// with all accumulated nodes for cross-package edge detection
|
|
1269
|
-
if (!this.deferEdgeEnhancements) {
|
|
1270
|
-
await this.applyEdgeEnhancements();
|
|
1271
|
-
}
|
|
1272
|
-
const neo4jNodes = Array.from(this.parsedNodes.values()).map(this.toNeo4jNode);
|
|
1273
|
-
const neo4jEdges = Array.from(this.parsedEdges.values()).map(this.toNeo4jEdge);
|
|
1274
|
-
return { nodes: neo4jNodes, edges: neo4jEdges };
|
|
1275
|
-
}
|
|
1276
|
-
finally {
|
|
1277
|
-
// Always clean up in lazy mode to prevent memory leaks
|
|
1278
|
-
if (this.lazyLoad) {
|
|
1279
|
-
for (const sourceFile of sourceFiles) {
|
|
1280
|
-
try {
|
|
1281
|
-
this.project.removeSourceFile(sourceFile);
|
|
1282
|
-
}
|
|
1283
|
-
catch {
|
|
1284
|
-
// Ignore errors when removing files
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1616
|
+
serialized.push([key, value]);
|
|
1287
1617
|
}
|
|
1288
1618
|
}
|
|
1619
|
+
return serialized;
|
|
1289
1620
|
}
|
|
1290
1621
|
/**
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
* @returns Resolved edges
|
|
1622
|
+
* Merge serialized shared context from workers.
|
|
1623
|
+
* Handles Map merging by combining entries.
|
|
1294
1624
|
*/
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const unresolvedImplements = [];
|
|
1304
|
-
for (const deferred of this.deferredEdges) {
|
|
1305
|
-
// Pass filePath for precise matching (especially important for EXTENDS/IMPLEMENTS)
|
|
1306
|
-
const targetNode = this.findNodeByNameAndType(deferred.targetName, deferred.targetType, deferred.targetFilePath);
|
|
1307
|
-
if (targetNode) {
|
|
1308
|
-
const edge = this.createCoreEdge(deferred.edgeType, deferred.sourceNodeId, targetNode.id);
|
|
1309
|
-
resolvedEdges.push(edge);
|
|
1310
|
-
this.addEdge(edge);
|
|
1311
|
-
if (deferred.edgeType === CoreEdgeType.EXTENDS) {
|
|
1312
|
-
extendsResolved++;
|
|
1313
|
-
}
|
|
1314
|
-
else if (deferred.edgeType === CoreEdgeType.IMPLEMENTS) {
|
|
1315
|
-
implementsResolved++;
|
|
1625
|
+
mergeSerializedSharedContext(serialized) {
|
|
1626
|
+
for (const [key, value] of serialized) {
|
|
1627
|
+
if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
|
|
1628
|
+
// It's a serialized Map - merge with existing
|
|
1629
|
+
const existingMap = this.sharedContext.get(key);
|
|
1630
|
+
const newMap = existingMap ?? new Map();
|
|
1631
|
+
for (const [k, v] of value) {
|
|
1632
|
+
newMap.set(k, v);
|
|
1316
1633
|
}
|
|
1634
|
+
this.sharedContext.set(key, newMap);
|
|
1317
1635
|
}
|
|
1318
1636
|
else {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
}
|
|
1322
|
-
else if (deferred.edgeType === CoreEdgeType.IMPLEMENTS) {
|
|
1323
|
-
unresolvedImplements.push(deferred.targetName);
|
|
1324
|
-
}
|
|
1637
|
+
// Simple value - just set it
|
|
1638
|
+
this.sharedContext.set(key, value);
|
|
1325
1639
|
}
|
|
1326
1640
|
}
|
|
1327
|
-
// Log inheritance resolution stats
|
|
1328
|
-
if (extendsCount > 0 || implementsCount > 0) {
|
|
1329
|
-
await debugLog('Inheritance edge resolution (manual)', {
|
|
1330
|
-
extendsQueued: extendsCount,
|
|
1331
|
-
extendsResolved,
|
|
1332
|
-
extendsUnresolved: unresolvedExtends.length,
|
|
1333
|
-
unresolvedExtendsSample: unresolvedExtends.slice(0, 10),
|
|
1334
|
-
implementsQueued: implementsCount,
|
|
1335
|
-
implementsResolved,
|
|
1336
|
-
implementsUnresolved: unresolvedImplements.length,
|
|
1337
|
-
unresolvedImplementsSample: unresolvedImplements.slice(0, 10),
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
this.deferredEdges = [];
|
|
1341
|
-
return resolvedEdges.map(this.toNeo4jEdge);
|
|
1342
1641
|
}
|
|
1343
1642
|
/**
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
* This allows context-dependent edges (like INTERNAL_API_CALL) to be detected
|
|
1347
|
-
* after all nodes and their context have been collected.
|
|
1348
|
-
* @returns New edges created by edge enhancements
|
|
1643
|
+
* Get deferred edges for cross-chunk resolution.
|
|
1644
|
+
* Returns serializable format for worker thread transfer.
|
|
1349
1645
|
*/
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1646
|
+
getDeferredEdges() {
|
|
1647
|
+
return this.deferredEdges.map((e) => ({
|
|
1648
|
+
edgeType: e.edgeType,
|
|
1649
|
+
sourceNodeId: e.sourceNodeId,
|
|
1650
|
+
targetName: e.targetName,
|
|
1651
|
+
targetType: e.targetType,
|
|
1652
|
+
targetFilePath: e.targetFilePath,
|
|
1653
|
+
callContext: e.callContext,
|
|
1654
|
+
}));
|
|
1359
1655
|
}
|
|
1360
1656
|
/**
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1657
|
+
* Merge deferred edges from workers for resolution.
|
|
1658
|
+
* Converts back to internal format with CoreEdgeType/CoreNodeType.
|
|
1363
1659
|
*/
|
|
1364
|
-
|
|
1365
|
-
for (const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1660
|
+
mergeDeferredEdges(edges) {
|
|
1661
|
+
for (const e of edges) {
|
|
1662
|
+
this.deferredEdges.push({
|
|
1663
|
+
edgeType: e.edgeType,
|
|
1664
|
+
sourceNodeId: e.sourceNodeId,
|
|
1665
|
+
targetName: e.targetName,
|
|
1666
|
+
targetType: e.targetType,
|
|
1667
|
+
targetFilePath: e.targetFilePath,
|
|
1668
|
+
callContext: e.callContext,
|
|
1669
|
+
});
|
|
1374
1670
|
}
|
|
1375
1671
|
}
|
|
1376
1672
|
}
|