codesearch-mcp 0.2.3-alpha → 0.4.0-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.
Files changed (317) hide show
  1. package/CHANGELOG.md +88 -5
  2. package/LICENSE +661 -21
  3. package/README.md +76 -40
  4. package/dist/app.js +17 -38
  5. package/dist/src/ast/CallExtractor.d.ts +8 -0
  6. package/dist/src/ast/CallExtractor.js +68 -0
  7. package/dist/src/ast/GraphqlNameExtractor.d.ts +22 -0
  8. package/dist/src/ast/GraphqlNameExtractor.js +240 -0
  9. package/dist/src/ast/ImportEdgeExtractor.d.ts +11 -0
  10. package/dist/src/ast/ImportEdgeExtractor.js +129 -0
  11. package/dist/src/ast/IndexedNodeResolver.d.ts +14 -0
  12. package/dist/src/ast/IndexedNodeResolver.js +217 -0
  13. package/dist/src/ast/MemberExtractor.d.ts +8 -0
  14. package/dist/src/ast/MemberExtractor.js +129 -0
  15. package/dist/src/ast/TopLevelSymbolExtractor.d.ts +5 -0
  16. package/dist/src/ast/TopLevelSymbolExtractor.js +86 -0
  17. package/dist/src/ast/graphql-grammar.d.ts +24 -0
  18. package/dist/src/ast/graphql-grammar.js +93 -0
  19. package/dist/src/ast/node-helpers.d.ts +11 -0
  20. package/dist/src/ast/node-helpers.js +55 -0
  21. package/dist/src/chunking/AstChunker.d.ts +12 -0
  22. package/dist/src/chunking/AstChunker.js +30 -0
  23. package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
  24. package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
  25. package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
  26. package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
  27. package/dist/src/chunking/FragmentMerger.d.ts +11 -0
  28. package/dist/src/chunking/FragmentMerger.js +61 -0
  29. package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
  30. package/dist/src/chunking/ImportOnlyFilter.js +14 -0
  31. package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
  32. package/dist/src/chunking/LangchainTextSplitter.js +133 -0
  33. package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
  34. package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
  35. package/dist/src/chunking/RunPacker.d.ts +12 -0
  36. package/dist/src/chunking/RunPacker.js +84 -0
  37. package/dist/src/chunking/create-chunker.d.ts +3 -0
  38. package/dist/src/chunking/create-chunker.js +15 -0
  39. package/dist/src/chunking/interfaces.d.ts +27 -0
  40. package/dist/src/commands/ConfigCommand.js +1 -9
  41. package/dist/src/commands/EvalCommand.d.ts +10 -5
  42. package/dist/src/commands/EvalCommand.js +60 -149
  43. package/dist/src/commands/IndexCommand.d.ts +10 -4
  44. package/dist/src/commands/IndexCommand.js +52 -146
  45. package/dist/src/commands/McpCommand.d.ts +6 -3
  46. package/dist/src/commands/McpCommand.js +19 -20
  47. package/dist/src/commands/WebCommand.d.ts +9 -3
  48. package/dist/src/commands/WebCommand.js +17 -11
  49. package/dist/src/commands/eval/baseline-comparison.d.ts +6 -0
  50. package/dist/src/commands/eval/baseline-comparison.js +54 -0
  51. package/dist/src/commands/eval/eval-args.d.ts +6 -0
  52. package/dist/src/commands/eval/eval-args.js +71 -0
  53. package/dist/src/commands/eval/eval-corpora.d.ts +6 -0
  54. package/dist/src/commands/eval/eval-corpora.js +76 -0
  55. package/dist/src/commands/eval/eval-metadata.d.ts +17 -0
  56. package/dist/src/commands/eval/eval-metadata.js +75 -0
  57. package/dist/src/commands/eval/index-inspector.d.ts +3 -0
  58. package/dist/src/commands/eval/index-inspector.js +21 -0
  59. package/dist/src/commands/eval/staged-search.d.ts +3 -0
  60. package/dist/src/commands/eval/staged-search.js +20 -0
  61. package/dist/src/commands/eval/suite-loader.d.ts +8 -0
  62. package/dist/src/commands/eval/suite-loader.js +52 -0
  63. package/dist/src/commands/index/backend-probe.d.ts +2 -0
  64. package/dist/src/commands/index/backend-probe.js +15 -0
  65. package/dist/src/commands/index/embedding-setup.d.ts +3 -0
  66. package/dist/src/commands/index/embedding-setup.js +16 -0
  67. package/dist/src/commands/index/index-summary.d.ts +2 -0
  68. package/dist/src/commands/index/index-summary.js +22 -0
  69. package/dist/src/commands/index/legacy-ollama-guard.d.ts +3 -0
  70. package/dist/src/commands/index/legacy-ollama-guard.js +23 -0
  71. package/dist/src/commands/index/prune-step.d.ts +3 -0
  72. package/dist/src/commands/index/prune-step.js +22 -0
  73. package/dist/src/commands/index/scan-targets.d.ts +8 -0
  74. package/dist/src/commands/index/scan-targets.js +107 -0
  75. package/dist/src/config-error.d.ts +0 -4
  76. package/dist/src/config-error.js +0 -4
  77. package/dist/src/config-store.d.ts +1 -55
  78. package/dist/src/config-store.js +11 -73
  79. package/dist/src/config.d.ts +9 -18
  80. package/dist/src/config.js +36 -35
  81. package/dist/src/constants.d.ts +17 -28
  82. package/dist/src/constants.js +35 -29
  83. package/dist/src/container.d.ts +10 -2
  84. package/dist/src/container.js +19 -8
  85. package/dist/src/db/drizzle-types.d.ts +7 -0
  86. package/dist/src/db/drizzle-types.js +2 -0
  87. package/dist/src/db/index.d.ts +1 -0
  88. package/dist/src/db/schema-steps.d.ts +8 -0
  89. package/dist/src/db/schema-steps.js +105 -0
  90. package/dist/src/db/schema.d.ts +17 -0
  91. package/dist/src/db/schema.js +1 -6
  92. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  93. package/dist/src/declarations/DeclarationTree.js +66 -0
  94. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  95. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  96. package/dist/src/declarations/SourceLines.d.ts +37 -0
  97. package/dist/src/declarations/SourceLines.js +415 -0
  98. package/dist/src/dependency-traversal.d.ts +0 -6
  99. package/dist/src/dependency-traversal.js +0 -6
  100. package/dist/src/enriched-prompt.d.ts +0 -6
  101. package/dist/src/enriched-prompt.js +0 -6
  102. package/dist/src/error-record.d.ts +12 -0
  103. package/dist/src/error-record.js +248 -0
  104. package/dist/src/eval-corpus.d.ts +16 -0
  105. package/dist/src/eval-corpus.js +169 -0
  106. package/dist/src/eval-results.d.ts +6 -3
  107. package/dist/src/eval-results.js +323 -26
  108. package/dist/src/eval-vector-index.d.ts +5 -0
  109. package/dist/src/eval-vector-index.js +60 -0
  110. package/dist/src/file-utils.d.ts +0 -5
  111. package/dist/src/file-utils.js +0 -5
  112. package/dist/src/graph.d.ts +0 -25
  113. package/dist/src/graph.js +0 -28
  114. package/dist/src/help.js +9 -1
  115. package/dist/src/hnsw-settings.d.ts +3 -0
  116. package/dist/src/hnsw-settings.js +24 -0
  117. package/dist/src/index-args.d.ts +13 -0
  118. package/dist/src/index-args.js +48 -0
  119. package/dist/src/indexing/AnalysisBatcher.d.ts +12 -0
  120. package/dist/src/indexing/AnalysisBatcher.js +28 -0
  121. package/dist/src/indexing/ChunkEmbedder.d.ts +24 -0
  122. package/dist/src/indexing/ChunkEmbedder.js +37 -0
  123. package/dist/src/indexing/ConsoleScanReporter.d.ts +10 -0
  124. package/dist/src/indexing/ConsoleScanReporter.js +73 -0
  125. package/dist/src/indexing/EmbeddingSchemaPreparer.d.ts +14 -0
  126. package/dist/src/indexing/EmbeddingSchemaPreparer.js +66 -0
  127. package/dist/src/indexing/FileIngestor.d.ts +22 -0
  128. package/dist/src/indexing/FileIngestor.js +116 -0
  129. package/dist/src/indexing/IndexStore.d.ts +24 -0
  130. package/dist/src/indexing/IndexStore.js +69 -0
  131. package/dist/src/indexing/StaleFilePruner.d.ts +8 -0
  132. package/dist/src/indexing/StaleFilePruner.js +55 -0
  133. package/dist/src/indexing/analysis-rows.d.ts +4 -0
  134. package/dist/src/indexing/analysis-rows.js +65 -0
  135. package/dist/src/indexing/project-notices.d.ts +2 -0
  136. package/dist/src/indexing/project-notices.js +63 -0
  137. package/dist/src/interfaces/IAstParser.d.ts +4 -40
  138. package/dist/src/interfaces/ICallGraph.d.ts +28 -0
  139. package/dist/src/interfaces/ICallGraph.js +2 -0
  140. package/dist/src/interfaces/IChunkSearch.d.ts +42 -0
  141. package/dist/src/interfaces/IChunkSearch.js +2 -0
  142. package/dist/src/interfaces/IChunker.d.ts +3 -12
  143. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  144. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  145. package/dist/src/interfaces/IDatabase.d.ts +8 -24
  146. package/dist/src/interfaces/IDependencyGraph.d.ts +24 -0
  147. package/dist/src/interfaces/IDependencyGraph.js +2 -0
  148. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  149. package/dist/src/interfaces/IIndexIdentityGuard.d.ts +3 -0
  150. package/dist/src/interfaces/IIndexIdentityGuard.js +2 -0
  151. package/dist/src/interfaces/IIndexedNodeResolver.d.ts +10 -0
  152. package/dist/src/interfaces/IIndexedNodeResolver.js +2 -0
  153. package/dist/src/interfaces/ILogger.d.ts +1 -11
  154. package/dist/src/interfaces/IMcpReadiness.d.ts +12 -0
  155. package/dist/src/interfaces/IMcpReadiness.js +2 -0
  156. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  157. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  158. package/dist/src/interfaces/IProjectCatalog.d.ts +13 -0
  159. package/dist/src/interfaces/IProjectCatalog.js +2 -0
  160. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  161. package/dist/src/interfaces/IProjectRoots.d.ts +3 -0
  162. package/dist/src/interfaces/IProjectRoots.js +2 -0
  163. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +153 -12
  164. package/dist/src/interfaces/IScanReporter.d.ts +13 -0
  165. package/dist/src/interfaces/IScanReporter.js +2 -0
  166. package/dist/src/interfaces/ISymbolLookup.d.ts +49 -0
  167. package/dist/src/interfaces/ISymbolLookup.js +2 -0
  168. package/dist/src/interfaces/IToolPaths.d.ts +62 -0
  169. package/dist/src/interfaces/IToolPaths.js +2 -0
  170. package/dist/src/interfaces/IWorkspaceFiles.d.ts +28 -0
  171. package/dist/src/interfaces/IWorkspaceFiles.js +2 -0
  172. package/dist/src/interfaces/SymbolKind.d.ts +1 -0
  173. package/dist/src/interfaces/SymbolKind.js +2 -0
  174. package/dist/src/interfaces/index.d.ts +12 -1
  175. package/dist/src/interrupt.js +0 -2
  176. package/dist/src/owner-path.d.ts +9 -10
  177. package/dist/src/owner-path.js +11 -23
  178. package/dist/src/project-scope.d.ts +1 -12
  179. package/dist/src/project-scope.js +5 -12
  180. package/dist/src/project-selector.d.ts +0 -5
  181. package/dist/src/project-selector.js +0 -11
  182. package/dist/src/retrieval-eval.d.ts +15 -6
  183. package/dist/src/retrieval-eval.js +388 -59
  184. package/dist/src/search/CallGraph.d.ts +9 -0
  185. package/dist/src/search/CallGraph.js +84 -0
  186. package/dist/src/search/ChunkSearch.d.ts +23 -0
  187. package/dist/src/search/ChunkSearch.js +115 -0
  188. package/dist/src/search/DependencyGraph.d.ts +19 -0
  189. package/dist/src/search/DependencyGraph.js +180 -0
  190. package/dist/src/search/IndexIdentityGuard.d.ts +11 -0
  191. package/dist/src/search/IndexIdentityGuard.js +37 -0
  192. package/dist/src/search/ProjectCatalog.d.ts +11 -0
  193. package/dist/src/search/ProjectCatalog.js +38 -0
  194. package/dist/src/search/SymbolLookup.d.ts +24 -0
  195. package/dist/src/search/SymbolLookup.js +97 -0
  196. package/dist/src/search/chunks/CandidateFetcher.d.ts +19 -0
  197. package/dist/src/search/chunks/CandidateFetcher.js +40 -0
  198. package/dist/src/search/chunks/IterativeScanGate.d.ts +10 -0
  199. package/dist/src/search/chunks/IterativeScanGate.js +42 -0
  200. package/dist/src/search/chunks/RerankStage.d.ts +15 -0
  201. package/dist/src/search/chunks/RerankStage.js +42 -0
  202. package/dist/src/search/chunks/search-metrics.d.ts +18 -0
  203. package/dist/src/search/chunks/search-metrics.js +32 -0
  204. package/dist/src/search/chunks/search-plan.d.ts +18 -0
  205. package/dist/src/search/chunks/search-plan.js +19 -0
  206. package/dist/src/search/create-search-services.d.ts +26 -0
  207. package/dist/src/search/create-search-services.js +29 -0
  208. package/dist/src/search/index-paths.d.ts +1 -0
  209. package/dist/src/search/index-paths.js +5 -0
  210. package/dist/src/search/result-shaping.d.ts +4 -0
  211. package/dist/src/search/result-shaping.js +19 -0
  212. package/dist/src/search/symbol-sql.d.ts +13 -0
  213. package/dist/src/search/symbol-sql.js +66 -0
  214. package/dist/src/search/symbols/SymbolGrepFallback.d.ts +19 -0
  215. package/dist/src/search/symbols/SymbolGrepFallback.js +106 -0
  216. package/dist/src/search/symbols/SymbolIndexQuery.d.ts +14 -0
  217. package/dist/src/search/symbols/SymbolIndexQuery.js +52 -0
  218. package/dist/src/search/symbols/SymbolTextSearch.d.ts +13 -0
  219. package/dist/src/search/symbols/SymbolTextSearch.js +84 -0
  220. package/dist/src/search/symbols/TargetColumnGate.d.ts +7 -0
  221. package/dist/src/search/symbols/TargetColumnGate.js +23 -0
  222. package/dist/src/search/symbols/find-symbol-constants.d.ts +4 -0
  223. package/dist/src/search/symbols/find-symbol-constants.js +8 -0
  224. package/dist/src/search/symbols/grep-args.d.ts +3 -0
  225. package/dist/src/search/symbols/grep-args.js +7 -0
  226. package/dist/src/search/symbols/grep-patterns.d.ts +4 -0
  227. package/dist/src/search/symbols/grep-patterns.js +60 -0
  228. package/dist/src/services/AstParserService.d.ts +10 -106
  229. package/dist/src/services/AstParserService.js +37 -1068
  230. package/dist/src/services/DatabaseService.d.ts +10 -17
  231. package/dist/src/services/DatabaseService.js +71 -115
  232. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  233. package/dist/src/services/EmbedderFactory.js +0 -5
  234. package/dist/src/services/IndexerService.d.ts +7 -26
  235. package/dist/src/services/IndexerService.js +38 -286
  236. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  237. package/dist/src/services/InfinityEmbedder.js +0 -15
  238. package/dist/src/services/LoggerService.d.ts +7 -5
  239. package/dist/src/services/LoggerService.js +37 -41
  240. package/dist/src/services/McpReadiness.d.ts +18 -0
  241. package/dist/src/services/McpReadiness.js +73 -0
  242. package/dist/src/services/McpServer.d.ts +50 -19
  243. package/dist/src/services/McpServer.js +132 -62
  244. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  245. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  246. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  247. package/dist/src/services/NxMonorepoResolver.js +5 -56
  248. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  249. package/dist/src/services/OpenAICompatibleEmbedder.js +0 -14
  250. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  251. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  252. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  253. package/dist/src/services/VoyageEmbedder.js +0 -6
  254. package/dist/src/services/WebServerService.d.ts +5 -56
  255. package/dist/src/services/WebServerService.js +25 -63
  256. package/dist/src/services/http-retry.d.ts +0 -7
  257. package/dist/src/services/http-retry.js +1 -10
  258. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  259. package/dist/src/services/infinity-provisioning.js +5 -88
  260. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  261. package/dist/src/services/legacy-ollama-index.js +0 -23
  262. package/dist/src/services/reranker.d.ts +0 -9
  263. package/dist/src/services/reranker.js +46 -12
  264. package/dist/src/setup.d.ts +24 -24
  265. package/dist/src/setup.js +205 -103
  266. package/dist/src/symbol-name.d.ts +1 -12
  267. package/dist/src/symbol-name.js +16 -25
  268. package/dist/src/symbol-rows.d.ts +0 -14
  269. package/dist/src/symbol-rows.js +0 -14
  270. package/dist/src/tool-call-log.d.ts +2 -0
  271. package/dist/src/tool-call-log.js +24 -0
  272. package/dist/src/tools/FindSymbolTool.d.ts +17 -7
  273. package/dist/src/tools/FindSymbolTool.js +54 -35
  274. package/dist/src/tools/GetCallersTool.d.ts +16 -8
  275. package/dist/src/tools/GetCallersTool.js +65 -35
  276. package/dist/src/tools/GetDependenciesTool.d.ts +16 -8
  277. package/dist/src/tools/GetDependenciesTool.js +36 -29
  278. package/dist/src/tools/GetDependentsTool.d.ts +20 -8
  279. package/dist/src/tools/GetDependentsTool.js +48 -34
  280. package/dist/src/tools/ListFilesTool.d.ts +16 -8
  281. package/dist/src/tools/ListFilesTool.js +20 -26
  282. package/dist/src/tools/ListProjectsTool.d.ts +11 -7
  283. package/dist/src/tools/ListProjectsTool.js +13 -11
  284. package/dist/src/tools/McpTool.d.ts +17 -0
  285. package/dist/src/tools/McpTool.js +28 -0
  286. package/dist/src/tools/ReadFileRangeTool.d.ts +16 -8
  287. package/dist/src/tools/ReadFileRangeTool.js +23 -25
  288. package/dist/src/tools/ReadFileTool.d.ts +15 -8
  289. package/dist/src/tools/ReadFileTool.js +19 -24
  290. package/dist/src/tools/SearchCodebaseTool.d.ts +22 -10
  291. package/dist/src/tools/SearchCodebaseTool.js +71 -50
  292. package/dist/src/tools/format-helpers.d.ts +9 -9
  293. package/dist/src/tools/format-helpers.js +146 -28
  294. package/dist/src/tools/tool-args.d.ts +38 -0
  295. package/dist/src/tools/tool-args.js +95 -0
  296. package/dist/src/web/dashboard.d.ts +0 -8
  297. package/dist/src/web/dashboard.js +7 -10
  298. package/dist/src/workspace/GrepRunner.d.ts +15 -0
  299. package/dist/src/workspace/GrepRunner.js +104 -0
  300. package/dist/src/workspace/ProjectRoots.d.ts +22 -0
  301. package/dist/src/workspace/ProjectRoots.js +102 -0
  302. package/dist/src/workspace/ToolPaths.d.ts +31 -0
  303. package/dist/src/workspace/ToolPaths.js +312 -0
  304. package/dist/src/workspace/WorkspaceFiles.d.ts +19 -0
  305. package/dist/src/workspace/WorkspaceFiles.js +285 -0
  306. package/dist/src/workspace/fs-access.d.ts +9 -0
  307. package/dist/src/workspace/fs-access.js +47 -0
  308. package/dist/src/workspace/path-helpers.d.ts +3 -0
  309. package/dist/src/workspace/path-helpers.js +43 -0
  310. package/package.json +5 -4
  311. package/dist/src/interfaces/ISearcher.d.ts +0 -35
  312. package/dist/src/services/AstChunker.d.ts +0 -42
  313. package/dist/src/services/AstChunker.js +0 -158
  314. package/dist/src/services/SearcherService.d.ts +0 -342
  315. package/dist/src/services/SearcherService.js +0 -1199
  316. package/docs/web-dashboard.md +0 -206
  317. /package/dist/src/{interfaces/ISearcher.js → chunking/interfaces.js} +0 -0
@@ -37,29 +37,36 @@ exports.AstParserService = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const fs = __importStar(require("fs"));
39
39
  const ts_morph_1 = require("ts-morph");
40
+ const DeclarationTreeBuilder_1 = require("../declarations/DeclarationTreeBuilder");
41
+ const DeclarationTree_1 = require("../declarations/DeclarationTree");
40
42
  const LoggerService_1 = require("./LoggerService");
41
- const GRAPHQL_MODULE = '@nestjs/graphql';
42
- const GRAPHQL_TYPE_DECORATORS = new Set(['ObjectType', 'InputType', 'InterfaceType']);
43
- const GRAPHQL_OPERATION_KIND = {
44
- Query: 'graphql_query', Mutation: 'graphql_mutation', Subscription: 'graphql_subscription',
45
- };
46
- const GRAPHQL_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
43
+ const IndexedNodeResolver_1 = require("../ast/IndexedNodeResolver");
44
+ const ImportEdgeExtractor_1 = require("../ast/ImportEdgeExtractor");
45
+ const TopLevelSymbolExtractor_1 = require("../ast/TopLevelSymbolExtractor");
46
+ const MemberExtractor_1 = require("../ast/MemberExtractor");
47
+ const CallExtractor_1 = require("../ast/CallExtractor");
48
+ const GraphqlNameExtractor_1 = require("../ast/GraphqlNameExtractor");
47
49
  const FALLBACK_OPTS = {
48
50
  skipAddingFilesFromTsConfig: true,
49
51
  skipFileDependencyResolution: false,
50
52
  compilerOptions: { allowJs: true, noEmit: true },
51
53
  };
52
54
  const MORPH_CACHE_MAX = 10;
53
- const VALID_SEGMENT = /^#?[\w$]+$/;
54
55
  class AstParserService {
55
56
  logger;
56
- deps;
57
57
  morphProjects = new Map();
58
+ declarationBuilder = new DeclarationTreeBuilder_1.DeclarationTreeBuilder();
59
+ resolver = new IndexedNodeResolver_1.IndexedNodeResolver();
60
+ edges;
61
+ topLevel = new TopLevelSymbolExtractor_1.TopLevelSymbolExtractor();
62
+ members = new MemberExtractor_1.MemberExtractor(this.resolver);
63
+ calls = new CallExtractor_1.CallExtractor(this.resolver);
64
+ graphql;
58
65
  constructor(logger = LoggerService_1.LoggerService.silent(), deps = {}) {
59
66
  this.logger = logger;
60
- this.deps = deps;
67
+ this.edges = new ImportEdgeExtractor_1.ImportEdgeExtractor(logger);
68
+ this.graphql = new GraphqlNameExtractor_1.GraphqlNameExtractor(this.resolver, logger, deps.resolveDeclaration);
61
69
  }
62
- // ── public interface ────────────────────────────────────────────────────────
63
70
  async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
64
71
  const t0 = Date.now();
65
72
  const skipDependencyResolution = opts.skipDependencyResolution ?? false;
@@ -76,7 +83,7 @@ class AstParserService {
76
83
  morphProject.addSourceFileAtPath(filePath);
77
84
  }
78
85
  catch (err) {
79
- this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
86
+ this.logger.warn('AstParser: failed to add source file', { filePath, err });
80
87
  analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
81
88
  }
82
89
  }
@@ -84,7 +91,6 @@ class AstParserService {
84
91
  const filePath = sourceFile.getFilePath();
85
92
  analysisMap.set(filePath, this.analyzeSourceFile(sourceFile, filePath, serviceName));
86
93
  }
87
- // Release source file memory before returning
88
94
  for (const sourceFile of morphProject.getSourceFiles()) {
89
95
  morphProject.removeSourceFile(sourceFile);
90
96
  }
@@ -120,7 +126,7 @@ class AstParserService {
120
126
  sourceFile = morphProject.addSourceFileAtPath(filePath);
121
127
  }
122
128
  catch (err) {
123
- this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
129
+ this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, err, ms: Date.now() - t0 });
124
130
  return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
125
131
  }
126
132
  const result = this.analyzeSourceFile(sourceFile, filePath, serviceName);
@@ -130,64 +136,46 @@ class AstParserService {
130
136
  });
131
137
  return result;
132
138
  }
133
- // Kept for test compatibility (extract-import-edges.test.ts).
134
139
  async extractImportEdges(filePath, serviceName, projectRoot) {
135
140
  const { edges } = await this.analyzeFile(filePath, serviceName, projectRoot);
136
141
  return edges;
137
142
  }
138
143
  analyzeSourceFile(sourceFile, filePath, serviceName) {
139
144
  try {
140
- const edges = this.extractEdges(sourceFile, filePath, serviceName);
141
- const symbolCandidates = this.extractSymbols(sourceFile, filePath, serviceName);
145
+ const edges = this.edges.extractEdges(sourceFile, filePath, serviceName);
146
+ const symbolCandidates = this.topLevel.extractSymbols(sourceFile, filePath, serviceName);
142
147
  const symbols = symbolCandidates.map(c => c.entry);
143
- // Member extraction (classes anywhere, class/interface/object-literal
144
- // members, namespace-level declarations) is isolated in its own try: a
145
- // bug there must never change a file's top-level symbols, chunks or
146
- // embedding prompt (see the `symbol-index-members` retrieval-invariance
147
- // requirement).
148
148
  let memberCandidates = [];
149
149
  try {
150
- memberCandidates = this.extractMembers(sourceFile, filePath, serviceName);
150
+ memberCandidates = this.members.extractMembers(sourceFile, filePath, serviceName);
151
151
  }
152
152
  catch (err) {
153
- this.logger.warn('AstParser: member extraction failed', { filePath, reason: err.message });
153
+ this.logger.warn('AstParser: member extraction failed', { filePath, err });
154
154
  memberCandidates = [];
155
155
  }
156
156
  const members = memberCandidates.map(c => c.entry);
157
- const calls = this.extractCalls(sourceFile, filePath, serviceName);
158
- const ownerRanges = this.buildOwnerRanges([...symbolCandidates, ...memberCandidates]);
159
- // GraphQL-named rows are isolated like members: a bug here must never
160
- // change a file's symbols, members, chunks, ownerPath or call graph.
161
- // Runs after member extraction (decision 6 of `graphql-symbols`) — a
162
- // GraphQL row is written only when the decorated declaration's own row
163
- // was written to code_symbol.
157
+ const calls = this.calls.extractCalls(sourceFile, filePath, serviceName);
158
+ const declarations = this.buildDeclarations(sourceFile, filePath, symbolCandidates, memberCandidates);
164
159
  let graphqlNames = [];
165
160
  try {
166
- graphqlNames = this.extractGraphqlNames(sourceFile, filePath, serviceName, [...symbolCandidates, ...memberCandidates]);
161
+ graphqlNames = this.graphql.extractGraphqlNames(sourceFile, filePath, serviceName, [...symbolCandidates, ...memberCandidates]);
167
162
  }
168
163
  catch (err) {
169
- this.logger.warn('AstParser: GraphQL extraction failed', { filePath, reason: err.message });
164
+ this.logger.warn('AstParser: GraphQL extraction failed', { filePath, err });
170
165
  graphqlNames = [];
171
166
  }
172
- return { edges, symbols, members, calls, ownerRanges, graphqlNames };
167
+ return { edges, symbols, members, calls, declarations, graphqlNames };
173
168
  }
174
169
  catch (err) {
175
- this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
170
+ this.logger.warn('AstParser: failed to analyze file', { filePath, err });
176
171
  return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
177
172
  }
178
173
  }
179
- // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
180
174
  createMorphProject(projectRoot, skipFileDependencyResolution = false, rootTsConfigPath) {
181
175
  const t0 = Date.now();
182
- // Determine which tsconfig to use:
183
- // 1. Project-local tsconfig.json
184
- // 2. Root tsconfig (e.g. tsconfig.base.json for Nx monorepos)
185
- // 3. Fallback options
186
176
  const localTsconfigPath = path.join(projectRoot, 'tsconfig.json');
187
177
  const hasLocalTsconfig = fs.existsSync(localTsconfigPath);
188
178
  const hasRootTsconfig = rootTsConfigPath ? fs.existsSync(rootTsConfigPath) : false;
189
- // For monorepos: prefer the root tsconfig.base.json which contains
190
- // workspace-wide path aliases (e.g. @scope/* → libs/*/src)
191
179
  const effectiveTsconfig = hasRootTsconfig ? rootTsConfigPath : (hasLocalTsconfig ? localTsconfigPath : null);
192
180
  this.logger.debug('AstParser: tsconfig lookup', {
193
181
  projectRoot,
@@ -219,7 +207,7 @@ class AstParserService {
219
207
  catch (err) {
220
208
  this.logger.warn('AstParser: tsconfig load failed, using fallback', {
221
209
  tsconfigPath: effectiveTsconfig ?? localTsconfigPath,
222
- reason: err.message,
210
+ err,
223
211
  ms: Date.now() - t0,
224
212
  });
225
213
  project = new ts_morph_1.Project(FALLBACK_OPTS);
@@ -228,7 +216,6 @@ class AstParserService {
228
216
  }
229
217
  getMorphProject(projectRoot) {
230
218
  if (this.morphProjects.has(projectRoot)) {
231
- // Move to end = most recently used
232
219
  const cached = this.morphProjects.get(projectRoot);
233
220
  this.morphProjects.delete(projectRoot);
234
221
  this.morphProjects.set(projectRoot, cached);
@@ -236,7 +223,6 @@ class AstParserService {
236
223
  return cached;
237
224
  }
238
225
  if (this.morphProjects.size >= MORPH_CACHE_MAX) {
239
- // Evict least recently used (first entry in insertion-order Map)
240
226
  const lruKey = this.morphProjects.keys().next().value;
241
227
  this.morphProjects.delete(lruKey);
242
228
  this.logger.debug('AstParser: evicted LRU project', { lruKey });
@@ -246,1036 +232,19 @@ class AstParserService {
246
232
  this.morphProjects.set(projectRoot, project);
247
233
  return project;
248
234
  }
249
- // ── private: edge extraction (imports + re-exports) ────────────────────────
250
- extractEdges(sourceFile, filePath, serviceName) {
251
- const edges = [];
252
- const usageMap = this.buildUsageMap(sourceFile);
253
- for (const decl of sourceFile.getImportDeclarations()) {
254
- const namedImports = decl.getNamedImports();
255
- const defaultImport = decl.getDefaultImport();
256
- const namespaceImport = decl.getNamespaceImport();
257
- const hasClause = Boolean(defaultImport || namespaceImport || namedImports.length > 0);
258
- const resolved = decl.getModuleSpecifierSourceFile();
259
- let toFile = resolved ? resolved.getFilePath() : null;
260
- // A bindingless specifier never resolves through ts-morph's bound symbol
261
- // (nothing is bound), even for a real project file — fall back to
262
- // TypeScript's own resolver and map the result back onto the project.
263
- if (!toFile && !hasClause) {
264
- toFile = this.resolveSideEffectModuleSpecifier(decl, sourceFile, filePath);
265
- }
266
- if (!toFile) {
267
- const specifier = decl.getModuleSpecifierValue();
268
- const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
269
- if (specifier.startsWith('.') && isCodeImport) {
270
- this.logger.debug('AstParser: unresolved import', { specifier, file: filePath });
271
- }
272
- continue;
273
- }
274
- if (toFile.includes('/node_modules/'))
275
- continue;
276
- if (!hasClause) {
277
- edges.push({
278
- project: serviceName, fromFile: filePath, fromSymbol: null,
279
- toFile, toSymbol: null, depType: 'import-side-effect',
280
- });
281
- continue;
282
- }
283
- if (defaultImport) {
284
- const fromSymbol = this.resolveFromSymbol(defaultImport.getText(), usageMap);
285
- edges.push({
286
- project: serviceName, fromFile: filePath, fromSymbol,
287
- toFile, toSymbol: null, depType: 'import-default',
288
- });
289
- }
290
- if (namespaceImport) {
291
- const fromSymbol = this.resolveFromSymbol(namespaceImport.getText(), usageMap);
292
- edges.push({
293
- project: serviceName, fromFile: filePath, fromSymbol,
294
- toFile, toSymbol: null, depType: 'import-namespace',
295
- });
296
- }
297
- for (const named of namedImports) {
298
- const localName = named.getAliasNode()?.getText() ?? named.getName();
299
- const fromSymbol = this.resolveFromSymbol(localName, usageMap);
300
- edges.push({
301
- project: serviceName, fromFile: filePath, fromSymbol,
302
- toFile, toSymbol: named.getName(), depType: 'import',
303
- });
304
- }
305
- }
306
- for (const decl of sourceFile.getExportDeclarations()) {
307
- const resolved = decl.getModuleSpecifierSourceFile();
308
- if (!resolved)
309
- continue;
310
- const toFile = resolved.getFilePath();
311
- if (toFile.includes('/node_modules/'))
312
- continue;
313
- const namedExports = decl.getNamedExports();
314
- if (namedExports.length > 0) {
315
- for (const named of namedExports) {
316
- edges.push({
317
- project: serviceName, fromFile: filePath, fromSymbol: null,
318
- toFile, toSymbol: named.getName(), depType: 'reexport',
319
- });
320
- }
321
- }
322
- else {
323
- edges.push({
324
- project: serviceName, fromFile: filePath, fromSymbol: null,
325
- toFile, toSymbol: null, depType: 'reexport-namespace',
326
- });
327
- }
328
- }
329
- return edges;
330
- }
331
- /** Manual fallback for a bindingless import specifier (design.md decision 2) —
332
- * ts-morph's `getModuleSpecifierSourceFile()` is `null` for these even when
333
- * the target is a real project file, since nothing is bound to resolve through. */
334
- resolveSideEffectModuleSpecifier(decl, sourceFile, filePath) {
335
- const specifier = decl.getModuleSpecifierValue();
336
- const compilerOptions = sourceFile.getProject().getCompilerOptions();
337
- const result = ts_morph_1.ts.resolveModuleName(specifier, filePath, compilerOptions, ts_morph_1.ts.sys);
338
- const resolvedFileName = result.resolvedModule?.resolvedFileName;
339
- if (!resolvedFileName)
340
- return null;
341
- return sourceFile.getProject().getSourceFile(resolvedFileName)?.getFilePath() ?? null;
342
- }
343
- resolveFromSymbol(localName, usageMap) {
344
- const users = usageMap.get(localName);
345
- return users?.length === 1 ? users[0] : null;
346
- }
347
- buildUsageMap(sourceFile) {
348
- const map = new Map();
349
- const containers = [
350
- ...sourceFile.getClasses().flatMap(n => n.getName() ? [{ name: n.getName(), node: n }] : []),
351
- ...sourceFile.getFunctions().flatMap(n => n.getName() ? [{ name: n.getName(), node: n }] : []),
352
- // include top-level variable declarations so arrow function exports are tracked for fromSymbol
353
- ...sourceFile.getVariableStatements()
354
- .flatMap(stmt => stmt.getDeclarations())
355
- .flatMap(n => {
356
- const name = n.getName();
357
- return name ? [{ name, node: n }] : [];
358
- }),
359
- ];
360
- for (const { name, node } of containers) {
361
- for (const id of node.getDescendantsOfKind(ts_morph_1.SyntaxKind.Identifier)) {
362
- const text = id.getText();
363
- const existing = map.get(text);
364
- if (existing) {
365
- if (!existing.includes(name))
366
- existing.push(name);
367
- }
368
- else {
369
- map.set(text, [name]);
370
- }
371
- }
372
- }
373
- return map;
374
- }
375
- // ── private: position / signature helpers ───────────────────────────────────
376
- getPos(sourceFile, node) {
377
- const { line, character } = sourceFile.compilerNode
378
- .getLineAndCharacterOfPosition(node.getStart());
379
- return { line: line + 1, col: character + 1 };
380
- }
381
- /** Trimmed text of the source line containing `node`'s start — the signature
382
- * rule for every kind (name node for members, the position node for
383
- * constructors/anonymous default classes). */
384
- lineTextAt(sourceLines, sourceFile, node) {
385
- const { line } = this.getPos(sourceFile, node);
386
- return (sourceLines[line - 1] ?? node.getText().split('\n')[0]).trim();
387
- }
388
- // ── private: top-level symbol extraction (feeds the chunker + prompt) ──────
389
- extractSymbols(sourceFile, filePath, serviceName) {
390
- const candidates = [];
391
- const sourceLines = sourceFile.getFullText().split('\n');
392
- const lineAt = (node) => this.lineTextAt(sourceLines, sourceFile, node);
393
- for (const cls of sourceFile.getClasses()) {
394
- const name = cls.getName();
395
- if (!name)
396
- continue;
397
- const nameNode = cls.getNameNode() ?? cls;
398
- const { line, col } = this.getPos(sourceFile, nameNode);
399
- candidates.push({
400
- declNode: cls,
401
- entry: {
402
- project: serviceName, filePath, name, kind: 'class',
403
- signature: lineAt(nameNode),
404
- line, endLine: cls.getEndLineNumber(), col,
405
- container: null, isStatic: false,
406
- },
407
- });
408
- }
409
- for (const fn of sourceFile.getFunctions()) {
410
- const name = fn.getName();
411
- if (!name)
412
- continue;
413
- const nameNode = fn.getNameNode() ?? fn;
414
- const { line, col } = this.getPos(sourceFile, nameNode);
415
- candidates.push({
416
- declNode: fn,
417
- entry: {
418
- project: serviceName, filePath, name, kind: 'function',
419
- signature: lineAt(nameNode),
420
- line, endLine: fn.getEndLineNumber(), col,
421
- container: null, isStatic: false, hasBody: fn.getBody() !== undefined,
422
- },
423
- });
424
- }
425
- for (const iface of sourceFile.getInterfaces()) {
426
- const nameNode = iface.getNameNode();
427
- const { line, col } = this.getPos(sourceFile, nameNode);
428
- candidates.push({
429
- declNode: iface,
430
- entry: {
431
- project: serviceName, filePath, name: iface.getName(), kind: 'interface',
432
- signature: lineAt(nameNode),
433
- line, endLine: iface.getEndLineNumber(), col,
434
- container: null, isStatic: false,
435
- },
436
- });
437
- }
438
- for (const ta of sourceFile.getTypeAliases()) {
439
- const nameNode = ta.getNameNode();
440
- const { line, col } = this.getPos(sourceFile, nameNode);
441
- candidates.push({
442
- declNode: ta,
443
- entry: {
444
- project: serviceName, filePath, name: ta.getName(), kind: 'type',
445
- signature: lineAt(nameNode),
446
- line, endLine: ta.getEndLineNumber(), col,
447
- container: null, isStatic: false,
448
- },
449
- });
450
- }
451
- for (const stmt of sourceFile.getVariableStatements()) {
452
- for (const decl of stmt.getDeclarations()) {
453
- const nameNode = decl.getNameNode();
454
- const { line, col } = this.getPos(sourceFile, nameNode);
455
- candidates.push({
456
- // Owner ranges use the declarator's own span, not the statement's —
457
- // each declarator of `const a = 1, b = () => {…}` owns only its lines.
458
- declNode: decl,
459
- entry: {
460
- project: serviceName, filePath, name: decl.getName(), kind: 'variable',
461
- signature: lineAt(nameNode),
462
- line, endLine: stmt.getEndLineNumber(), col,
463
- container: null, isStatic: false,
464
- },
465
- });
466
- }
467
- }
468
- return candidates;
469
- }
470
- // ── private: member extraction (classes anywhere, members, namespace body) ─
471
- extractMembers(sourceFile, filePath, serviceName) {
472
- const candidates = [];
473
- const sourceLines = sourceFile.getFullText().split('\n');
474
- const lineAt = (node) => this.lineTextAt(sourceLines, sourceFile, node);
475
- const push = (declNode, posNode, name, kind, container, isStatic, hasBody) => {
476
- const { line, col } = this.getPos(sourceFile, posNode);
477
- candidates.push({
478
- declNode,
479
- entry: {
480
- project: serviceName, filePath, name, kind,
481
- signature: lineAt(posNode),
482
- line, endLine: declNode.getEndLineNumber(), col,
483
- container: container.length ? container.join('.') : null,
484
- isStatic, hasBody,
485
- },
486
- });
487
- };
488
- // Classes anywhere in the file — nested, expression, anonymous default.
489
- for (const cls of [
490
- ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration),
491
- ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassExpression),
492
- ]) {
493
- // Named top-level classes are already handled by extractSymbols; an
494
- // anonymous top-level class (`export default class {}`) is not — it
495
- // must still be collected here.
496
- if (ts_morph_1.Node.isClassDeclaration(cls) && cls.getName() && ts_morph_1.Node.isSourceFile(cls.getParent()))
497
- continue;
498
- const described = this.describeIndexedNode(cls);
499
- if (!described)
500
- continue;
501
- const posNode = (ts_morph_1.Node.isClassDeclaration(cls) ? cls.getNameNode() : undefined) ?? cls;
502
- push(cls, posNode, described.name, 'class', described.container, false, true);
503
- }
504
- // Class / object-literal methods, accessors, constructors, function properties.
505
- for (const m of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
506
- const described = this.describeIndexedNode(m);
507
- if (!described)
508
- continue;
509
- push(m, m.getNameNode(), described.name, 'method', described.container, m.isStatic(), m.getBody() !== undefined);
510
- }
511
- for (const a of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.GetAccessor)) {
512
- const described = this.describeIndexedNode(a);
513
- if (!described)
514
- continue;
515
- push(a, a.getNameNode(), described.name, 'get_accessor', described.container, a.isStatic(), true);
516
- }
517
- for (const a of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.SetAccessor)) {
518
- const described = this.describeIndexedNode(a);
519
- if (!described)
520
- continue;
521
- push(a, a.getNameNode(), described.name, 'set_accessor', described.container, a.isStatic(), true);
522
- }
523
- for (const c of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.Constructor)) {
524
- const described = this.describeIndexedNode(c);
525
- if (!described)
526
- continue;
527
- const kw = c.getFirstChildByKind(ts_morph_1.SyntaxKind.ConstructorKeyword) ?? c;
528
- push(c, kw, described.name, 'constructor', described.container, false, c.getBody() !== undefined);
529
- }
530
- for (const p of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyDeclaration)) {
531
- const described = this.describeIndexedNode(p);
532
- if (!described)
533
- continue;
534
- push(p, p.getNameNode(), described.name, 'function_property', described.container, p.isStatic(), true);
535
- }
536
- for (const p of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
537
- const described = this.describeIndexedNode(p);
538
- if (!described)
539
- continue;
540
- push(p, p.getNameNode(), described.name, 'function_property', described.container, false, true);
541
- }
542
- // Interface / type-literal method and function-typed property signatures.
543
- for (const s of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodSignature)) {
544
- const described = this.describeIndexedNode(s);
545
- if (!described)
546
- continue;
547
- push(s, s.getNameNode(), described.name, 'method_signature', described.container, false, false);
548
- }
549
- for (const s of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertySignature)) {
550
- const described = this.describeIndexedNode(s);
551
- if (!described)
552
- continue;
553
- push(s, s.getNameNode(), described.name, 'method_signature', described.container, false, false);
554
- }
555
- // Declarations directly in a namespace body.
556
- for (const fn of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.FunctionDeclaration)) {
557
- if (!ts_morph_1.Node.isModuleBlock(fn.getParent()))
558
- continue;
559
- const described = this.describeIndexedNode(fn);
560
- if (!described)
561
- continue;
562
- const nameNode = fn.getNameNode();
563
- if (!nameNode)
564
- continue;
565
- push(fn, nameNode, described.name, 'function', described.container, false, fn.getBody() !== undefined);
566
- }
567
- for (const iface of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.InterfaceDeclaration)) {
568
- if (!ts_morph_1.Node.isModuleBlock(iface.getParent()))
569
- continue;
570
- const described = this.describeIndexedNode(iface);
571
- if (!described)
572
- continue;
573
- push(iface, iface.getNameNode(), described.name, 'interface', described.container, false, true);
574
- }
575
- for (const ta of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.TypeAliasDeclaration)) {
576
- if (!ts_morph_1.Node.isModuleBlock(ta.getParent()))
577
- continue;
578
- const described = this.describeIndexedNode(ta);
579
- if (!described)
580
- continue;
581
- push(ta, ta.getNameNode(), described.name, 'type', described.container, false, true);
582
- }
583
- for (const stmt of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableStatement)) {
584
- if (!ts_morph_1.Node.isModuleBlock(stmt.getParent()))
585
- continue;
586
- for (const decl of stmt.getDeclarations()) {
587
- const described = this.describeIndexedNode(decl);
588
- if (!described)
589
- continue;
590
- push(decl, decl.getNameNode(), described.name, 'variable', described.container, false, true);
591
- }
592
- }
593
- return candidates;
594
- }
595
- /** Chunk-ownership ranges from every indexed declaration (top-level symbols +
596
- * members) except `method_signature`. Each range spans the declaration node's
597
- * own first-to-last line — decorator-inclusive, comment-exclusive, and (for a
598
- * variable) the declarator alone, never the whole statement. */
599
- buildOwnerRanges(candidates) {
600
- return candidates
601
- .filter(c => c.entry.kind !== 'method_signature')
602
- .map(c => ({
603
- name: c.entry.name,
604
- container: c.entry.container,
605
- line: c.declNode.getStartLineNumber(),
606
- endLine: c.declNode.getEndLineNumber(),
607
- }));
608
- }
609
- // ── private: GraphQL-named rows (graphql-symbols) ───────────────────────────
610
- /** Local name → the name exported by `@nestjs/graphql`, for every named import
611
- * of that module specifier (aliases followed). Recognition is import-based,
612
- * not type-checker-based, so `skipFileDependencyResolution` batches agree. */
613
- buildGraphqlImportMap(sourceFile) {
614
- const map = new Map();
615
- for (const decl of sourceFile.getImportDeclarations()) {
616
- if (decl.getModuleSpecifierValue() !== GRAPHQL_MODULE)
617
- continue;
618
- for (const named of decl.getNamedImports()) {
619
- const exportedName = named.getName();
620
- const localName = named.getAliasNode()?.getText() ?? exportedName;
621
- map.set(localName, exportedName);
622
- }
623
- }
624
- return map;
625
- }
626
- /** The identifier naming a decorator's expression (`@Foo` or the callee of `@Foo(...)`). */
627
- static decoratorIdentifier(dec) {
628
- const expr = dec.getExpression();
629
- if (ts_morph_1.Node.isCallExpression(expr)) {
630
- const callee = expr.getExpression();
631
- return ts_morph_1.Node.isIdentifier(callee) ? callee : undefined;
632
- }
633
- return ts_morph_1.Node.isIdentifier(expr) ? expr : undefined;
634
- }
635
- /** `dec` recognised against `importMap` — its exported `@nestjs/graphql` name and its call arguments (`[]` for a bare `@Foo`). */
636
- recognizedDecorator(dec, importMap) {
637
- const idNode = AstParserService.decoratorIdentifier(dec);
638
- if (!idNode)
639
- return null;
640
- const exported = importMap.get(idNode.getText());
641
- if (!exported)
642
- return null;
643
- const expr = dec.getExpression();
644
- const args = ts_morph_1.Node.isCallExpression(expr) ? expr.getArguments() : [];
645
- return { name: exported, args };
646
- }
647
- static literalStringValue(node) {
648
- if (ts_morph_1.Node.isStringLiteral(node) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node))
649
- return node.getLiteralValue();
650
- return null;
651
- }
652
- static checkGraphqlGrammar(name) {
653
- return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, reason: 'invalid-name', value: name };
654
- }
655
- /** `true` when any decorator argument is an (unwrapped) options object
656
- * literal with `isAbstract: true` — an abstract base type (`PaginatedType`
657
- * pattern) that NestJS never registers in the actual GraphQL schema. */
658
- static isAbstractGraphqlType(args) {
659
- return args.some((arg) => {
660
- if (!ts_morph_1.Node.isObjectLiteralExpression(arg))
661
- return false;
662
- const prop = arg.getProperties().find((p) => {
663
- if (!ts_morph_1.Node.isPropertyAssignment(p))
664
- return false;
665
- const nameNode = p.getNameNode();
666
- if (ts_morph_1.Node.isIdentifier(nameNode))
667
- return nameNode.getText() === 'isAbstract';
668
- if (ts_morph_1.Node.isStringLiteral(nameNode))
669
- return nameNode.getLiteralValue() === 'isAbstract';
670
- return false;
671
- });
672
- const init = prop?.getInitializer();
673
- return init !== undefined && ts_morph_1.Node.isTrueLiteral(init);
674
- });
675
- }
676
- /** `obj`'s own `name` property (`PropertyAssignment` or `ShorthandPropertyAssignment`), if any. */
677
- static findNameProperty(obj) {
678
- return obj.getProperties().find((p) => {
679
- if (!ts_morph_1.Node.isPropertyAssignment(p) && !ts_morph_1.Node.isShorthandPropertyAssignment(p))
680
- return false;
681
- const nameNode = p.getNameNode();
682
- if (ts_morph_1.Node.isIdentifier(nameNode))
683
- return nameNode.getText() === 'name';
684
- if (ts_morph_1.Node.isStringLiteral(nameNode))
685
- return nameNode.getLiteralValue() === 'name';
686
- return false;
687
- });
688
- }
689
- /** Strips parens/`as`/`satisfies`/non-null wrappers (any nesting) to find the
690
- * underlying node — used only to classify an argument's *shape* (type
691
- * function vs. options object vs. literal vs. other), never to read a name
692
- * value through the wrapper (decision 4 of `graphql-symbols` forbids that). */
693
- static peekThroughWrapper(node) {
694
- let current = node;
695
- let wrapped = false;
696
- while (true) {
697
- if (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current) || ts_morph_1.Node.isNonNullExpression(current)) {
698
- current = current.getExpression();
699
- wrapped = true;
700
- continue;
701
- }
702
- break;
703
- }
704
- return { inner: current, wrapped };
705
- }
706
- /**
707
- * GraphQL name from a decorator's arguments (design decision 4 of
708
- * `graphql-symbols`) — syntactic, no wrapper looked through at any step.
709
- * `allowOptionsName` gates step 2 (the `name` option — `Query`/`Mutation`/
710
- * `Subscription`/`ResolveField` only); `fallbackName` is the TS name used at
711
- * step 3.
712
- */
713
- deriveGraphqlName(args, allowOptionsName, fallbackName) {
714
- if (args.length > 0) {
715
- const first = args[0];
716
- const { inner, wrapped } = AstParserService.peekThroughWrapper(first);
717
- const literal = AstParserService.literalStringValue(inner);
718
- if (literal !== null) {
719
- // A wrapped literal is still "a wrapped ... argument", an anything-else
720
- // node (decision 4) — reading its value through the wrapper is exactly
721
- // what "no wrapper is looked through" forbids.
722
- if (wrapped)
723
- return { ok: false, reason: 'non-literal-name' };
724
- return AstParserService.checkGraphqlGrammar(literal);
725
- }
726
- const isTypeFnOrOptions = ts_morph_1.Node.isArrowFunction(inner) || ts_morph_1.Node.isFunctionExpression(inner) || ts_morph_1.Node.isObjectLiteralExpression(inner);
727
- if (!isTypeFnOrOptions)
728
- return { ok: false, reason: 'non-literal-name' };
729
- // Type function / options object, wrapped or not — this is a routing
730
- // decision only (go to step 2/3); nothing is read from it here, so its
731
- // wrapper doesn't matter yet.
732
- }
733
- if (allowOptionsName) {
734
- for (const arg of args) {
735
- const { inner, wrapped } = AstParserService.peekThroughWrapper(arg);
736
- if (!ts_morph_1.Node.isObjectLiteralExpression(inner))
737
- continue;
738
- // A wrapped options object cannot be trusted without looking through
739
- // the wrapper, which decision 4 forbids — treat it as non-literal
740
- // rather than silently skipping to the next argument or to step 3.
741
- if (wrapped)
742
- return { ok: false, reason: 'non-literal-name' };
743
- const nameProp = AstParserService.findNameProperty(inner);
744
- if (!nameProp)
745
- continue;
746
- if (ts_morph_1.Node.isShorthandPropertyAssignment(nameProp))
747
- return { ok: false, reason: 'non-literal-name' };
748
- const init = nameProp.getInitializer();
749
- const value = init ? AstParserService.literalStringValue(init) : null;
750
- if (value !== null)
751
- return AstParserService.checkGraphqlGrammar(value);
752
- return { ok: false, reason: 'non-literal-name' };
753
- }
754
- }
755
- return AstParserService.checkGraphqlGrammar(fallbackName);
756
- }
757
- logGraphqlSkip(filePath, declaration, decorator, reason, value) {
758
- const fields = { filePath, declaration, decorator, reason };
759
- if (value !== undefined)
760
- fields.value = value;
761
- this.logger.debug('AstParser: graphql row skipped', fields);
762
- }
763
- /** `declNode`'s qualified TypeScript name for diagnostics, independent of
764
- * whether its own `code_symbol` row was actually written. */
765
- declarationNameFor(node, fallback) {
766
- const described = this.describeIndexedNode(node);
767
- return described ? this.qualifiedName(described) : fallback;
768
- }
769
- /** The identifier named by a `@Resolver` argument: directly, or as an arrow
770
- * function's expression body; or the sole identifier of a single-element
771
- * array literal in either position (list-returning resolver). */
772
- static extractResolverIdentifier(argNode) {
773
- let target = argNode;
774
- if (ts_morph_1.Node.isArrowFunction(target)) {
775
- const body = target.getBody();
776
- if (ts_morph_1.Node.isBlock(body))
777
- return null;
778
- target = body;
779
- }
780
- if (ts_morph_1.Node.isIdentifier(target))
781
- return target;
782
- if (ts_morph_1.Node.isArrayLiteralExpression(target)) {
783
- const elements = target.getElements();
784
- if (elements.length === 1 && ts_morph_1.Node.isIdentifier(elements[0]))
785
- return elements[0];
786
- }
787
- return null;
788
- }
789
- defaultResolveDeclaration(identifier) {
790
- const sym = identifier.getSymbol();
791
- if (!sym)
792
- return undefined;
793
- const firstDecl = sym.getDeclarations()?.[0];
794
- const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl) ? (sym.getAliasedSymbol() ?? sym) : sym;
795
- return resolved.getDeclarations()?.[0];
796
- }
797
- /** The GraphQL type name of a `@ResolveField`'s resolved parent class
798
- * (design decision 5, second half): its recognised `@ObjectType` (else
799
- * `@InterfaceType`) decorator, or its class name when it carries no
800
- * decorator spelled `ObjectType`/`InterfaceType` at all, recognised or not. */
801
- classGraphqlTypeName(declClass) {
802
- const declImportMap = this.buildGraphqlImportMap(declClass.getSourceFile());
803
- const decorators = declClass.getDecorators();
804
- const hasAnyTypeSpelledDecorator = decorators.some((d) => {
805
- const idNode = AstParserService.decoratorIdentifier(d);
806
- const text = idNode?.getText();
807
- return text === 'ObjectType' || text === 'InterfaceType';
808
- });
809
- const recognized = decorators
810
- .map(d => this.recognizedDecorator(d, declImportMap))
811
- .find(r => r?.name === 'ObjectType')
812
- ?? decorators.map(d => this.recognizedDecorator(d, declImportMap)).find(r => r?.name === 'InterfaceType');
813
- if (recognized) {
814
- const fallbackName = declClass.getName() ?? (declClass.isDefaultExport() ? 'default' : '');
815
- const nameResult = this.deriveGraphqlName(recognized.args, false, fallbackName);
816
- return nameResult.ok ? { ok: true, name: nameResult.name } : { ok: false };
817
- }
818
- // Carries a type-spelled decorator that isn't recognised (e.g. imported
819
- // through a barrel) — its GraphQL name is unreadable; the class-name
820
- // fallback never applies to it (it may differ from the unreadable name).
821
- if (hasAnyTypeSpelledDecorator)
822
- return { ok: false };
823
- const name = declClass.getName();
824
- if (!name)
825
- return { ok: false };
826
- return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, value: name };
827
- }
828
- /** The parent type of a `@ResolveField` — the enclosing class's recognised
829
- * `@Resolver` argument (design decision 5, first half). */
830
- resolveResolverParentType(cls, importMap) {
831
- const resolverDec = cls.getDecorators()
832
- .map(d => this.recognizedDecorator(d, importMap))
833
- .find(r => r?.name === 'Resolver');
834
- if (!resolverDec || resolverDec.args.length === 0)
835
- return { ok: false };
836
- const argNode = resolverDec.args[0];
837
- const literal = AstParserService.literalStringValue(argNode);
838
- if (literal !== null) {
839
- return GRAPHQL_NAME_RE.test(literal) ? { ok: true, name: literal } : { ok: false, value: literal };
840
- }
841
- const identifier = AstParserService.extractResolverIdentifier(argNode);
842
- if (!identifier)
843
- return { ok: false };
844
- const resolve = this.deps.resolveDeclaration ?? this.defaultResolveDeclaration.bind(this);
845
- const declaration = resolve(identifier);
846
- if (!declaration || !ts_morph_1.Node.isClassDeclaration(declaration))
847
- return { ok: false };
848
- return this.classGraphqlTypeName(declaration);
849
- }
850
- extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries) {
851
- const own = ownByDeclNode.get(cls);
852
- const declaration = this.declarationNameFor(cls, own?.entry.name ?? cls.getName() ?? 'default');
853
- const seenNames = new Set();
854
- for (const dec of cls.getDecorators()) {
855
- const rec = this.recognizedDecorator(dec, importMap);
856
- if (!rec || !GRAPHQL_TYPE_DECORATORS.has(rec.name))
857
- continue;
858
- if (AstParserService.isAbstractGraphqlType(rec.args)) {
859
- this.logGraphqlSkip(filePath, declaration, rec.name, 'abstract-type');
860
- continue;
861
- }
862
- if (!own) {
863
- this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
864
- continue;
865
- }
866
- const nameResult = this.deriveGraphqlName(rec.args, false, own.entry.name);
867
- if (!nameResult.ok) {
868
- this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
869
- continue;
870
- }
871
- if (seenNames.has(nameResult.name)) {
872
- this.logGraphqlSkip(filePath, declaration, rec.name, 'duplicate', own.entry.name);
873
- continue;
874
- }
875
- seenNames.add(nameResult.name);
876
- entries.push({
877
- project: serviceName, filePath, name: nameResult.name, kind: 'graphql_type',
878
- signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
879
- container: null, isStatic: false, target: declaration,
880
- });
881
- }
882
- }
883
- extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries) {
884
- if (!own) {
885
- this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'no-symbol-row');
886
- return;
887
- }
888
- const cls = method.getParent();
889
- if (!cls || !ts_morph_1.Node.isClassDeclaration(cls)) {
890
- this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
891
- return;
892
- }
893
- const parent = this.resolveResolverParentType(cls, importMap);
894
- if (!parent.ok) {
895
- this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent', parent.value);
896
- return;
897
- }
898
- const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
899
- if (!nameResult.ok) {
900
- this.logGraphqlSkip(filePath, declaration, 'ResolveField', nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
901
- return;
902
- }
903
- entries.push({
904
- project: serviceName, filePath, name: nameResult.name, kind: 'graphql_field',
905
- signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
906
- container: parent.name, isStatic: own.entry.isStatic, target: declaration,
907
- });
908
- }
909
- extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries) {
910
- const own = ownByDeclNode.get(method);
911
- const declaration = this.declarationNameFor(method, own?.entry.name ?? AstParserService.memberName(method.getNameNode()) ?? '<anonymous>');
912
- for (const dec of method.getDecorators()) {
913
- const rec = this.recognizedDecorator(dec, importMap);
914
- if (!rec)
915
- continue;
916
- if (rec.name === 'ResolveField') {
917
- // Isolated per field: a throw resolving this field's parent (e.g. the
918
- // injected test seam) degrades to "no row for that field" and never
919
- // trips the file-level catch in `analyzeSourceFile`.
920
- try {
921
- this.extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries);
922
- }
923
- catch (err) {
924
- this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
925
- }
926
- continue;
927
- }
928
- const kind = GRAPHQL_OPERATION_KIND[rec.name];
929
- if (!kind)
930
- continue; // recognised but not an operation decorator (wrong declaration kind) — ignored, no log
931
- if (!own) {
932
- this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
933
- continue;
934
- }
935
- const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
936
- if (!nameResult.ok) {
937
- this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
938
- continue;
939
- }
940
- entries.push({
941
- project: serviceName, filePath, name: nameResult.name, kind,
942
- signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
943
- container: rec.name, isStatic: own.entry.isStatic, target: declaration,
944
- });
945
- }
946
- }
947
- /** GraphQL-named rows for every recognised `@nestjs/graphql` decorator in
948
- * the file (`graphql-symbols`). `ownRows` is `symbols ∪ members` — the
949
- * declarations' own `code_symbol` candidates, checked so a GraphQL row is
950
- * never written without its decorated declaration's own row. */
951
- extractGraphqlNames(sourceFile, filePath, serviceName, ownRows) {
952
- const importMap = this.buildGraphqlImportMap(sourceFile);
953
- if (importMap.size === 0)
954
- return [];
955
- const ownByDeclNode = new Map();
956
- for (const c of ownRows)
957
- ownByDeclNode.set(c.declNode, c);
958
- const entries = [];
959
- for (const cls of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration)) {
960
- this.extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries);
961
- }
962
- for (const method of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
963
- this.extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries);
964
- }
965
- return entries;
966
- }
967
- // ── private: shared container resolution ────────────────────────────────────
968
- static isValidSegment(s) {
969
- return VALID_SEGMENT.test(s);
970
- }
971
- static memberName(nameNode) {
972
- if (!nameNode)
973
- return null;
974
- if (ts_morph_1.Node.isPrivateIdentifier(nameNode))
975
- return nameNode.getText();
976
- if (ts_morph_1.Node.isIdentifier(nameNode))
977
- return nameNode.getText();
978
- if (ts_morph_1.Node.isStringLiteral(nameNode))
979
- return nameNode.getLiteralValue();
980
- if (ts_morph_1.Node.isNumericLiteral(nameNode))
981
- return nameNode.getText();
982
- if (ts_morph_1.Node.isComputedPropertyName(nameNode))
983
- return nameNode.getText().replace(/\s+/g, '');
984
- return null;
985
- }
986
- static unwrapExpr(node) {
987
- let current = node;
988
- while (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current)) {
989
- current = current.getExpression();
990
- }
991
- return current;
992
- }
993
- static unwrapParent(node) {
994
- let current = node.getParent();
995
- while (current && (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current))) {
996
- current = current.getParent();
997
- }
998
- return current;
999
- }
1000
- isModuleOrNamespaceLevelVariable(varDecl) {
1001
- // VariableDeclaration -> VariableDeclarationList -> VariableStatement -> (SourceFile | ModuleBlock)
1002
- const stmt = varDecl.getParent()?.getParent();
1003
- const stmtParent = stmt?.getParent();
1004
- return ts_morph_1.Node.isSourceFile(stmtParent) || ts_morph_1.Node.isModuleBlock(stmtParent);
1005
- }
1006
- classExpressionRoot(classExpr) {
1007
- const parent = AstParserService.unwrapParent(classExpr);
1008
- if (!parent)
1009
- return null;
1010
- if (ts_morph_1.Node.isVariableDeclaration(parent)) {
1011
- const name = parent.getName();
1012
- return name ? { name, namingParent: parent } : null;
1013
- }
1014
- if (ts_morph_1.Node.isExportAssignment(parent) && !parent.isExportEquals()) {
1015
- return { name: 'default', namingParent: parent };
1016
- }
1017
- return null;
1018
- }
1019
- objectLiteralRootValid(obj) {
1020
- const parent = AstParserService.unwrapParent(obj);
1021
- if (!parent)
1022
- return false;
1023
- if (ts_morph_1.Node.isPropertyAssignment(parent))
1024
- return true;
1025
- if (ts_morph_1.Node.isVariableDeclaration(parent))
1026
- return this.isModuleOrNamespaceLevelVariable(parent);
1027
- if (ts_morph_1.Node.isExportAssignment(parent) && !parent.isExportEquals())
1028
- return true;
1029
- return false;
1030
- }
1031
- isTopLevelOrNamespaceDirect(node) {
1032
- return ts_morph_1.Node.isSourceFile(node) || ts_morph_1.Node.isModuleBlock(node);
1033
- }
1034
- /** One step of the ancestor walk used by `containerSegments`: does `node`
1035
- * contribute a named segment, pass through transparently, or disqualify
1036
- * (exclude) everything nested inside it? */
1037
- walkStep(node) {
1038
- if (ts_morph_1.Node.isModuleDeclaration(node)) {
1039
- if (node.compilerNode.flags & ts_morph_1.NodeFlags.GlobalAugmentation)
1040
- return { kind: 'exclude' };
1041
- const nameNode = node.getNameNode();
1042
- if (ts_morph_1.Node.isStringLiteral(nameNode))
1043
- return { kind: 'exclude' };
1044
- return { kind: 'segment', text: nameNode.getText() };
1045
- }
1046
- if (ts_morph_1.Node.isClassDeclaration(node)) {
1047
- const name = node.getName();
1048
- if (name)
1049
- return { kind: 'segment', text: name };
1050
- if (node.isDefaultExport())
1051
- return { kind: 'segment', text: 'default' };
1052
- return { kind: 'exclude' };
1053
- }
1054
- if (ts_morph_1.Node.isClassExpression(node)) {
1055
- const root = this.classExpressionRoot(node);
1056
- if (!root)
1057
- return { kind: 'exclude' };
1058
- return { kind: 'segment', text: root.name, skipTo: root.namingParent };
1059
- }
1060
- if (ts_morph_1.Node.isFunctionDeclaration(node)) {
1061
- const name = node.getName();
1062
- return name ? { kind: 'segment', text: name } : { kind: 'transparent' };
1063
- }
1064
- if (ts_morph_1.Node.isMethodDeclaration(node) || ts_morph_1.Node.isGetAccessorDeclaration(node) || ts_morph_1.Node.isSetAccessorDeclaration(node)) {
1065
- const name = AstParserService.memberName(node.getNameNode());
1066
- return name !== null ? { kind: 'segment', text: name } : { kind: 'exclude' };
1067
- }
1068
- if (ts_morph_1.Node.isConstructorDeclaration(node)) {
1069
- return { kind: 'segment', text: 'constructor' };
1070
- }
1071
- if (ts_morph_1.Node.isPropertyDeclaration(node) || ts_morph_1.Node.isPropertyAssignment(node)) {
1072
- const name = AstParserService.memberName(node.getNameNode());
1073
- return name !== null ? { kind: 'segment', text: name } : { kind: 'exclude' };
1074
- }
1075
- if (ts_morph_1.Node.isInterfaceDeclaration(node)) {
1076
- return { kind: 'segment', text: node.getName() };
1077
- }
1078
- if (ts_morph_1.Node.isTypeAliasDeclaration(node)) {
1079
- return { kind: 'segment', text: node.getName() };
1080
- }
1081
- if (ts_morph_1.Node.isVariableDeclaration(node)) {
1082
- return { kind: 'segment', text: node.getName() };
1083
- }
1084
- if (ts_morph_1.Node.isObjectLiteralExpression(node)) {
1085
- return this.objectLiteralRootValid(node) ? { kind: 'transparent' } : { kind: 'exclude' };
1086
- }
1087
- return { kind: 'transparent' };
1088
- }
1089
- /** The dotted container path of `node`'s enclosing named scopes, outermost
1090
- * first, or null when any contributing ancestor is invalid (a computed /
1091
- * string-literal segment, a string-named module, `declare global`, or an
1092
- * unrooted class expression / object literal). */
1093
- containerSegments(node) {
1094
- const segments = [];
1095
- let current = node.getParent();
1096
- while (current) {
1097
- const step = this.walkStep(current);
1098
- if (step.kind === 'exclude')
1099
- return null;
1100
- if (step.kind === 'segment') {
1101
- if (!AstParserService.isValidSegment(step.text))
1102
- return null;
1103
- segments.unshift(step.text);
1104
- current = step.skipTo ? step.skipTo.getParent() : current.getParent();
1105
- continue;
1106
- }
1107
- current = current.getParent();
1108
- }
1109
- return segments;
1110
- }
1111
- /** If `node` is a declaration this service indexes (top-level symbol, member,
1112
- * or collected class), its own name + container. Otherwise null. Shared by
1113
- * member extraction, ownership ranges and the call graph so all three name a
1114
- * declaration identically. */
1115
- describeIndexedNode(node) {
1116
- if (ts_morph_1.Node.isClassDeclaration(node)) {
1117
- const name = node.getName() ?? (node.isDefaultExport() ? 'default' : null);
1118
- if (name === null)
1119
- return null;
1120
- const container = this.containerSegments(node);
1121
- return container === null ? null : { name, container };
1122
- }
1123
- if (ts_morph_1.Node.isClassExpression(node)) {
1124
- const root = this.classExpressionRoot(node);
1125
- if (!root)
1126
- return null;
1127
- const container = this.containerSegments(root.namingParent);
1128
- return container === null ? null : { name: root.name, container };
1129
- }
1130
- if (ts_morph_1.Node.isFunctionDeclaration(node)) {
1131
- const name = node.getName();
1132
- if (!name || !this.isTopLevelOrNamespaceDirect(node.getParent()))
1133
- return null;
1134
- const container = this.containerSegments(node);
1135
- return container === null ? null : { name, container };
1136
- }
1137
- if (ts_morph_1.Node.isMethodDeclaration(node) || ts_morph_1.Node.isGetAccessorDeclaration(node) || ts_morph_1.Node.isSetAccessorDeclaration(node)) {
1138
- const name = AstParserService.memberName(node.getNameNode());
1139
- if (name === null)
1140
- return null;
1141
- const container = this.containerSegments(node);
1142
- return container === null ? null : { name, container };
1143
- }
1144
- if (ts_morph_1.Node.isConstructorDeclaration(node)) {
1145
- const container = this.containerSegments(node);
1146
- return container === null ? null : { name: 'constructor', container };
1147
- }
1148
- if (ts_morph_1.Node.isPropertyDeclaration(node) || ts_morph_1.Node.isPropertyAssignment(node)) {
1149
- const init = node.getInitializer();
1150
- if (!init)
1151
- return null;
1152
- const unwrapped = AstParserService.unwrapExpr(init);
1153
- if (!(ts_morph_1.Node.isArrowFunction(unwrapped) || ts_morph_1.Node.isFunctionExpression(unwrapped)))
1154
- return null;
1155
- const name = AstParserService.memberName(node.getNameNode());
1156
- if (name === null)
1157
- return null;
1158
- const container = this.containerSegments(node);
1159
- return container === null ? null : { name, container };
1160
- }
1161
- if (ts_morph_1.Node.isInterfaceDeclaration(node)) {
1162
- if (!this.isTopLevelOrNamespaceDirect(node.getParent()))
1163
- return null;
1164
- const container = this.containerSegments(node);
1165
- return container === null ? null : { name: node.getName(), container };
1166
- }
1167
- if (ts_morph_1.Node.isTypeAliasDeclaration(node)) {
1168
- if (!this.isTopLevelOrNamespaceDirect(node.getParent()))
1169
- return null;
1170
- const container = this.containerSegments(node);
1171
- return container === null ? null : { name: node.getName(), container };
1172
- }
1173
- if (ts_morph_1.Node.isVariableDeclaration(node)) {
1174
- if (!this.isModuleOrNamespaceLevelVariable(node))
1175
- return null;
1176
- const container = this.containerSegments(node);
1177
- return container === null ? null : { name: node.getName(), container };
1178
- }
1179
- if (ts_morph_1.Node.isMethodSignature(node) || ts_morph_1.Node.isPropertySignature(node)) {
1180
- if (ts_morph_1.Node.isPropertySignature(node)) {
1181
- const typeNode = node.getTypeNode();
1182
- if (!typeNode || !ts_morph_1.Node.isFunctionTypeNode(typeNode))
1183
- return null;
1184
- }
1185
- const parent = node.getParent();
1186
- let validParent = false;
1187
- if (ts_morph_1.Node.isInterfaceDeclaration(parent)) {
1188
- validParent = true;
1189
- }
1190
- else if (ts_morph_1.Node.isTypeLiteral(parent)) {
1191
- const aliasParent = parent.getParent();
1192
- validParent = ts_morph_1.Node.isTypeAliasDeclaration(aliasParent) && aliasParent.getTypeNode() === parent;
1193
- }
1194
- if (!validParent)
1195
- return null;
1196
- const name = AstParserService.memberName(node.getNameNode());
1197
- if (name === null)
1198
- return null;
1199
- const container = this.containerSegments(node);
1200
- return container === null ? null : { name, container };
1201
- }
1202
- return null;
1203
- }
1204
- qualifiedName(described) {
1205
- return described.container.length ? `${described.container.join('.')}.${described.name}` : described.name;
1206
- }
1207
- /** The qualified name of the nearest ancestor of `node` that is itself an
1208
- * indexed declaration (method, accessor, constructor, function property,
1209
- * namespace/top-level function, or module-/namespace-level variable). */
1210
- findFromSymbol(node) {
1211
- let current = node.getParent();
1212
- while (current) {
1213
- const described = this.describeIndexedNode(current);
1214
- if (described)
1215
- return this.qualifiedName(described);
1216
- current = current.getParent();
235
+ buildDeclarations(sourceFile, filePath, symbolCandidates, memberCandidates) {
236
+ try {
237
+ return this.declarationBuilder.build(sourceFile, [...symbolCandidates, ...memberCandidates]);
1217
238
  }
1218
- return null;
1219
- }
1220
- // ── private: call graph extraction ────────────────────────────────────────
1221
- extractCalls(sourceFile, filePath, serviceName) {
1222
- const calls = [];
1223
- for (const callExpr of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
1224
- const expr = callExpr.getExpression();
1225
- let calleeName = null;
1226
- let calleeFile = null;
1227
- let calleeDecl;
239
+ catch (err) {
240
+ this.logger.warn('AstParser: declaration tree build failed', { filePath, err });
1228
241
  try {
1229
- if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
1230
- calleeName = expr.getName();
1231
- const sym = expr.getNameNode().getSymbol();
1232
- const decl = sym?.getDeclarations()?.[0];
1233
- const declPath = decl?.getSourceFile().getFilePath();
1234
- if (declPath && !declPath.includes('/node_modules/')) {
1235
- calleeFile = declPath;
1236
- calleeDecl = decl;
1237
- }
1238
- }
1239
- else if (ts_morph_1.Node.isIdentifier(expr)) {
1240
- calleeName = expr.getText();
1241
- const sym = expr.getSymbol();
1242
- if (sym) {
1243
- // Imported identifiers resolve to ImportSpecifier (same file); follow the alias.
1244
- const firstDecl = sym.getDeclarations()?.[0];
1245
- const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl)
1246
- ? (sym.getAliasedSymbol() ?? sym)
1247
- : sym;
1248
- const decl = resolved.getDeclarations()?.[0];
1249
- const declPath = decl?.getSourceFile().getFilePath();
1250
- if (declPath && !declPath.includes('/node_modules/')) {
1251
- calleeFile = declPath;
1252
- calleeDecl = decl;
1253
- }
1254
- }
1255
- }
242
+ return this.declarationBuilder.flat(symbolCandidates);
1256
243
  }
1257
244
  catch {
1258
- // type resolution failures are normal — skip this call
245
+ return DeclarationTree_1.DeclarationTree.EMPTY;
1259
246
  }
1260
- if (!calleeName || !calleeFile)
1261
- continue;
1262
- const described = calleeDecl ? this.describeIndexedNode(calleeDecl) : null;
1263
- const calleeSymbol = described ? this.qualifiedName(described) : null;
1264
- const fromSymbol = this.findFromSymbol(callExpr);
1265
- const pos = sourceFile.compilerNode
1266
- .getLineAndCharacterOfPosition(callExpr.getStart());
1267
- calls.push({
1268
- project: serviceName,
1269
- fromFile: filePath,
1270
- fromSymbol,
1271
- calleeName,
1272
- calleeFile,
1273
- calleeSymbol,
1274
- line: pos.line + 1,
1275
- col: pos.character + 1,
1276
- });
1277
247
  }
1278
- return calls;
1279
248
  }
1280
249
  }
1281
250
  exports.AstParserService = AstParserService;