codesearch-mcp 0.2.0-alpha → 0.2.2-alpha
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/CHANGELOG.md +22 -0
- package/README.md +5 -2
- package/dist/app.js +2 -17
- package/dist/src/commands/IndexCommand.js +27 -7
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +75 -0
- package/dist/src/db/schema.js +10 -1
- package/dist/src/enriched-prompt.d.ts +8 -0
- package/dist/src/enriched-prompt.js +87 -0
- package/dist/src/interfaces/IAstParser.d.ts +18 -2
- package/dist/src/interfaces/IDatabase.d.ts +14 -0
- package/dist/src/interfaces/ISearcher.d.ts +3 -0
- package/dist/src/services/AstParserService.d.ts +39 -7
- package/dist/src/services/AstParserService.js +503 -104
- package/dist/src/services/DatabaseService.d.ts +13 -0
- package/dist/src/services/DatabaseService.js +84 -2
- package/dist/src/services/IndexerService.d.ts +0 -14
- package/dist/src/services/IndexerService.js +29 -151
- package/dist/src/services/McpServer.js +3 -3
- package/dist/src/services/SearcherService.d.ts +24 -3
- package/dist/src/services/SearcherService.js +149 -58
- package/dist/src/symbol-name.d.ts +44 -0
- package/dist/src/symbol-name.js +68 -0
- package/dist/src/symbol-rows.d.ts +12 -0
- package/dist/src/symbol-rows.js +38 -0
- package/dist/src/tools/FindSymbolTool.js +30 -7
- package/dist/src/tools/GetCallersTool.js +20 -5
- package/docs/architecture.svg +103 -0
- package/package.json +1 -2
- package/dist/src/commands/BackfillCommand.d.ts +0 -7
- package/dist/src/commands/BackfillCommand.js +0 -37
|
@@ -44,6 +44,7 @@ const FALLBACK_OPTS = {
|
|
|
44
44
|
compilerOptions: { allowJs: true, noEmit: true },
|
|
45
45
|
};
|
|
46
46
|
const MORPH_CACHE_MAX = 10;
|
|
47
|
+
const VALID_SEGMENT = /^#?[\w$]+$/;
|
|
47
48
|
class AstParserService {
|
|
48
49
|
logger;
|
|
49
50
|
morphProjects = new Map();
|
|
@@ -60,7 +61,7 @@ class AstParserService {
|
|
|
60
61
|
const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution, rootTsConfigPath);
|
|
61
62
|
for (const filePath of filePaths) {
|
|
62
63
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
63
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
|
|
64
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
66
67
|
try {
|
|
@@ -68,22 +69,12 @@ class AstParserService {
|
|
|
68
69
|
}
|
|
69
70
|
catch (err) {
|
|
70
71
|
this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
|
|
71
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
|
|
72
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
for (const sourceFile of morphProject.getSourceFiles()) {
|
|
75
76
|
const filePath = sourceFile.getFilePath();
|
|
76
|
-
|
|
77
|
-
const edges = this.extractEdges(sourceFile, filePath, serviceName);
|
|
78
|
-
const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
|
|
79
|
-
const calls = this.extractCalls(sourceFile, filePath, serviceName);
|
|
80
|
-
const ownerRanges = this.extractOwnerRanges(sourceFile);
|
|
81
|
-
analysisMap.set(filePath, { edges, symbols, calls, ownerRanges });
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
|
|
85
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
|
|
86
|
-
}
|
|
77
|
+
analysisMap.set(filePath, this.analyzeSourceFile(sourceFile, filePath, serviceName));
|
|
87
78
|
}
|
|
88
79
|
// Release source file memory before returning
|
|
89
80
|
for (const sourceFile of morphProject.getSourceFiles()) {
|
|
@@ -110,7 +101,7 @@ class AstParserService {
|
|
|
110
101
|
this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
|
|
111
102
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
112
103
|
this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
|
|
113
|
-
return { edges: [], symbols: [], calls: [] };
|
|
104
|
+
return { edges: [], symbols: [], calls: [], members: [] };
|
|
114
105
|
}
|
|
115
106
|
const morphProject = this.getMorphProject(projectRoot);
|
|
116
107
|
const existing = morphProject.getSourceFile(filePath);
|
|
@@ -122,21 +113,48 @@ class AstParserService {
|
|
|
122
113
|
}
|
|
123
114
|
catch (err) {
|
|
124
115
|
this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
|
|
125
|
-
return { edges: [], symbols: [], calls: [] };
|
|
116
|
+
return { edges: [], symbols: [], calls: [], members: [] };
|
|
126
117
|
}
|
|
127
|
-
const
|
|
128
|
-
const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
|
|
129
|
-
const calls = this.extractCalls(sourceFile, filePath, serviceName);
|
|
130
|
-
const ownerRanges = this.extractOwnerRanges(sourceFile);
|
|
118
|
+
const result = this.analyzeSourceFile(sourceFile, filePath, serviceName);
|
|
131
119
|
morphProject.removeSourceFile(sourceFile);
|
|
132
|
-
this.logger.debug('AstParser: analyzeFile done', {
|
|
133
|
-
|
|
120
|
+
this.logger.debug('AstParser: analyzeFile done', {
|
|
121
|
+
filePath, edges: result.edges.length, symbols: result.symbols.length, members: result.members.length, calls: result.calls.length, ms: Date.now() - t0,
|
|
122
|
+
});
|
|
123
|
+
return result;
|
|
134
124
|
}
|
|
135
125
|
// Kept for test compatibility (extract-import-edges.test.ts).
|
|
136
126
|
async extractImportEdges(filePath, serviceName, projectRoot) {
|
|
137
127
|
const { edges } = await this.analyzeFile(filePath, serviceName, projectRoot);
|
|
138
128
|
return edges;
|
|
139
129
|
}
|
|
130
|
+
analyzeSourceFile(sourceFile, filePath, serviceName) {
|
|
131
|
+
try {
|
|
132
|
+
const edges = this.extractEdges(sourceFile, filePath, serviceName);
|
|
133
|
+
const symbolCandidates = this.extractSymbols(sourceFile, filePath, serviceName);
|
|
134
|
+
const symbols = symbolCandidates.map(c => c.entry);
|
|
135
|
+
// Member extraction (classes anywhere, class/interface/object-literal
|
|
136
|
+
// members, namespace-level declarations) is isolated in its own try: a
|
|
137
|
+
// bug there must never change a file's top-level symbols, chunks or
|
|
138
|
+
// embedding prompt (see the `symbol-index-members` retrieval-invariance
|
|
139
|
+
// requirement).
|
|
140
|
+
let memberCandidates = [];
|
|
141
|
+
try {
|
|
142
|
+
memberCandidates = this.extractMembers(sourceFile, filePath, serviceName);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
this.logger.warn('AstParser: member extraction failed', { filePath, reason: err.message });
|
|
146
|
+
memberCandidates = [];
|
|
147
|
+
}
|
|
148
|
+
const members = memberCandidates.map(c => c.entry);
|
|
149
|
+
const calls = this.extractCalls(sourceFile, filePath, serviceName);
|
|
150
|
+
const ownerRanges = this.buildOwnerRanges([...symbolCandidates, ...memberCandidates]);
|
|
151
|
+
return { edges, symbols, members, calls, ownerRanges };
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
|
|
155
|
+
return { edges: [], symbols: [], calls: [], members: [] };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
140
158
|
// ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
|
|
141
159
|
createMorphProject(projectRoot, skipFileDependencyResolution = false, rootTsConfigPath) {
|
|
142
160
|
const t0 = Date.now();
|
|
@@ -307,96 +325,492 @@ class AstParserService {
|
|
|
307
325
|
}
|
|
308
326
|
return map;
|
|
309
327
|
}
|
|
310
|
-
// ── private:
|
|
328
|
+
// ── private: position / signature helpers ───────────────────────────────────
|
|
329
|
+
getPos(sourceFile, node) {
|
|
330
|
+
const { line, character } = sourceFile.compilerNode
|
|
331
|
+
.getLineAndCharacterOfPosition(node.getStart());
|
|
332
|
+
return { line: line + 1, col: character + 1 };
|
|
333
|
+
}
|
|
334
|
+
/** Trimmed text of the source line containing `node`'s start — the signature
|
|
335
|
+
* rule for every kind (name node for members, the position node for
|
|
336
|
+
* constructors/anonymous default classes). */
|
|
337
|
+
lineTextAt(sourceLines, sourceFile, node) {
|
|
338
|
+
const { line } = this.getPos(sourceFile, node);
|
|
339
|
+
return (sourceLines[line - 1] ?? node.getText().split('\n')[0]).trim();
|
|
340
|
+
}
|
|
341
|
+
// ── private: top-level symbol extraction (feeds the chunker + prompt) ──────
|
|
311
342
|
extractSymbols(sourceFile, filePath, serviceName) {
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
return found?.trim() ?? text.split('\n')[0].trim();
|
|
316
|
-
};
|
|
317
|
-
const getPos = (node) => {
|
|
318
|
-
const { line, character } = sourceFile.compilerNode
|
|
319
|
-
.getLineAndCharacterOfPosition(node.getStart());
|
|
320
|
-
return { line: line + 1, col: character + 1 };
|
|
321
|
-
};
|
|
343
|
+
const candidates = [];
|
|
344
|
+
const sourceLines = sourceFile.getFullText().split('\n');
|
|
345
|
+
const lineAt = (node) => this.lineTextAt(sourceLines, sourceFile, node);
|
|
322
346
|
for (const cls of sourceFile.getClasses()) {
|
|
323
347
|
const name = cls.getName();
|
|
324
348
|
if (!name)
|
|
325
349
|
continue;
|
|
326
|
-
const nameNode = cls.getNameNode();
|
|
327
|
-
const { line, col } =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
350
|
+
const nameNode = cls.getNameNode() ?? cls;
|
|
351
|
+
const { line, col } = this.getPos(sourceFile, nameNode);
|
|
352
|
+
candidates.push({
|
|
353
|
+
declNode: cls,
|
|
354
|
+
entry: {
|
|
355
|
+
project: serviceName, filePath, name, kind: 'class',
|
|
356
|
+
signature: lineAt(nameNode),
|
|
357
|
+
line, endLine: cls.getEndLineNumber(), col,
|
|
358
|
+
container: null, isStatic: false,
|
|
359
|
+
},
|
|
332
360
|
});
|
|
333
361
|
}
|
|
334
362
|
for (const fn of sourceFile.getFunctions()) {
|
|
335
363
|
const name = fn.getName();
|
|
336
364
|
if (!name)
|
|
337
365
|
continue;
|
|
338
|
-
const nameNode = fn.getNameNode();
|
|
339
|
-
const { line, col } =
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
366
|
+
const nameNode = fn.getNameNode() ?? fn;
|
|
367
|
+
const { line, col } = this.getPos(sourceFile, nameNode);
|
|
368
|
+
candidates.push({
|
|
369
|
+
declNode: fn,
|
|
370
|
+
entry: {
|
|
371
|
+
project: serviceName, filePath, name, kind: 'function',
|
|
372
|
+
signature: lineAt(nameNode),
|
|
373
|
+
line, endLine: fn.getEndLineNumber(), col,
|
|
374
|
+
container: null, isStatic: false, hasBody: fn.getBody() !== undefined,
|
|
375
|
+
},
|
|
344
376
|
});
|
|
345
377
|
}
|
|
346
378
|
for (const iface of sourceFile.getInterfaces()) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
379
|
+
const nameNode = iface.getNameNode();
|
|
380
|
+
const { line, col } = this.getPos(sourceFile, nameNode);
|
|
381
|
+
candidates.push({
|
|
382
|
+
declNode: iface,
|
|
383
|
+
entry: {
|
|
384
|
+
project: serviceName, filePath, name: iface.getName(), kind: 'interface',
|
|
385
|
+
signature: lineAt(nameNode),
|
|
386
|
+
line, endLine: iface.getEndLineNumber(), col,
|
|
387
|
+
container: null, isStatic: false,
|
|
388
|
+
},
|
|
352
389
|
});
|
|
353
390
|
}
|
|
354
391
|
for (const ta of sourceFile.getTypeAliases()) {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
392
|
+
const nameNode = ta.getNameNode();
|
|
393
|
+
const { line, col } = this.getPos(sourceFile, nameNode);
|
|
394
|
+
candidates.push({
|
|
395
|
+
declNode: ta,
|
|
396
|
+
entry: {
|
|
397
|
+
project: serviceName, filePath, name: ta.getName(), kind: 'type',
|
|
398
|
+
signature: lineAt(nameNode),
|
|
399
|
+
line, endLine: ta.getEndLineNumber(), col,
|
|
400
|
+
container: null, isStatic: false,
|
|
401
|
+
},
|
|
360
402
|
});
|
|
361
403
|
}
|
|
362
404
|
for (const stmt of sourceFile.getVariableStatements()) {
|
|
363
405
|
for (const decl of stmt.getDeclarations()) {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
406
|
+
const nameNode = decl.getNameNode();
|
|
407
|
+
const { line, col } = this.getPos(sourceFile, nameNode);
|
|
408
|
+
candidates.push({
|
|
409
|
+
// Owner ranges use the declarator's own span, not the statement's —
|
|
410
|
+
// each declarator of `const a = 1, b = () => {…}` owns only its lines.
|
|
411
|
+
declNode: decl,
|
|
412
|
+
entry: {
|
|
413
|
+
project: serviceName, filePath, name: decl.getName(), kind: 'variable',
|
|
414
|
+
signature: lineAt(nameNode),
|
|
415
|
+
line, endLine: stmt.getEndLineNumber(), col,
|
|
416
|
+
container: null, isStatic: false,
|
|
417
|
+
},
|
|
369
418
|
});
|
|
370
419
|
}
|
|
371
420
|
}
|
|
372
|
-
return
|
|
421
|
+
return candidates;
|
|
373
422
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
423
|
+
// ── private: member extraction (classes anywhere, members, namespace body) ─
|
|
424
|
+
extractMembers(sourceFile, filePath, serviceName) {
|
|
425
|
+
const candidates = [];
|
|
426
|
+
const sourceLines = sourceFile.getFullText().split('\n');
|
|
427
|
+
const lineAt = (node) => this.lineTextAt(sourceLines, sourceFile, node);
|
|
428
|
+
const push = (declNode, posNode, name, kind, container, isStatic, hasBody) => {
|
|
429
|
+
const { line, col } = this.getPos(sourceFile, posNode);
|
|
430
|
+
candidates.push({
|
|
431
|
+
declNode,
|
|
432
|
+
entry: {
|
|
433
|
+
project: serviceName, filePath, name, kind,
|
|
434
|
+
signature: lineAt(posNode),
|
|
435
|
+
line, endLine: declNode.getEndLineNumber(), col,
|
|
436
|
+
container: container.length ? container.join('.') : null,
|
|
437
|
+
isStatic, hasBody,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
// Classes anywhere in the file — nested, expression, anonymous default.
|
|
442
|
+
for (const cls of [
|
|
443
|
+
...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration),
|
|
444
|
+
...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassExpression),
|
|
445
|
+
]) {
|
|
446
|
+
// Named top-level classes are already handled by extractSymbols; an
|
|
447
|
+
// anonymous top-level class (`export default class {}`) is not — it
|
|
448
|
+
// must still be collected here.
|
|
449
|
+
if (ts_morph_1.Node.isClassDeclaration(cls) && cls.getName() && ts_morph_1.Node.isSourceFile(cls.getParent()))
|
|
384
450
|
continue;
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
451
|
+
const described = this.describeIndexedNode(cls);
|
|
452
|
+
if (!described)
|
|
453
|
+
continue;
|
|
454
|
+
const posNode = (ts_morph_1.Node.isClassDeclaration(cls) ? cls.getNameNode() : undefined) ?? cls;
|
|
455
|
+
push(cls, posNode, described.name, 'class', described.container, false, true);
|
|
456
|
+
}
|
|
457
|
+
// Class / object-literal methods, accessors, constructors, function properties.
|
|
458
|
+
for (const m of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
|
|
459
|
+
const described = this.describeIndexedNode(m);
|
|
460
|
+
if (!described)
|
|
461
|
+
continue;
|
|
462
|
+
push(m, m.getNameNode(), described.name, 'method', described.container, m.isStatic(), m.getBody() !== undefined);
|
|
463
|
+
}
|
|
464
|
+
for (const a of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.GetAccessor)) {
|
|
465
|
+
const described = this.describeIndexedNode(a);
|
|
466
|
+
if (!described)
|
|
467
|
+
continue;
|
|
468
|
+
push(a, a.getNameNode(), described.name, 'get_accessor', described.container, a.isStatic(), true);
|
|
469
|
+
}
|
|
470
|
+
for (const a of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.SetAccessor)) {
|
|
471
|
+
const described = this.describeIndexedNode(a);
|
|
472
|
+
if (!described)
|
|
473
|
+
continue;
|
|
474
|
+
push(a, a.getNameNode(), described.name, 'set_accessor', described.container, a.isStatic(), true);
|
|
475
|
+
}
|
|
476
|
+
for (const c of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.Constructor)) {
|
|
477
|
+
const described = this.describeIndexedNode(c);
|
|
478
|
+
if (!described)
|
|
479
|
+
continue;
|
|
480
|
+
const kw = c.getFirstChildByKind(ts_morph_1.SyntaxKind.ConstructorKeyword) ?? c;
|
|
481
|
+
push(c, kw, described.name, 'constructor', described.container, false, c.getBody() !== undefined);
|
|
482
|
+
}
|
|
483
|
+
for (const p of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyDeclaration)) {
|
|
484
|
+
const described = this.describeIndexedNode(p);
|
|
485
|
+
if (!described)
|
|
486
|
+
continue;
|
|
487
|
+
push(p, p.getNameNode(), described.name, 'function_property', described.container, p.isStatic(), true);
|
|
488
|
+
}
|
|
489
|
+
for (const p of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
|
|
490
|
+
const described = this.describeIndexedNode(p);
|
|
491
|
+
if (!described)
|
|
492
|
+
continue;
|
|
493
|
+
push(p, p.getNameNode(), described.name, 'function_property', described.container, false, true);
|
|
494
|
+
}
|
|
495
|
+
// Interface / type-literal method and function-typed property signatures.
|
|
496
|
+
for (const s of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodSignature)) {
|
|
497
|
+
const described = this.describeIndexedNode(s);
|
|
498
|
+
if (!described)
|
|
499
|
+
continue;
|
|
500
|
+
push(s, s.getNameNode(), described.name, 'method_signature', described.container, false, false);
|
|
501
|
+
}
|
|
502
|
+
for (const s of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertySignature)) {
|
|
503
|
+
const described = this.describeIndexedNode(s);
|
|
504
|
+
if (!described)
|
|
505
|
+
continue;
|
|
506
|
+
push(s, s.getNameNode(), described.name, 'method_signature', described.container, false, false);
|
|
507
|
+
}
|
|
508
|
+
// Declarations directly in a namespace body.
|
|
509
|
+
for (const fn of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.FunctionDeclaration)) {
|
|
510
|
+
if (!ts_morph_1.Node.isModuleBlock(fn.getParent()))
|
|
511
|
+
continue;
|
|
512
|
+
const described = this.describeIndexedNode(fn);
|
|
513
|
+
if (!described)
|
|
514
|
+
continue;
|
|
515
|
+
const nameNode = fn.getNameNode();
|
|
516
|
+
if (!nameNode)
|
|
517
|
+
continue;
|
|
518
|
+
push(fn, nameNode, described.name, 'function', described.container, false, fn.getBody() !== undefined);
|
|
519
|
+
}
|
|
520
|
+
for (const iface of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.InterfaceDeclaration)) {
|
|
521
|
+
if (!ts_morph_1.Node.isModuleBlock(iface.getParent()))
|
|
522
|
+
continue;
|
|
523
|
+
const described = this.describeIndexedNode(iface);
|
|
524
|
+
if (!described)
|
|
525
|
+
continue;
|
|
526
|
+
push(iface, iface.getNameNode(), described.name, 'interface', described.container, false, true);
|
|
527
|
+
}
|
|
528
|
+
for (const ta of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.TypeAliasDeclaration)) {
|
|
529
|
+
if (!ts_morph_1.Node.isModuleBlock(ta.getParent()))
|
|
530
|
+
continue;
|
|
531
|
+
const described = this.describeIndexedNode(ta);
|
|
532
|
+
if (!described)
|
|
533
|
+
continue;
|
|
534
|
+
push(ta, ta.getNameNode(), described.name, 'type', described.container, false, true);
|
|
535
|
+
}
|
|
536
|
+
for (const stmt of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableStatement)) {
|
|
537
|
+
if (!ts_morph_1.Node.isModuleBlock(stmt.getParent()))
|
|
538
|
+
continue;
|
|
539
|
+
for (const decl of stmt.getDeclarations()) {
|
|
540
|
+
const described = this.describeIndexedNode(decl);
|
|
541
|
+
if (!described)
|
|
542
|
+
continue;
|
|
543
|
+
push(decl, decl.getNameNode(), described.name, 'variable', described.container, false, true);
|
|
397
544
|
}
|
|
398
545
|
}
|
|
399
|
-
return
|
|
546
|
+
return candidates;
|
|
547
|
+
}
|
|
548
|
+
/** Chunk-ownership ranges from every indexed declaration (top-level symbols +
|
|
549
|
+
* members) except `method_signature`. Each range spans the declaration node's
|
|
550
|
+
* own first-to-last line — decorator-inclusive, comment-exclusive, and (for a
|
|
551
|
+
* variable) the declarator alone, never the whole statement. */
|
|
552
|
+
buildOwnerRanges(candidates) {
|
|
553
|
+
return candidates
|
|
554
|
+
.filter(c => c.entry.kind !== 'method_signature')
|
|
555
|
+
.map(c => ({
|
|
556
|
+
name: c.entry.name,
|
|
557
|
+
container: c.entry.container,
|
|
558
|
+
line: c.declNode.getStartLineNumber(),
|
|
559
|
+
endLine: c.declNode.getEndLineNumber(),
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
// ── private: shared container resolution ────────────────────────────────────
|
|
563
|
+
static isValidSegment(s) {
|
|
564
|
+
return VALID_SEGMENT.test(s);
|
|
565
|
+
}
|
|
566
|
+
static memberName(nameNode) {
|
|
567
|
+
if (!nameNode)
|
|
568
|
+
return null;
|
|
569
|
+
if (ts_morph_1.Node.isPrivateIdentifier(nameNode))
|
|
570
|
+
return nameNode.getText();
|
|
571
|
+
if (ts_morph_1.Node.isIdentifier(nameNode))
|
|
572
|
+
return nameNode.getText();
|
|
573
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
574
|
+
return nameNode.getLiteralValue();
|
|
575
|
+
if (ts_morph_1.Node.isNumericLiteral(nameNode))
|
|
576
|
+
return nameNode.getText();
|
|
577
|
+
if (ts_morph_1.Node.isComputedPropertyName(nameNode))
|
|
578
|
+
return nameNode.getText().replace(/\s+/g, '');
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
static unwrapExpr(node) {
|
|
582
|
+
let current = node;
|
|
583
|
+
while (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current)) {
|
|
584
|
+
current = current.getExpression();
|
|
585
|
+
}
|
|
586
|
+
return current;
|
|
587
|
+
}
|
|
588
|
+
static unwrapParent(node) {
|
|
589
|
+
let current = node.getParent();
|
|
590
|
+
while (current && (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current))) {
|
|
591
|
+
current = current.getParent();
|
|
592
|
+
}
|
|
593
|
+
return current;
|
|
594
|
+
}
|
|
595
|
+
isModuleOrNamespaceLevelVariable(varDecl) {
|
|
596
|
+
// VariableDeclaration -> VariableDeclarationList -> VariableStatement -> (SourceFile | ModuleBlock)
|
|
597
|
+
const stmt = varDecl.getParent()?.getParent();
|
|
598
|
+
const stmtParent = stmt?.getParent();
|
|
599
|
+
return ts_morph_1.Node.isSourceFile(stmtParent) || ts_morph_1.Node.isModuleBlock(stmtParent);
|
|
600
|
+
}
|
|
601
|
+
classExpressionRoot(classExpr) {
|
|
602
|
+
const parent = AstParserService.unwrapParent(classExpr);
|
|
603
|
+
if (!parent)
|
|
604
|
+
return null;
|
|
605
|
+
if (ts_morph_1.Node.isVariableDeclaration(parent)) {
|
|
606
|
+
const name = parent.getName();
|
|
607
|
+
return name ? { name, namingParent: parent } : null;
|
|
608
|
+
}
|
|
609
|
+
if (ts_morph_1.Node.isExportAssignment(parent) && !parent.isExportEquals()) {
|
|
610
|
+
return { name: 'default', namingParent: parent };
|
|
611
|
+
}
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
objectLiteralRootValid(obj) {
|
|
615
|
+
const parent = AstParserService.unwrapParent(obj);
|
|
616
|
+
if (!parent)
|
|
617
|
+
return false;
|
|
618
|
+
if (ts_morph_1.Node.isPropertyAssignment(parent))
|
|
619
|
+
return true;
|
|
620
|
+
if (ts_morph_1.Node.isVariableDeclaration(parent))
|
|
621
|
+
return this.isModuleOrNamespaceLevelVariable(parent);
|
|
622
|
+
if (ts_morph_1.Node.isExportAssignment(parent) && !parent.isExportEquals())
|
|
623
|
+
return true;
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
isTopLevelOrNamespaceDirect(node) {
|
|
627
|
+
return ts_morph_1.Node.isSourceFile(node) || ts_morph_1.Node.isModuleBlock(node);
|
|
628
|
+
}
|
|
629
|
+
/** One step of the ancestor walk used by `containerSegments`: does `node`
|
|
630
|
+
* contribute a named segment, pass through transparently, or disqualify
|
|
631
|
+
* (exclude) everything nested inside it? */
|
|
632
|
+
walkStep(node) {
|
|
633
|
+
if (ts_morph_1.Node.isModuleDeclaration(node)) {
|
|
634
|
+
if (node.compilerNode.flags & ts_morph_1.NodeFlags.GlobalAugmentation)
|
|
635
|
+
return { kind: 'exclude' };
|
|
636
|
+
const nameNode = node.getNameNode();
|
|
637
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
638
|
+
return { kind: 'exclude' };
|
|
639
|
+
return { kind: 'segment', text: nameNode.getText() };
|
|
640
|
+
}
|
|
641
|
+
if (ts_morph_1.Node.isClassDeclaration(node)) {
|
|
642
|
+
const name = node.getName();
|
|
643
|
+
if (name)
|
|
644
|
+
return { kind: 'segment', text: name };
|
|
645
|
+
if (node.isDefaultExport())
|
|
646
|
+
return { kind: 'segment', text: 'default' };
|
|
647
|
+
return { kind: 'exclude' };
|
|
648
|
+
}
|
|
649
|
+
if (ts_morph_1.Node.isClassExpression(node)) {
|
|
650
|
+
const root = this.classExpressionRoot(node);
|
|
651
|
+
if (!root)
|
|
652
|
+
return { kind: 'exclude' };
|
|
653
|
+
return { kind: 'segment', text: root.name, skipTo: root.namingParent };
|
|
654
|
+
}
|
|
655
|
+
if (ts_morph_1.Node.isFunctionDeclaration(node)) {
|
|
656
|
+
const name = node.getName();
|
|
657
|
+
return name ? { kind: 'segment', text: name } : { kind: 'transparent' };
|
|
658
|
+
}
|
|
659
|
+
if (ts_morph_1.Node.isMethodDeclaration(node) || ts_morph_1.Node.isGetAccessorDeclaration(node) || ts_morph_1.Node.isSetAccessorDeclaration(node)) {
|
|
660
|
+
const name = AstParserService.memberName(node.getNameNode());
|
|
661
|
+
return name !== null ? { kind: 'segment', text: name } : { kind: 'exclude' };
|
|
662
|
+
}
|
|
663
|
+
if (ts_morph_1.Node.isConstructorDeclaration(node)) {
|
|
664
|
+
return { kind: 'segment', text: 'constructor' };
|
|
665
|
+
}
|
|
666
|
+
if (ts_morph_1.Node.isPropertyDeclaration(node) || ts_morph_1.Node.isPropertyAssignment(node)) {
|
|
667
|
+
const name = AstParserService.memberName(node.getNameNode());
|
|
668
|
+
return name !== null ? { kind: 'segment', text: name } : { kind: 'exclude' };
|
|
669
|
+
}
|
|
670
|
+
if (ts_morph_1.Node.isInterfaceDeclaration(node)) {
|
|
671
|
+
return { kind: 'segment', text: node.getName() };
|
|
672
|
+
}
|
|
673
|
+
if (ts_morph_1.Node.isTypeAliasDeclaration(node)) {
|
|
674
|
+
return { kind: 'segment', text: node.getName() };
|
|
675
|
+
}
|
|
676
|
+
if (ts_morph_1.Node.isVariableDeclaration(node)) {
|
|
677
|
+
return { kind: 'segment', text: node.getName() };
|
|
678
|
+
}
|
|
679
|
+
if (ts_morph_1.Node.isObjectLiteralExpression(node)) {
|
|
680
|
+
return this.objectLiteralRootValid(node) ? { kind: 'transparent' } : { kind: 'exclude' };
|
|
681
|
+
}
|
|
682
|
+
return { kind: 'transparent' };
|
|
683
|
+
}
|
|
684
|
+
/** The dotted container path of `node`'s enclosing named scopes, outermost
|
|
685
|
+
* first, or null when any contributing ancestor is invalid (a computed /
|
|
686
|
+
* string-literal segment, a string-named module, `declare global`, or an
|
|
687
|
+
* unrooted class expression / object literal). */
|
|
688
|
+
containerSegments(node) {
|
|
689
|
+
const segments = [];
|
|
690
|
+
let current = node.getParent();
|
|
691
|
+
while (current) {
|
|
692
|
+
const step = this.walkStep(current);
|
|
693
|
+
if (step.kind === 'exclude')
|
|
694
|
+
return null;
|
|
695
|
+
if (step.kind === 'segment') {
|
|
696
|
+
if (!AstParserService.isValidSegment(step.text))
|
|
697
|
+
return null;
|
|
698
|
+
segments.unshift(step.text);
|
|
699
|
+
current = step.skipTo ? step.skipTo.getParent() : current.getParent();
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
current = current.getParent();
|
|
703
|
+
}
|
|
704
|
+
return segments;
|
|
705
|
+
}
|
|
706
|
+
/** If `node` is a declaration this service indexes (top-level symbol, member,
|
|
707
|
+
* or collected class), its own name + container. Otherwise null. Shared by
|
|
708
|
+
* member extraction, ownership ranges and the call graph so all three name a
|
|
709
|
+
* declaration identically. */
|
|
710
|
+
describeIndexedNode(node) {
|
|
711
|
+
if (ts_morph_1.Node.isClassDeclaration(node)) {
|
|
712
|
+
const name = node.getName() ?? (node.isDefaultExport() ? 'default' : null);
|
|
713
|
+
if (name === null)
|
|
714
|
+
return null;
|
|
715
|
+
const container = this.containerSegments(node);
|
|
716
|
+
return container === null ? null : { name, container };
|
|
717
|
+
}
|
|
718
|
+
if (ts_morph_1.Node.isClassExpression(node)) {
|
|
719
|
+
const root = this.classExpressionRoot(node);
|
|
720
|
+
if (!root)
|
|
721
|
+
return null;
|
|
722
|
+
const container = this.containerSegments(root.namingParent);
|
|
723
|
+
return container === null ? null : { name: root.name, container };
|
|
724
|
+
}
|
|
725
|
+
if (ts_morph_1.Node.isFunctionDeclaration(node)) {
|
|
726
|
+
const name = node.getName();
|
|
727
|
+
if (!name || !this.isTopLevelOrNamespaceDirect(node.getParent()))
|
|
728
|
+
return null;
|
|
729
|
+
const container = this.containerSegments(node);
|
|
730
|
+
return container === null ? null : { name, container };
|
|
731
|
+
}
|
|
732
|
+
if (ts_morph_1.Node.isMethodDeclaration(node) || ts_morph_1.Node.isGetAccessorDeclaration(node) || ts_morph_1.Node.isSetAccessorDeclaration(node)) {
|
|
733
|
+
const name = AstParserService.memberName(node.getNameNode());
|
|
734
|
+
if (name === null)
|
|
735
|
+
return null;
|
|
736
|
+
const container = this.containerSegments(node);
|
|
737
|
+
return container === null ? null : { name, container };
|
|
738
|
+
}
|
|
739
|
+
if (ts_morph_1.Node.isConstructorDeclaration(node)) {
|
|
740
|
+
const container = this.containerSegments(node);
|
|
741
|
+
return container === null ? null : { name: 'constructor', container };
|
|
742
|
+
}
|
|
743
|
+
if (ts_morph_1.Node.isPropertyDeclaration(node) || ts_morph_1.Node.isPropertyAssignment(node)) {
|
|
744
|
+
const init = node.getInitializer();
|
|
745
|
+
if (!init)
|
|
746
|
+
return null;
|
|
747
|
+
const unwrapped = AstParserService.unwrapExpr(init);
|
|
748
|
+
if (!(ts_morph_1.Node.isArrowFunction(unwrapped) || ts_morph_1.Node.isFunctionExpression(unwrapped)))
|
|
749
|
+
return null;
|
|
750
|
+
const name = AstParserService.memberName(node.getNameNode());
|
|
751
|
+
if (name === null)
|
|
752
|
+
return null;
|
|
753
|
+
const container = this.containerSegments(node);
|
|
754
|
+
return container === null ? null : { name, container };
|
|
755
|
+
}
|
|
756
|
+
if (ts_morph_1.Node.isInterfaceDeclaration(node)) {
|
|
757
|
+
if (!this.isTopLevelOrNamespaceDirect(node.getParent()))
|
|
758
|
+
return null;
|
|
759
|
+
const container = this.containerSegments(node);
|
|
760
|
+
return container === null ? null : { name: node.getName(), container };
|
|
761
|
+
}
|
|
762
|
+
if (ts_morph_1.Node.isTypeAliasDeclaration(node)) {
|
|
763
|
+
if (!this.isTopLevelOrNamespaceDirect(node.getParent()))
|
|
764
|
+
return null;
|
|
765
|
+
const container = this.containerSegments(node);
|
|
766
|
+
return container === null ? null : { name: node.getName(), container };
|
|
767
|
+
}
|
|
768
|
+
if (ts_morph_1.Node.isVariableDeclaration(node)) {
|
|
769
|
+
if (!this.isModuleOrNamespaceLevelVariable(node))
|
|
770
|
+
return null;
|
|
771
|
+
const container = this.containerSegments(node);
|
|
772
|
+
return container === null ? null : { name: node.getName(), container };
|
|
773
|
+
}
|
|
774
|
+
if (ts_morph_1.Node.isMethodSignature(node) || ts_morph_1.Node.isPropertySignature(node)) {
|
|
775
|
+
if (ts_morph_1.Node.isPropertySignature(node)) {
|
|
776
|
+
const typeNode = node.getTypeNode();
|
|
777
|
+
if (!typeNode || !ts_morph_1.Node.isFunctionTypeNode(typeNode))
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
const parent = node.getParent();
|
|
781
|
+
let validParent = false;
|
|
782
|
+
if (ts_morph_1.Node.isInterfaceDeclaration(parent)) {
|
|
783
|
+
validParent = true;
|
|
784
|
+
}
|
|
785
|
+
else if (ts_morph_1.Node.isTypeLiteral(parent)) {
|
|
786
|
+
const aliasParent = parent.getParent();
|
|
787
|
+
validParent = ts_morph_1.Node.isTypeAliasDeclaration(aliasParent) && aliasParent.getTypeNode() === parent;
|
|
788
|
+
}
|
|
789
|
+
if (!validParent)
|
|
790
|
+
return null;
|
|
791
|
+
const name = AstParserService.memberName(node.getNameNode());
|
|
792
|
+
if (name === null)
|
|
793
|
+
return null;
|
|
794
|
+
const container = this.containerSegments(node);
|
|
795
|
+
return container === null ? null : { name, container };
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
qualifiedName(described) {
|
|
800
|
+
return described.container.length ? `${described.container.join('.')}.${described.name}` : described.name;
|
|
801
|
+
}
|
|
802
|
+
/** The qualified name of the nearest ancestor of `node` that is itself an
|
|
803
|
+
* indexed declaration (method, accessor, constructor, function property,
|
|
804
|
+
* namespace/top-level function, or module-/namespace-level variable). */
|
|
805
|
+
findFromSymbol(node) {
|
|
806
|
+
let current = node.getParent();
|
|
807
|
+
while (current) {
|
|
808
|
+
const described = this.describeIndexedNode(current);
|
|
809
|
+
if (described)
|
|
810
|
+
return this.qualifiedName(described);
|
|
811
|
+
current = current.getParent();
|
|
812
|
+
}
|
|
813
|
+
return null;
|
|
400
814
|
}
|
|
401
815
|
// ── private: call graph extraction ────────────────────────────────────────
|
|
402
816
|
extractCalls(sourceFile, filePath, serviceName) {
|
|
@@ -405,7 +819,7 @@ class AstParserService {
|
|
|
405
819
|
const expr = callExpr.getExpression();
|
|
406
820
|
let calleeName = null;
|
|
407
821
|
let calleeFile = null;
|
|
408
|
-
let
|
|
822
|
+
let calleeDecl;
|
|
409
823
|
try {
|
|
410
824
|
if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
|
|
411
825
|
calleeName = expr.getName();
|
|
@@ -414,7 +828,7 @@ class AstParserService {
|
|
|
414
828
|
const declPath = decl?.getSourceFile().getFilePath();
|
|
415
829
|
if (declPath && !declPath.includes('/node_modules/')) {
|
|
416
830
|
calleeFile = declPath;
|
|
417
|
-
|
|
831
|
+
calleeDecl = decl;
|
|
418
832
|
}
|
|
419
833
|
}
|
|
420
834
|
else if (ts_morph_1.Node.isIdentifier(expr)) {
|
|
@@ -430,7 +844,7 @@ class AstParserService {
|
|
|
430
844
|
const declPath = decl?.getSourceFile().getFilePath();
|
|
431
845
|
if (declPath && !declPath.includes('/node_modules/')) {
|
|
432
846
|
calleeFile = declPath;
|
|
433
|
-
|
|
847
|
+
calleeDecl = decl;
|
|
434
848
|
}
|
|
435
849
|
}
|
|
436
850
|
}
|
|
@@ -440,7 +854,9 @@ class AstParserService {
|
|
|
440
854
|
}
|
|
441
855
|
if (!calleeName || !calleeFile)
|
|
442
856
|
continue;
|
|
443
|
-
const
|
|
857
|
+
const described = calleeDecl ? this.describeIndexedNode(calleeDecl) : null;
|
|
858
|
+
const calleeSymbol = described ? this.qualifiedName(described) : null;
|
|
859
|
+
const fromSymbol = this.findFromSymbol(callExpr);
|
|
444
860
|
const pos = sourceFile.compilerNode
|
|
445
861
|
.getLineAndCharacterOfPosition(callExpr.getStart());
|
|
446
862
|
calls.push({
|
|
@@ -456,22 +872,5 @@ class AstParserService {
|
|
|
456
872
|
}
|
|
457
873
|
return calls;
|
|
458
874
|
}
|
|
459
|
-
getContainerName(node) {
|
|
460
|
-
let current = node.getParent();
|
|
461
|
-
while (current) {
|
|
462
|
-
if (ts_morph_1.Node.isMethodDeclaration(current) || ts_morph_1.Node.isFunctionDeclaration(current)) {
|
|
463
|
-
return current.getName() ?? null;
|
|
464
|
-
}
|
|
465
|
-
if (ts_morph_1.Node.isArrowFunction(current) || ts_morph_1.Node.isFunctionExpression(current)) {
|
|
466
|
-
const parent = current.getParent();
|
|
467
|
-
if (ts_morph_1.Node.isVariableDeclaration(parent))
|
|
468
|
-
return parent.getName();
|
|
469
|
-
if (ts_morph_1.Node.isPropertyDeclaration(parent))
|
|
470
|
-
return parent.getName();
|
|
471
|
-
}
|
|
472
|
-
current = current.getParent();
|
|
473
|
-
}
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
875
|
}
|
|
477
876
|
exports.AstParserService = AstParserService;
|