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.
@@ -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
- try {
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 edges = this.extractEdges(sourceFile, filePath, serviceName);
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', { filePath, edges: edges.length, symbols: symbols.length, calls: calls.length, ms: Date.now() - t0 });
133
- return { edges, symbols, calls, ownerRanges };
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: symbol extraction ─────────────────────────────────────────────
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 symbols = [];
313
- const sig = (text, keyword) => {
314
- const found = text.split('\n').find(l => keyword.test(l));
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 } = nameNode ? getPos(nameNode) : { line: cls.getStartLineNumber(), col: 1 };
328
- symbols.push({
329
- project: serviceName, filePath, name, kind: 'class',
330
- signature: sig(cls.getText(), /\bclass\s/),
331
- line, endLine: cls.getEndLineNumber(), col,
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 } = nameNode ? getPos(nameNode) : { line: fn.getStartLineNumber(), col: 1 };
340
- symbols.push({
341
- project: serviceName, filePath, name, kind: 'function',
342
- signature: sig(fn.getText(), /\bfunction\s/),
343
- line, endLine: fn.getEndLineNumber(), col,
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 { line, col } = getPos(iface.getNameNode());
348
- symbols.push({
349
- project: serviceName, filePath, name: iface.getName(), kind: 'interface',
350
- signature: sig(iface.getText(), /\binterface\s/),
351
- line, endLine: iface.getEndLineNumber(), col,
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 { line, col } = getPos(ta.getNameNode());
356
- symbols.push({
357
- project: serviceName, filePath, name: ta.getName(), kind: 'type',
358
- signature: sig(ta.getText(), /\btype\s/),
359
- line, endLine: ta.getEndLineNumber(), col,
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 { line, col } = getPos(decl.getNameNode());
365
- symbols.push({
366
- project: serviceName, filePath, name: decl.getName(), kind: 'variable',
367
- signature: sig(stmt.getText(), /\b(const|let|var)\s/),
368
- line, endLine: stmt.getEndLineNumber(), col,
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 symbols;
421
+ return candidates;
373
422
  }
374
- /**
375
- * Method / constructor / accessor line ranges with their enclosing class, for
376
- * owner-path resolution only. Deliberately NOT fed into `code_symbol` (methods
377
- * stay grep-based in find_symbol), so this is separate from extractSymbols.
378
- */
379
- extractOwnerRanges(sourceFile) {
380
- const ranges = [];
381
- for (const cls of sourceFile.getClasses()) {
382
- const container = cls.getName();
383
- if (!container)
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 members = [
386
- ...cls.getMethods().map(n => ({ name: n.getName(), node: n })),
387
- ...cls.getGetAccessors().map(n => ({ name: n.getName(), node: n })),
388
- ...cls.getSetAccessors().map(n => ({ name: n.getName(), node: n })),
389
- ...cls.getConstructors().map(n => ({ name: 'constructor', node: n })),
390
- ];
391
- for (const { name, node } of members) {
392
- ranges.push({
393
- name, container,
394
- line: node.getStartLineNumber(),
395
- endLine: node.getEndLineNumber(),
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 ranges;
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 calleeSymbol = null;
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
- calleeSymbol = calleeName;
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
- calleeSymbol = calleeName;
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 fromSymbol = this.getContainerName(callExpr);
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;