@zuvia-software-solutions/code-mapper 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +215 -0
  2. package/dist/cli/ai-context.d.ts +19 -0
  3. package/dist/cli/ai-context.js +168 -0
  4. package/dist/cli/analyze.d.ts +7 -0
  5. package/dist/cli/analyze.js +325 -0
  6. package/dist/cli/augment.d.ts +7 -0
  7. package/dist/cli/augment.js +27 -0
  8. package/dist/cli/clean.d.ts +5 -0
  9. package/dist/cli/clean.js +56 -0
  10. package/dist/cli/eval-server.d.ts +25 -0
  11. package/dist/cli/eval-server.js +365 -0
  12. package/dist/cli/index.d.ts +6 -0
  13. package/dist/cli/index.js +102 -0
  14. package/dist/cli/lazy-action.d.ts +6 -0
  15. package/dist/cli/lazy-action.js +19 -0
  16. package/dist/cli/list.d.ts +2 -0
  17. package/dist/cli/list.js +27 -0
  18. package/dist/cli/mcp.d.ts +8 -0
  19. package/dist/cli/mcp.js +35 -0
  20. package/dist/cli/refresh.d.ts +12 -0
  21. package/dist/cli/refresh.js +165 -0
  22. package/dist/cli/serve.d.ts +5 -0
  23. package/dist/cli/serve.js +8 -0
  24. package/dist/cli/setup.d.ts +6 -0
  25. package/dist/cli/setup.js +218 -0
  26. package/dist/cli/status.d.ts +2 -0
  27. package/dist/cli/status.js +33 -0
  28. package/dist/cli/tool.d.ts +28 -0
  29. package/dist/cli/tool.js +87 -0
  30. package/dist/config/ignore-service.d.ts +32 -0
  31. package/dist/config/ignore-service.js +282 -0
  32. package/dist/config/supported-languages.d.ts +23 -0
  33. package/dist/config/supported-languages.js +52 -0
  34. package/dist/core/augmentation/engine.d.ts +22 -0
  35. package/dist/core/augmentation/engine.js +232 -0
  36. package/dist/core/embeddings/embedder.d.ts +35 -0
  37. package/dist/core/embeddings/embedder.js +171 -0
  38. package/dist/core/embeddings/embedding-pipeline.d.ts +41 -0
  39. package/dist/core/embeddings/embedding-pipeline.js +402 -0
  40. package/dist/core/embeddings/index.d.ts +5 -0
  41. package/dist/core/embeddings/index.js +6 -0
  42. package/dist/core/embeddings/text-generator.d.ts +20 -0
  43. package/dist/core/embeddings/text-generator.js +159 -0
  44. package/dist/core/embeddings/types.d.ts +60 -0
  45. package/dist/core/embeddings/types.js +23 -0
  46. package/dist/core/graph/graph.d.ts +4 -0
  47. package/dist/core/graph/graph.js +65 -0
  48. package/dist/core/graph/types.d.ts +69 -0
  49. package/dist/core/graph/types.js +3 -0
  50. package/dist/core/incremental/child-process.d.ts +8 -0
  51. package/dist/core/incremental/child-process.js +649 -0
  52. package/dist/core/incremental/refresh-coordinator.d.ts +32 -0
  53. package/dist/core/incremental/refresh-coordinator.js +147 -0
  54. package/dist/core/incremental/types.d.ts +78 -0
  55. package/dist/core/incremental/types.js +153 -0
  56. package/dist/core/incremental/watcher.d.ts +63 -0
  57. package/dist/core/incremental/watcher.js +338 -0
  58. package/dist/core/ingestion/ast-cache.d.ts +12 -0
  59. package/dist/core/ingestion/ast-cache.js +34 -0
  60. package/dist/core/ingestion/call-processor.d.ts +34 -0
  61. package/dist/core/ingestion/call-processor.js +937 -0
  62. package/dist/core/ingestion/call-routing.d.ts +40 -0
  63. package/dist/core/ingestion/call-routing.js +97 -0
  64. package/dist/core/ingestion/cluster-enricher.d.ts +30 -0
  65. package/dist/core/ingestion/cluster-enricher.js +151 -0
  66. package/dist/core/ingestion/community-processor.d.ts +26 -0
  67. package/dist/core/ingestion/community-processor.js +272 -0
  68. package/dist/core/ingestion/constants.d.ts +5 -0
  69. package/dist/core/ingestion/constants.js +8 -0
  70. package/dist/core/ingestion/entry-point-scoring.d.ts +23 -0
  71. package/dist/core/ingestion/entry-point-scoring.js +317 -0
  72. package/dist/core/ingestion/export-detection.d.ts +11 -0
  73. package/dist/core/ingestion/export-detection.js +203 -0
  74. package/dist/core/ingestion/filesystem-walker.d.ts +18 -0
  75. package/dist/core/ingestion/filesystem-walker.js +64 -0
  76. package/dist/core/ingestion/framework-detection.d.ts +42 -0
  77. package/dist/core/ingestion/framework-detection.js +405 -0
  78. package/dist/core/ingestion/heritage-processor.d.ts +15 -0
  79. package/dist/core/ingestion/heritage-processor.js +237 -0
  80. package/dist/core/ingestion/import-processor.d.ts +31 -0
  81. package/dist/core/ingestion/import-processor.js +416 -0
  82. package/dist/core/ingestion/language-config.d.ts +32 -0
  83. package/dist/core/ingestion/language-config.js +161 -0
  84. package/dist/core/ingestion/mro-processor.d.ts +32 -0
  85. package/dist/core/ingestion/mro-processor.js +343 -0
  86. package/dist/core/ingestion/named-binding-extraction.d.ts +51 -0
  87. package/dist/core/ingestion/named-binding-extraction.js +343 -0
  88. package/dist/core/ingestion/parsing-processor.d.ts +20 -0
  89. package/dist/core/ingestion/parsing-processor.js +282 -0
  90. package/dist/core/ingestion/pipeline.d.ts +3 -0
  91. package/dist/core/ingestion/pipeline.js +416 -0
  92. package/dist/core/ingestion/process-processor.d.ts +42 -0
  93. package/dist/core/ingestion/process-processor.js +357 -0
  94. package/dist/core/ingestion/resolution-context.d.ts +40 -0
  95. package/dist/core/ingestion/resolution-context.js +171 -0
  96. package/dist/core/ingestion/resolvers/csharp.d.ts +10 -0
  97. package/dist/core/ingestion/resolvers/csharp.js +101 -0
  98. package/dist/core/ingestion/resolvers/go.d.ts +8 -0
  99. package/dist/core/ingestion/resolvers/go.js +33 -0
  100. package/dist/core/ingestion/resolvers/index.d.ts +14 -0
  101. package/dist/core/ingestion/resolvers/index.js +10 -0
  102. package/dist/core/ingestion/resolvers/jvm.d.ts +9 -0
  103. package/dist/core/ingestion/resolvers/jvm.js +74 -0
  104. package/dist/core/ingestion/resolvers/php.d.ts +7 -0
  105. package/dist/core/ingestion/resolvers/php.js +30 -0
  106. package/dist/core/ingestion/resolvers/ruby.d.ts +9 -0
  107. package/dist/core/ingestion/resolvers/ruby.js +13 -0
  108. package/dist/core/ingestion/resolvers/rust.d.ts +5 -0
  109. package/dist/core/ingestion/resolvers/rust.js +62 -0
  110. package/dist/core/ingestion/resolvers/standard.d.ts +16 -0
  111. package/dist/core/ingestion/resolvers/standard.js +144 -0
  112. package/dist/core/ingestion/resolvers/utils.d.ts +18 -0
  113. package/dist/core/ingestion/resolvers/utils.js +113 -0
  114. package/dist/core/ingestion/structure-processor.d.ts +4 -0
  115. package/dist/core/ingestion/structure-processor.js +39 -0
  116. package/dist/core/ingestion/symbol-table.d.ts +34 -0
  117. package/dist/core/ingestion/symbol-table.js +48 -0
  118. package/dist/core/ingestion/tree-sitter-queries.d.ts +20 -0
  119. package/dist/core/ingestion/tree-sitter-queries.js +691 -0
  120. package/dist/core/ingestion/type-env.d.ts +52 -0
  121. package/dist/core/ingestion/type-env.js +349 -0
  122. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +4 -0
  123. package/dist/core/ingestion/type-extractors/c-cpp.js +214 -0
  124. package/dist/core/ingestion/type-extractors/csharp.d.ts +4 -0
  125. package/dist/core/ingestion/type-extractors/csharp.js +224 -0
  126. package/dist/core/ingestion/type-extractors/go.d.ts +4 -0
  127. package/dist/core/ingestion/type-extractors/go.js +261 -0
  128. package/dist/core/ingestion/type-extractors/index.d.ts +20 -0
  129. package/dist/core/ingestion/type-extractors/index.js +30 -0
  130. package/dist/core/ingestion/type-extractors/jvm.d.ts +5 -0
  131. package/dist/core/ingestion/type-extractors/jvm.js +386 -0
  132. package/dist/core/ingestion/type-extractors/php.d.ts +4 -0
  133. package/dist/core/ingestion/type-extractors/php.js +280 -0
  134. package/dist/core/ingestion/type-extractors/python.d.ts +4 -0
  135. package/dist/core/ingestion/type-extractors/python.js +175 -0
  136. package/dist/core/ingestion/type-extractors/ruby.d.ts +12 -0
  137. package/dist/core/ingestion/type-extractors/ruby.js +218 -0
  138. package/dist/core/ingestion/type-extractors/rust.d.ts +4 -0
  139. package/dist/core/ingestion/type-extractors/rust.js +290 -0
  140. package/dist/core/ingestion/type-extractors/shared.d.ts +81 -0
  141. package/dist/core/ingestion/type-extractors/shared.js +322 -0
  142. package/dist/core/ingestion/type-extractors/swift.d.ts +4 -0
  143. package/dist/core/ingestion/type-extractors/swift.js +140 -0
  144. package/dist/core/ingestion/type-extractors/types.d.ts +111 -0
  145. package/dist/core/ingestion/type-extractors/types.js +4 -0
  146. package/dist/core/ingestion/type-extractors/typescript.d.ts +4 -0
  147. package/dist/core/ingestion/type-extractors/typescript.js +227 -0
  148. package/dist/core/ingestion/utils.d.ts +73 -0
  149. package/dist/core/ingestion/utils.js +992 -0
  150. package/dist/core/ingestion/workers/parse-worker.d.ts +99 -0
  151. package/dist/core/ingestion/workers/parse-worker.js +1055 -0
  152. package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
  153. package/dist/core/ingestion/workers/worker-pool.js +123 -0
  154. package/dist/core/lbug/csv-generator.d.ts +28 -0
  155. package/dist/core/lbug/csv-generator.js +355 -0
  156. package/dist/core/lbug/lbug-adapter.d.ts +96 -0
  157. package/dist/core/lbug/lbug-adapter.js +753 -0
  158. package/dist/core/lbug/schema.d.ts +46 -0
  159. package/dist/core/lbug/schema.js +402 -0
  160. package/dist/core/search/bm25-index.d.ts +20 -0
  161. package/dist/core/search/bm25-index.js +123 -0
  162. package/dist/core/search/hybrid-search.d.ts +32 -0
  163. package/dist/core/search/hybrid-search.js +131 -0
  164. package/dist/core/search/query-cache.d.ts +18 -0
  165. package/dist/core/search/query-cache.js +47 -0
  166. package/dist/core/search/query-expansion.d.ts +19 -0
  167. package/dist/core/search/query-expansion.js +75 -0
  168. package/dist/core/search/reranker.d.ts +29 -0
  169. package/dist/core/search/reranker.js +122 -0
  170. package/dist/core/search/types.d.ts +154 -0
  171. package/dist/core/search/types.js +51 -0
  172. package/dist/core/semantic/tsgo-service.d.ts +67 -0
  173. package/dist/core/semantic/tsgo-service.js +355 -0
  174. package/dist/core/tree-sitter/parser-loader.d.ts +12 -0
  175. package/dist/core/tree-sitter/parser-loader.js +71 -0
  176. package/dist/lib/memory-guard.d.ts +35 -0
  177. package/dist/lib/memory-guard.js +70 -0
  178. package/dist/lib/utils.d.ts +3 -0
  179. package/dist/lib/utils.js +6 -0
  180. package/dist/mcp/compatible-stdio-transport.d.ts +32 -0
  181. package/dist/mcp/compatible-stdio-transport.js +209 -0
  182. package/dist/mcp/core/embedder.d.ts +24 -0
  183. package/dist/mcp/core/embedder.js +168 -0
  184. package/dist/mcp/core/lbug-adapter.d.ts +29 -0
  185. package/dist/mcp/core/lbug-adapter.js +330 -0
  186. package/dist/mcp/local/local-backend.d.ts +188 -0
  187. package/dist/mcp/local/local-backend.js +2759 -0
  188. package/dist/mcp/resources.d.ts +22 -0
  189. package/dist/mcp/resources.js +379 -0
  190. package/dist/mcp/server.d.ts +10 -0
  191. package/dist/mcp/server.js +217 -0
  192. package/dist/mcp/staleness.d.ts +10 -0
  193. package/dist/mcp/staleness.js +25 -0
  194. package/dist/mcp/tools.d.ts +21 -0
  195. package/dist/mcp/tools.js +202 -0
  196. package/dist/server/api.d.ts +5 -0
  197. package/dist/server/api.js +340 -0
  198. package/dist/server/mcp-http.d.ts +7 -0
  199. package/dist/server/mcp-http.js +95 -0
  200. package/dist/storage/git.d.ts +6 -0
  201. package/dist/storage/git.js +35 -0
  202. package/dist/storage/repo-manager.d.ts +87 -0
  203. package/dist/storage/repo-manager.js +249 -0
  204. package/dist/types/pipeline.d.ts +35 -0
  205. package/dist/types/pipeline.js +20 -0
  206. package/hooks/claude/code-mapper-hook.cjs +238 -0
  207. package/hooks/claude/pre-tool-use.sh +79 -0
  208. package/hooks/claude/session-start.sh +42 -0
  209. package/models/mlx-embedder.py +185 -0
  210. package/package.json +100 -0
  211. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  212. package/vendor/leiden/index.cjs +355 -0
  213. package/vendor/leiden/utils.cjs +392 -0
@@ -0,0 +1,1055 @@
1
+ // code-mapper/src/core/ingestion/workers/parse-worker.ts
2
+ /**
3
+ * @file Worker thread for parallel AST parsing and extraction
4
+ * @description Parses source files using tree-sitter, extracts definitions, calls,
5
+ * imports, heritage, and routes. Supports sub-batch streaming for bounded memory
6
+ */
7
+ import { parentPort } from 'node:worker_threads';
8
+ import Parser from 'tree-sitter';
9
+ import JavaScript from 'tree-sitter-javascript';
10
+ import TypeScript from 'tree-sitter-typescript';
11
+ import Python from 'tree-sitter-python';
12
+ import Java from 'tree-sitter-java';
13
+ import C from 'tree-sitter-c';
14
+ import CPP from 'tree-sitter-cpp';
15
+ import CSharp from 'tree-sitter-c-sharp';
16
+ import Go from 'tree-sitter-go';
17
+ import Rust from 'tree-sitter-rust';
18
+ import PHP from 'tree-sitter-php';
19
+ import Ruby from 'tree-sitter-ruby';
20
+ import { createRequire } from 'node:module';
21
+ import { SupportedLanguages } from '../../../config/supported-languages.js';
22
+ import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
23
+ import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from '../constants.js';
24
+ // tree-sitter-swift is an optionalDependency
25
+ const _require = createRequire(import.meta.url);
26
+ let Swift = null;
27
+ try {
28
+ Swift = _require('tree-sitter-swift');
29
+ }
30
+ catch { }
31
+ // tree-sitter-kotlin is an optionalDependency
32
+ let Kotlin = null;
33
+ try {
34
+ Kotlin = _require('tree-sitter-kotlin');
35
+ }
36
+ catch { }
37
+ import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, CALL_EXPRESSION_TYPES, extractCallChain, } from '../utils.js';
38
+ import { buildTypeEnv } from '../type-env.js';
39
+ import { isNodeExported } from '../export-detection.js';
40
+ import { detectFrameworkFromAST } from '../framework-detection.js';
41
+ import { typeConfigs } from '../type-extractors/index.js';
42
+ import { generateId } from '../../../lib/utils.js';
43
+ import { extractNamedBindings } from '../named-binding-extraction.js';
44
+ import { appendKotlinWildcard } from '../resolvers/index.js';
45
+ import { callRouters } from '../call-routing.js';
46
+ // Worker-local parser + language map
47
+ const parser = new Parser();
48
+ const languageMap = {
49
+ [SupportedLanguages.JavaScript]: JavaScript,
50
+ [SupportedLanguages.TypeScript]: TypeScript.typescript,
51
+ [`${SupportedLanguages.TypeScript}:tsx`]: TypeScript.tsx,
52
+ [SupportedLanguages.Python]: Python,
53
+ [SupportedLanguages.Java]: Java,
54
+ [SupportedLanguages.C]: C,
55
+ [SupportedLanguages.CPlusPlus]: CPP,
56
+ [SupportedLanguages.CSharp]: CSharp,
57
+ [SupportedLanguages.Go]: Go,
58
+ [SupportedLanguages.Rust]: Rust,
59
+ ...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
60
+ [SupportedLanguages.PHP]: PHP.php_only,
61
+ [SupportedLanguages.Ruby]: Ruby,
62
+ ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
63
+ };
64
+ // Check if a language grammar is available (duplicated from parser-loader.ts for worker isolation)
65
+ const isLanguageAvailable = (language, filePath) => {
66
+ const key = language === SupportedLanguages.TypeScript && filePath.endsWith('.tsx')
67
+ ? `${language}:tsx`
68
+ : language;
69
+ return key in languageMap && languageMap[key] != null;
70
+ };
71
+ const setLanguage = (language, filePath) => {
72
+ const key = language === SupportedLanguages.TypeScript && filePath.endsWith('.tsx')
73
+ ? `${language}:tsx`
74
+ : language;
75
+ const lang = languageMap[key];
76
+ if (!lang)
77
+ throw new Error(`Unsupported language: ${language}`);
78
+ parser.setLanguage(lang);
79
+ };
80
+ // Enclosing function detection
81
+ /** Walk up AST to find enclosing function, return its generateId or null for top-level */
82
+ const findEnclosingFunctionId = (node, filePath) => {
83
+ let current = node.parent;
84
+ while (current) {
85
+ if (FUNCTION_NODE_TYPES.has(current.type)) {
86
+ const { funcName, label } = extractFunctionName(current);
87
+ if (funcName) {
88
+ return generateId(label, `${filePath}:${funcName}`);
89
+ }
90
+ }
91
+ current = current.parent;
92
+ }
93
+ return null;
94
+ };
95
+ // Label detection from capture map
96
+ const getLabelFromCaptures = (captureMap) => {
97
+ // Skip imports (handled separately) and calls
98
+ if (captureMap['import'] || captureMap['call'])
99
+ return null;
100
+ if (!captureMap['name'])
101
+ return null;
102
+ if (captureMap['definition.function'])
103
+ return 'Function';
104
+ if (captureMap['definition.class'])
105
+ return 'Class';
106
+ if (captureMap['definition.interface'])
107
+ return 'Interface';
108
+ if (captureMap['definition.method'])
109
+ return 'Method';
110
+ if (captureMap['definition.struct'])
111
+ return 'Struct';
112
+ if (captureMap['definition.enum'])
113
+ return 'Enum';
114
+ if (captureMap['definition.namespace'])
115
+ return 'Namespace';
116
+ if (captureMap['definition.module'])
117
+ return 'Module';
118
+ if (captureMap['definition.trait'])
119
+ return 'Trait';
120
+ if (captureMap['definition.impl'])
121
+ return 'Impl';
122
+ if (captureMap['definition.type'])
123
+ return 'TypeAlias';
124
+ if (captureMap['definition.const'])
125
+ return 'Const';
126
+ if (captureMap['definition.static'])
127
+ return 'Static';
128
+ if (captureMap['definition.typedef'])
129
+ return 'Typedef';
130
+ if (captureMap['definition.macro'])
131
+ return 'Macro';
132
+ if (captureMap['definition.union'])
133
+ return 'Union';
134
+ if (captureMap['definition.property'])
135
+ return 'Property';
136
+ if (captureMap['definition.record'])
137
+ return 'Record';
138
+ if (captureMap['definition.delegate'])
139
+ return 'Delegate';
140
+ if (captureMap['definition.annotation'])
141
+ return 'Annotation';
142
+ if (captureMap['definition.constructor'])
143
+ return 'Constructor';
144
+ if (captureMap['definition.template'])
145
+ return 'Template';
146
+ return 'CodeElement';
147
+ };
148
+ // Process a batch of files
149
+ const processBatch = (files, onProgress) => {
150
+ const result = {
151
+ nodes: [],
152
+ relationships: [],
153
+ symbols: [],
154
+ imports: [],
155
+ calls: [],
156
+ heritage: [],
157
+ routes: [],
158
+ constructorBindings: [],
159
+ skippedLanguages: {},
160
+ fileCount: 0,
161
+ };
162
+ // Group by language to minimize setLanguage calls
163
+ const byLanguage = new Map();
164
+ for (const file of files) {
165
+ const lang = getLanguageFromFilename(file.path);
166
+ if (!lang)
167
+ continue;
168
+ let list = byLanguage.get(lang);
169
+ if (!list) {
170
+ list = [];
171
+ byLanguage.set(lang, list);
172
+ }
173
+ list.push(file);
174
+ }
175
+ let totalProcessed = 0;
176
+ let lastReported = 0;
177
+ const PROGRESS_INTERVAL = 100; // report every 100 files
178
+ const onFileProcessed = onProgress ? () => {
179
+ totalProcessed++;
180
+ if (totalProcessed - lastReported >= PROGRESS_INTERVAL) {
181
+ lastReported = totalProcessed;
182
+ onProgress(totalProcessed);
183
+ }
184
+ } : undefined;
185
+ for (const [language, langFiles] of byLanguage) {
186
+ const queryString = LANGUAGE_QUERIES[language];
187
+ if (!queryString)
188
+ continue;
189
+ // Track if we need to handle tsx separately
190
+ const tsxFiles = [];
191
+ const regularFiles = [];
192
+ if (language === SupportedLanguages.TypeScript) {
193
+ for (const f of langFiles) {
194
+ if (f.path.endsWith('.tsx')) {
195
+ tsxFiles.push(f);
196
+ }
197
+ else {
198
+ regularFiles.push(f);
199
+ }
200
+ }
201
+ }
202
+ else {
203
+ regularFiles.push(...langFiles);
204
+ }
205
+ // Process regular files for this language
206
+ if (regularFiles.length > 0) {
207
+ if (isLanguageAvailable(language, regularFiles[0].path)) {
208
+ try {
209
+ setLanguage(language, regularFiles[0].path);
210
+ processFileGroup(regularFiles, language, queryString, result, onFileProcessed);
211
+ }
212
+ catch {
213
+ // parser unavailable — skip this language group
214
+ }
215
+ }
216
+ else {
217
+ result.skippedLanguages[language] = (result.skippedLanguages[language] || 0) + regularFiles.length;
218
+ }
219
+ }
220
+ // Process tsx files separately (different grammar)
221
+ if (tsxFiles.length > 0) {
222
+ if (isLanguageAvailable(language, tsxFiles[0].path)) {
223
+ try {
224
+ setLanguage(language, tsxFiles[0].path);
225
+ processFileGroup(tsxFiles, language, queryString, result, onFileProcessed);
226
+ }
227
+ catch {
228
+ // parser unavailable — skip this language group
229
+ }
230
+ }
231
+ else {
232
+ result.skippedLanguages[language] = (result.skippedLanguages[language] || 0) + tsxFiles.length;
233
+ }
234
+ }
235
+ }
236
+ return result;
237
+ };
238
+ // PHP Eloquent metadata extraction
239
+ // Eloquent model properties whose array values are worth indexing
240
+ const ELOQUENT_ARRAY_PROPS = new Set(['fillable', 'casts', 'hidden', 'guarded', 'with', 'appends']);
241
+ // Eloquent relationship method names
242
+ const ELOQUENT_RELATIONS = new Set([
243
+ 'hasMany', 'hasOne', 'belongsTo', 'belongsToMany',
244
+ 'morphTo', 'morphMany', 'morphOne', 'morphToMany', 'morphedByMany',
245
+ 'hasManyThrough', 'hasOneThrough',
246
+ ]);
247
+ function findDescendant(node, type) {
248
+ if (node.type === type)
249
+ return node;
250
+ for (const child of (node.children ?? [])) {
251
+ const found = findDescendant(child, type);
252
+ if (found)
253
+ return found;
254
+ }
255
+ return null;
256
+ }
257
+ function extractStringContent(node) {
258
+ if (!node)
259
+ return null;
260
+ const content = node.children?.find((c) => c.type === 'string_content');
261
+ if (content)
262
+ return content.text;
263
+ if (node.type === 'string_content')
264
+ return node.text;
265
+ return null;
266
+ }
267
+ /** Extract array values from a PHP property_declaration as a description string */
268
+ function extractPhpPropertyDescription(propName, propDeclNode) {
269
+ if (!ELOQUENT_ARRAY_PROPS.has(propName))
270
+ return null;
271
+ const arrayNode = findDescendant(propDeclNode, 'array_creation_expression');
272
+ if (!arrayNode)
273
+ return null;
274
+ const items = [];
275
+ for (const child of (arrayNode.children ?? [])) {
276
+ if (child.type !== 'array_element_initializer')
277
+ continue;
278
+ const children = child.children ?? [];
279
+ const arrowIdx = children.findIndex((c) => c.type === '=>');
280
+ if (arrowIdx !== -1) {
281
+ // key => value pair (used in $casts)
282
+ const key = extractStringContent(children[arrowIdx - 1]);
283
+ const val = extractStringContent(children[arrowIdx + 1]);
284
+ if (key && val)
285
+ items.push(`${key}:${val}`);
286
+ }
287
+ else {
288
+ // Simple value (used in $fillable, $hidden, etc.)
289
+ const val = extractStringContent(children[0]);
290
+ if (val)
291
+ items.push(val);
292
+ }
293
+ }
294
+ return items.length > 0 ? items.join(', ') : null;
295
+ }
296
+ /** Detect Eloquent relationship in a PHP method_declaration (e.g., "hasMany(Post)") */
297
+ function extractEloquentRelationDescription(methodNode) {
298
+ function findRelationCall(node) {
299
+ if (node.type === 'member_call_expression') {
300
+ const children = node.children ?? [];
301
+ const objectNode = children.find((c) => c.type === 'variable_name' && c.text === '$this');
302
+ const nameNode = children.find((c) => c.type === 'name');
303
+ if (objectNode && nameNode && ELOQUENT_RELATIONS.has(nameNode.text))
304
+ return node;
305
+ }
306
+ for (const child of (node.children ?? [])) {
307
+ const found = findRelationCall(child);
308
+ if (found)
309
+ return found;
310
+ }
311
+ return null;
312
+ }
313
+ const callNode = findRelationCall(methodNode);
314
+ if (!callNode)
315
+ return null;
316
+ const relType = callNode.children?.find((c) => c.type === 'name')?.text;
317
+ const argsNode = callNode.children?.find((c) => c.type === 'arguments');
318
+ let targetModel = null;
319
+ if (argsNode) {
320
+ const firstArg = argsNode.children?.find((c) => c.type === 'argument');
321
+ if (firstArg) {
322
+ const classConstant = firstArg.children?.find((c) => c.type === 'class_constant_access_expression');
323
+ if (classConstant) {
324
+ targetModel = classConstant.children?.find((c) => c.type === 'name')?.text ?? null;
325
+ }
326
+ }
327
+ }
328
+ if (relType && targetModel)
329
+ return `${relType}(${targetModel})`;
330
+ if (relType)
331
+ return relType;
332
+ return null;
333
+ }
334
+ const ROUTE_HTTP_METHODS = new Set([
335
+ 'get', 'post', 'put', 'patch', 'delete', 'options', 'any', 'match',
336
+ ]);
337
+ const ROUTE_RESOURCE_METHODS = new Set(['resource', 'apiResource']);
338
+ const RESOURCE_ACTIONS = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
339
+ const API_RESOURCE_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
340
+ // Check if node is a scoped_call_expression with object 'Route'
341
+ function isRouteStaticCall(node) {
342
+ if (node.type !== 'scoped_call_expression')
343
+ return false;
344
+ const obj = node.childForFieldName?.('object') ?? node.children?.[0];
345
+ return obj?.text === 'Route';
346
+ }
347
+ // Get the method name from a scoped_call_expression or member_call_expression
348
+ function getCallMethodName(node) {
349
+ const nameNode = node.childForFieldName?.('name') ??
350
+ node.children?.find((c) => c.type === 'name');
351
+ return nameNode?.text ?? null;
352
+ }
353
+ // Get the arguments node from a call expression
354
+ function getArguments(node) {
355
+ return node.children?.find((c) => c.type === 'arguments') ?? null;
356
+ }
357
+ // Find the closure body inside arguments
358
+ function findClosureBody(argsNode) {
359
+ if (!argsNode)
360
+ return null;
361
+ for (const child of argsNode.children ?? []) {
362
+ if (child.type === 'argument') {
363
+ for (const inner of child.children ?? []) {
364
+ if (inner.type === 'anonymous_function' ||
365
+ inner.type === 'arrow_function') {
366
+ return inner.childForFieldName?.('body') ??
367
+ inner.children?.find((c) => c.type === 'compound_statement');
368
+ }
369
+ }
370
+ }
371
+ if (child.type === 'anonymous_function' ||
372
+ child.type === 'arrow_function') {
373
+ return child.childForFieldName?.('body') ??
374
+ child.children?.find((c) => c.type === 'compound_statement');
375
+ }
376
+ }
377
+ return null;
378
+ }
379
+ // Extract first string argument from arguments node
380
+ function extractFirstStringArg(argsNode) {
381
+ if (!argsNode)
382
+ return null;
383
+ for (const child of argsNode.children ?? []) {
384
+ const target = child.type === 'argument' ? child.children?.[0] : child;
385
+ if (!target)
386
+ continue;
387
+ if (target.type === 'string' || target.type === 'encapsed_string') {
388
+ return extractStringContent(target);
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+ // Extract middleware from arguments (handles string or array)
394
+ function extractMiddlewareArg(argsNode) {
395
+ if (!argsNode)
396
+ return [];
397
+ for (const child of argsNode.children ?? []) {
398
+ const target = child.type === 'argument' ? child.children?.[0] : child;
399
+ if (!target)
400
+ continue;
401
+ if (target.type === 'string' || target.type === 'encapsed_string') {
402
+ const val = extractStringContent(target);
403
+ return val ? [val] : [];
404
+ }
405
+ if (target.type === 'array_creation_expression') {
406
+ const items = [];
407
+ for (const el of target.children ?? []) {
408
+ if (el.type === 'array_element_initializer') {
409
+ const str = el.children?.find((c) => c.type === 'string' || c.type === 'encapsed_string');
410
+ const val = str ? extractStringContent(str) : null;
411
+ if (val)
412
+ items.push(val);
413
+ }
414
+ }
415
+ return items;
416
+ }
417
+ }
418
+ return [];
419
+ }
420
+ // Extract Controller::class from arguments
421
+ function extractClassArg(argsNode) {
422
+ if (!argsNode)
423
+ return null;
424
+ for (const child of argsNode.children ?? []) {
425
+ const target = child.type === 'argument' ? child.children?.[0] : child;
426
+ if (target?.type === 'class_constant_access_expression') {
427
+ return target.children?.find((c) => c.type === 'name')?.text ?? null;
428
+ }
429
+ }
430
+ return null;
431
+ }
432
+ // Extract controller target from [Controller::class, 'method'] or 'Controller@method'
433
+ function extractControllerTarget(argsNode) {
434
+ if (!argsNode)
435
+ return { controller: null, method: null };
436
+ const args = [];
437
+ for (const child of argsNode.children ?? []) {
438
+ if (child.type === 'argument')
439
+ args.push(child.children?.[0]);
440
+ else if (child.type !== '(' && child.type !== ')' && child.type !== ',')
441
+ args.push(child);
442
+ }
443
+ // Second arg is the handler
444
+ const handlerNode = args[1];
445
+ if (!handlerNode)
446
+ return { controller: null, method: null };
447
+ // Array syntax: [UserController::class, 'index']
448
+ if (handlerNode.type === 'array_creation_expression') {
449
+ let controller = null;
450
+ let method = null;
451
+ const elements = [];
452
+ for (const el of handlerNode.children ?? []) {
453
+ if (el.type === 'array_element_initializer')
454
+ elements.push(el);
455
+ }
456
+ if (elements[0]) {
457
+ const classAccess = findDescendant(elements[0], 'class_constant_access_expression');
458
+ if (classAccess) {
459
+ controller = classAccess.children?.find((c) => c.type === 'name')?.text ?? null;
460
+ }
461
+ }
462
+ if (elements[1]) {
463
+ const str = findDescendant(elements[1], 'string');
464
+ method = str ? extractStringContent(str) : null;
465
+ }
466
+ return { controller, method };
467
+ }
468
+ // String syntax: 'UserController@index'
469
+ if (handlerNode.type === 'string' || handlerNode.type === 'encapsed_string') {
470
+ const text = extractStringContent(handlerNode);
471
+ if (text?.includes('@')) {
472
+ const [controller, method] = text.split('@');
473
+ return { controller, method };
474
+ }
475
+ }
476
+ // Class reference: UserController::class (invokable controller)
477
+ if (handlerNode.type === 'class_constant_access_expression') {
478
+ const controller = handlerNode.children?.find((c) => c.type === 'name')?.text ?? null;
479
+ return { controller, method: '__invoke' };
480
+ }
481
+ return { controller: null, method: null };
482
+ }
483
+ // Unwrap a chained call like Route::middleware('auth')->prefix('api')->group(fn)
484
+ function unwrapRouteChain(node) {
485
+ if (node.type !== 'member_call_expression')
486
+ return null;
487
+ const terminalMethod = getCallMethodName(node);
488
+ if (!terminalMethod)
489
+ return null;
490
+ const terminalArgs = getArguments(node);
491
+ const attributes = [];
492
+ let current = node.children?.[0];
493
+ while (current) {
494
+ if (current.type === 'member_call_expression') {
495
+ const method = getCallMethodName(current);
496
+ const args = getArguments(current);
497
+ if (method)
498
+ attributes.unshift({ method, argsNode: args });
499
+ current = current.children?.[0];
500
+ }
501
+ else if (current.type === 'scoped_call_expression') {
502
+ const obj = current.childForFieldName?.('object') ?? current.children?.[0];
503
+ if (obj?.text !== 'Route')
504
+ return null;
505
+ const method = getCallMethodName(current);
506
+ const args = getArguments(current);
507
+ if (method)
508
+ attributes.unshift({ method, argsNode: args });
509
+ return { isRouteFacade: true, terminalMethod, attributes, terminalArgs, node };
510
+ }
511
+ else {
512
+ break;
513
+ }
514
+ }
515
+ return null;
516
+ }
517
+ // Parse Route::group(['middleware' => ..., 'prefix' => ...], fn) array syntax
518
+ function parseArrayGroupArgs(argsNode) {
519
+ const ctx = { middleware: [], prefix: null, controller: null };
520
+ if (!argsNode)
521
+ return ctx;
522
+ for (const child of argsNode.children ?? []) {
523
+ const target = child.type === 'argument' ? child.children?.[0] : child;
524
+ if (target?.type === 'array_creation_expression') {
525
+ for (const el of target.children ?? []) {
526
+ if (el.type !== 'array_element_initializer')
527
+ continue;
528
+ const children = el.children ?? [];
529
+ const arrowIdx = children.findIndex((c) => c.type === '=>');
530
+ if (arrowIdx === -1)
531
+ continue;
532
+ const key = extractStringContent(children[arrowIdx - 1]);
533
+ const val = children[arrowIdx + 1];
534
+ if (key === 'middleware') {
535
+ if (val?.type === 'string') {
536
+ const s = extractStringContent(val);
537
+ if (s)
538
+ ctx.middleware.push(s);
539
+ }
540
+ else if (val?.type === 'array_creation_expression') {
541
+ for (const item of val.children ?? []) {
542
+ if (item.type === 'array_element_initializer') {
543
+ const str = item.children?.find((c) => c.type === 'string');
544
+ const s = str ? extractStringContent(str) : null;
545
+ if (s)
546
+ ctx.middleware.push(s);
547
+ }
548
+ }
549
+ }
550
+ }
551
+ else if (key === 'prefix') {
552
+ ctx.prefix = extractStringContent(val) ?? null;
553
+ }
554
+ else if (key === 'controller') {
555
+ if (val?.type === 'class_constant_access_expression') {
556
+ ctx.controller = val.children?.find((c) => c.type === 'name')?.text ?? null;
557
+ }
558
+ }
559
+ }
560
+ }
561
+ }
562
+ return ctx;
563
+ }
564
+ function extractLaravelRoutes(tree, filePath) {
565
+ const routes = [];
566
+ function resolveStack(stack) {
567
+ const middleware = [];
568
+ let prefix = null;
569
+ let controller = null;
570
+ for (const ctx of stack) {
571
+ middleware.push(...ctx.middleware);
572
+ if (ctx.prefix)
573
+ prefix = prefix ? `${prefix}/${ctx.prefix}`.replace(/\/+/g, '/') : ctx.prefix;
574
+ if (ctx.controller)
575
+ controller = ctx.controller;
576
+ }
577
+ return { middleware, prefix, controller };
578
+ }
579
+ function emitRoute(httpMethod, argsNode, lineNumber, groupStack, chainAttrs) {
580
+ const effective = resolveStack(groupStack);
581
+ for (const attr of chainAttrs) {
582
+ if (attr.method === 'middleware')
583
+ effective.middleware.push(...extractMiddlewareArg(attr.argsNode));
584
+ if (attr.method === 'prefix') {
585
+ const p = extractFirstStringArg(attr.argsNode);
586
+ if (p)
587
+ effective.prefix = effective.prefix ? `${effective.prefix}/${p}` : p;
588
+ }
589
+ if (attr.method === 'controller') {
590
+ const cls = extractClassArg(attr.argsNode);
591
+ if (cls)
592
+ effective.controller = cls;
593
+ }
594
+ }
595
+ const routePath = extractFirstStringArg(argsNode);
596
+ if (ROUTE_RESOURCE_METHODS.has(httpMethod)) {
597
+ const target = extractControllerTarget(argsNode);
598
+ const actions = httpMethod === 'apiResource' ? API_RESOURCE_ACTIONS : RESOURCE_ACTIONS;
599
+ for (const action of actions) {
600
+ routes.push({
601
+ filePath, httpMethod, routePath,
602
+ controllerName: target.controller ?? effective.controller,
603
+ methodName: action,
604
+ middleware: [...effective.middleware],
605
+ prefix: effective.prefix,
606
+ lineNumber,
607
+ });
608
+ }
609
+ }
610
+ else {
611
+ const target = extractControllerTarget(argsNode);
612
+ routes.push({
613
+ filePath, httpMethod, routePath,
614
+ controllerName: target.controller ?? effective.controller,
615
+ methodName: target.method,
616
+ middleware: [...effective.middleware],
617
+ prefix: effective.prefix,
618
+ lineNumber,
619
+ });
620
+ }
621
+ }
622
+ function walk(node, groupStack) {
623
+ // Case 1: Simple Route::get(...), Route::post(...), etc
624
+ if (isRouteStaticCall(node)) {
625
+ const method = getCallMethodName(node);
626
+ if (method && (ROUTE_HTTP_METHODS.has(method) || ROUTE_RESOURCE_METHODS.has(method))) {
627
+ emitRoute(method, getArguments(node), node.startPosition.row, groupStack, []);
628
+ return;
629
+ }
630
+ if (method === 'group') {
631
+ const argsNode = getArguments(node);
632
+ const groupCtx = parseArrayGroupArgs(argsNode);
633
+ const body = findClosureBody(argsNode);
634
+ if (body) {
635
+ groupStack.push(groupCtx);
636
+ walkChildren(body, groupStack);
637
+ groupStack.pop();
638
+ }
639
+ return;
640
+ }
641
+ }
642
+ // Case 2: Fluent chain — Route::middleware(...)->group(...) or Route::middleware(...)->get(...)
643
+ const chain = unwrapRouteChain(node);
644
+ if (chain) {
645
+ if (chain.terminalMethod === 'group') {
646
+ const groupCtx = { middleware: [], prefix: null, controller: null };
647
+ for (const attr of chain.attributes) {
648
+ if (attr.method === 'middleware')
649
+ groupCtx.middleware.push(...extractMiddlewareArg(attr.argsNode));
650
+ if (attr.method === 'prefix')
651
+ groupCtx.prefix = extractFirstStringArg(attr.argsNode);
652
+ if (attr.method === 'controller')
653
+ groupCtx.controller = extractClassArg(attr.argsNode);
654
+ }
655
+ const body = findClosureBody(chain.terminalArgs);
656
+ if (body) {
657
+ groupStack.push(groupCtx);
658
+ walkChildren(body, groupStack);
659
+ groupStack.pop();
660
+ }
661
+ return;
662
+ }
663
+ if (ROUTE_HTTP_METHODS.has(chain.terminalMethod) || ROUTE_RESOURCE_METHODS.has(chain.terminalMethod)) {
664
+ emitRoute(chain.terminalMethod, chain.terminalArgs, node.startPosition.row, groupStack, chain.attributes);
665
+ return;
666
+ }
667
+ }
668
+ // Default: recurse into children
669
+ walkChildren(node, groupStack);
670
+ }
671
+ function walkChildren(node, groupStack) {
672
+ for (const child of node.children ?? []) {
673
+ walk(child, groupStack);
674
+ }
675
+ }
676
+ walk(tree.rootNode, []);
677
+ return routes;
678
+ }
679
+ const processFileGroup = (files, language, queryString, result, onFileProcessed) => {
680
+ let query;
681
+ try {
682
+ const lang = parser.getLanguage();
683
+ query = new Parser.Query(lang, queryString);
684
+ }
685
+ catch (err) {
686
+ const message = `Query compilation failed for ${language}: ${err instanceof Error ? err.message : String(err)}`;
687
+ if (parentPort) {
688
+ parentPort.postMessage({ type: 'warning', message });
689
+ }
690
+ else {
691
+ console.warn(message);
692
+ }
693
+ return;
694
+ }
695
+ for (const file of files) {
696
+ // Skip files larger than the max tree-sitter buffer (32 MB)
697
+ if (file.content.length > TREE_SITTER_MAX_BUFFER)
698
+ continue;
699
+ let tree;
700
+ try {
701
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
702
+ }
703
+ catch (err) {
704
+ console.warn(`Failed to parse file ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
705
+ continue;
706
+ }
707
+ result.fileCount++;
708
+ onFileProcessed?.();
709
+ // Build per-file type environment + constructor bindings in a single AST walk
710
+ const typeEnv = buildTypeEnv(tree, language);
711
+ const callRouter = callRouters[language];
712
+ if (typeEnv.constructorBindings.length > 0) {
713
+ result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
714
+ }
715
+ let matches;
716
+ try {
717
+ matches = query.matches(tree.rootNode);
718
+ }
719
+ catch (err) {
720
+ console.warn(`Query execution failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
721
+ continue;
722
+ }
723
+ for (const match of matches) {
724
+ const captureMap = {};
725
+ for (const c of match.captures) {
726
+ captureMap[c.name] = c.node;
727
+ }
728
+ // Extract import paths before skipping
729
+ if (captureMap['import'] && captureMap['import.source']) {
730
+ const rawImportPath = language === SupportedLanguages.Kotlin
731
+ ? appendKotlinWildcard(captureMap['import.source'].text.replace(/['"<>]/g, ''), captureMap['import'])
732
+ : captureMap['import.source'].text.replace(/['"<>]/g, '');
733
+ const namedBindings = extractNamedBindings(captureMap['import'], language);
734
+ result.imports.push({
735
+ filePath: file.path,
736
+ rawImportPath,
737
+ language: language,
738
+ ...(namedBindings ? { namedBindings } : {}),
739
+ });
740
+ continue;
741
+ }
742
+ // Extract call sites
743
+ if (captureMap['call']) {
744
+ const callNameNode = captureMap['call.name'];
745
+ if (callNameNode) {
746
+ const calledName = callNameNode.text;
747
+ // Dispatch: route language-specific calls (heritage, properties, imports)
748
+ const routed = callRouter(calledName, captureMap['call']);
749
+ if (routed) {
750
+ if (routed.kind === 'skip')
751
+ continue;
752
+ if (routed.kind === 'import') {
753
+ result.imports.push({
754
+ filePath: file.path,
755
+ rawImportPath: routed.importPath,
756
+ language,
757
+ });
758
+ continue;
759
+ }
760
+ if (routed.kind === 'heritage') {
761
+ for (const item of routed.items) {
762
+ result.heritage.push({
763
+ filePath: file.path,
764
+ className: item.enclosingClass,
765
+ parentName: item.mixinName,
766
+ kind: item.heritageKind,
767
+ });
768
+ }
769
+ continue;
770
+ }
771
+ if (routed.kind === 'properties') {
772
+ const propEnclosingClassId = findEnclosingClassId(captureMap['call'], file.path);
773
+ for (const item of routed.items) {
774
+ const nodeId = generateId('Property', `${file.path}:${item.propName}`);
775
+ result.nodes.push({
776
+ id: nodeId,
777
+ label: 'Property',
778
+ properties: {
779
+ name: item.propName,
780
+ filePath: file.path,
781
+ startLine: item.startLine,
782
+ endLine: item.endLine,
783
+ language,
784
+ isExported: true,
785
+ description: item.accessorType,
786
+ },
787
+ });
788
+ result.symbols.push({
789
+ filePath: file.path,
790
+ name: item.propName,
791
+ nodeId,
792
+ type: 'Property',
793
+ ...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
794
+ });
795
+ const fileId = generateId('File', file.path);
796
+ const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
797
+ result.relationships.push({
798
+ id: relId,
799
+ sourceId: fileId,
800
+ targetId: nodeId,
801
+ type: 'DEFINES',
802
+ confidence: 1.0,
803
+ reason: '',
804
+ });
805
+ if (propEnclosingClassId) {
806
+ result.relationships.push({
807
+ id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
808
+ sourceId: propEnclosingClassId,
809
+ targetId: nodeId,
810
+ type: 'HAS_METHOD',
811
+ confidence: 1.0,
812
+ reason: '',
813
+ });
814
+ }
815
+ }
816
+ continue;
817
+ }
818
+ // kind === 'call' — fall through to normal call processing below
819
+ }
820
+ if (!isBuiltInOrNoise(calledName)) {
821
+ const callNode = captureMap['call'];
822
+ const sourceId = findEnclosingFunctionId(callNode, file.path)
823
+ || generateId('File', file.path);
824
+ const callForm = inferCallForm(callNode, callNameNode);
825
+ let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
826
+ let receiverTypeName = receiverName ? typeEnv.lookup(receiverName, callNode) : undefined;
827
+ let receiverCallChain;
828
+ // When the receiver is a call_expression (e.g. svc.getUser().save()),
829
+ // extractReceiverName refuses complex expressions, so walk the receiver node
830
+ // to build a call chain for deferred resolution in processCallsFromExtracted
831
+ if (callForm === 'member' && receiverName === undefined && !receiverTypeName) {
832
+ const receiverNode = extractReceiverNode(callNameNode);
833
+ if (receiverNode && CALL_EXPRESSION_TYPES.has(receiverNode.type)) {
834
+ const extracted = extractCallChain(receiverNode);
835
+ if (extracted) {
836
+ receiverCallChain = extracted.chain;
837
+ // Set receiverName to the base object for constructor binding resolution
838
+ receiverName = extracted.baseReceiverName;
839
+ // Try the type environment immediately for explicitly-typed locals/params
840
+ if (receiverName) {
841
+ receiverTypeName = typeEnv.lookup(receiverName, callNode);
842
+ }
843
+ }
844
+ }
845
+ }
846
+ result.calls.push({
847
+ filePath: file.path,
848
+ calledName,
849
+ sourceId,
850
+ argCount: countCallArguments(callNode),
851
+ callLine: callNode.startPosition?.row != null ? callNode.startPosition.row + 1 : undefined,
852
+ ...(callForm !== undefined ? { callForm } : {}),
853
+ ...(receiverName !== undefined ? { receiverName } : {}),
854
+ ...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
855
+ ...(receiverCallChain !== undefined ? { receiverCallChain } : {}),
856
+ });
857
+ }
858
+ }
859
+ continue;
860
+ }
861
+ // Extract heritage (extends/implements)
862
+ if (captureMap['heritage.class']) {
863
+ if (captureMap['heritage.extends']) {
864
+ // Go struct embedding: only anonymous fields (no name) are embedded
865
+ // Named fields like `Breed string` also match — skip them
866
+ const extendsNode = captureMap['heritage.extends'];
867
+ const fieldDecl = extendsNode.parent;
868
+ const isNamedField = fieldDecl?.type === 'field_declaration'
869
+ && fieldDecl.childForFieldName('name');
870
+ if (!isNamedField) {
871
+ result.heritage.push({
872
+ filePath: file.path,
873
+ className: captureMap['heritage.class'].text,
874
+ parentName: captureMap['heritage.extends'].text,
875
+ kind: 'extends',
876
+ });
877
+ }
878
+ }
879
+ if (captureMap['heritage.implements']) {
880
+ result.heritage.push({
881
+ filePath: file.path,
882
+ className: captureMap['heritage.class'].text,
883
+ parentName: captureMap['heritage.implements'].text,
884
+ kind: 'implements',
885
+ });
886
+ }
887
+ if (captureMap['heritage.trait']) {
888
+ result.heritage.push({
889
+ filePath: file.path,
890
+ className: captureMap['heritage.class'].text,
891
+ parentName: captureMap['heritage.trait'].text,
892
+ kind: 'trait-impl',
893
+ });
894
+ }
895
+ if (captureMap['heritage.extends'] || captureMap['heritage.implements'] || captureMap['heritage.trait']) {
896
+ continue;
897
+ }
898
+ }
899
+ const nodeLabel = getLabelFromCaptures(captureMap);
900
+ if (!nodeLabel)
901
+ continue;
902
+ const nameNode = captureMap['name'];
903
+ // Synthesize name for constructors without explicit @name capture (e.g. Swift init)
904
+ if (!nameNode && nodeLabel !== 'Constructor')
905
+ continue;
906
+ const nodeName = nameNode ? nameNode.text : 'init';
907
+ const definitionNode = getDefinitionNodeFromCaptures(captureMap);
908
+ const startLine = definitionNode ? definitionNode.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
909
+ const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
910
+ let description;
911
+ if (language === SupportedLanguages.PHP) {
912
+ if (nodeLabel === 'Property' && captureMap['definition.property']) {
913
+ description = extractPhpPropertyDescription(nodeName, captureMap['definition.property']) ?? undefined;
914
+ }
915
+ else if (nodeLabel === 'Method' && captureMap['definition.method']) {
916
+ description = extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
917
+ }
918
+ }
919
+ const frameworkHint = definitionNode
920
+ ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
921
+ : null;
922
+ let parameterCount;
923
+ let returnType;
924
+ let parameterTypes;
925
+ if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
926
+ const sig = extractMethodSignature(definitionNode);
927
+ parameterCount = sig.parameterCount;
928
+ returnType = sig.returnType;
929
+ parameterTypes = sig.parameterTypes;
930
+ // Language-specific return type fallback (e.g. Ruby YARD @return [Type])
931
+ if (!returnType && definitionNode) {
932
+ const tc = typeConfigs[language];
933
+ if (tc?.extractReturnType) {
934
+ returnType = tc.extractReturnType(definitionNode);
935
+ }
936
+ }
937
+ }
938
+ result.nodes.push({
939
+ id: nodeId,
940
+ label: nodeLabel,
941
+ properties: {
942
+ name: nodeName,
943
+ filePath: file.path,
944
+ startLine: definitionNode ? definitionNode.startPosition.row : startLine,
945
+ endLine: definitionNode ? definitionNode.endPosition.row : startLine,
946
+ language: language,
947
+ isExported: isNodeExported(nameNode || definitionNode, nodeName, language),
948
+ ...(frameworkHint ? {
949
+ astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
950
+ astFrameworkReason: frameworkHint.reason,
951
+ } : {}),
952
+ ...(description !== undefined ? { description } : {}),
953
+ ...(parameterCount !== undefined ? { parameterCount } : {}),
954
+ ...(returnType !== undefined ? { returnType } : {}),
955
+ },
956
+ });
957
+ // Compute enclosing class for Method/Constructor/Property/Function — used for both ownerId and HAS_METHOD
958
+ // Function is included because Kotlin/Rust/Python capture class methods as Function nodes
959
+ const needsOwner = nodeLabel === 'Method' || nodeLabel === 'Constructor' || nodeLabel === 'Property' || nodeLabel === 'Function';
960
+ const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNode, file.path) : null;
961
+ result.symbols.push({
962
+ filePath: file.path,
963
+ name: nodeName,
964
+ nodeId,
965
+ type: nodeLabel,
966
+ ...(parameterCount !== undefined ? { parameterCount } : {}),
967
+ ...(returnType !== undefined ? { returnType } : {}),
968
+ ...(parameterTypes && parameterTypes.length > 0 ? { parameterTypes } : {}),
969
+ ...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
970
+ });
971
+ const fileId = generateId('File', file.path);
972
+ const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
973
+ result.relationships.push({
974
+ id: relId,
975
+ sourceId: fileId,
976
+ targetId: nodeId,
977
+ type: 'DEFINES',
978
+ confidence: 1.0,
979
+ reason: '',
980
+ });
981
+ // HAS_METHOD: link method/constructor/property to enclosing class
982
+ if (enclosingClassId) {
983
+ result.relationships.push({
984
+ id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`),
985
+ sourceId: enclosingClassId,
986
+ targetId: nodeId,
987
+ type: 'HAS_METHOD',
988
+ confidence: 1.0,
989
+ reason: '',
990
+ });
991
+ }
992
+ }
993
+ // Extract Laravel routes from route files via procedural AST walk
994
+ if (language === SupportedLanguages.PHP && (file.path.includes('/routes/') || file.path.startsWith('routes/')) && file.path.endsWith('.php')) {
995
+ const extractedRoutes = extractLaravelRoutes(tree, file.path);
996
+ result.routes.push(...extractedRoutes);
997
+ }
998
+ }
999
+ };
1000
+ // Worker message handler (supports sub-batch streaming)
1001
+ // Accumulated result across sub-batches
1002
+ let accumulated = {
1003
+ nodes: [], relationships: [], symbols: [],
1004
+ imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0,
1005
+ };
1006
+ let cumulativeProcessed = 0;
1007
+ const mergeResult = (target, src) => {
1008
+ target.nodes.push(...src.nodes);
1009
+ target.relationships.push(...src.relationships);
1010
+ target.symbols.push(...src.symbols);
1011
+ target.imports.push(...src.imports);
1012
+ target.calls.push(...src.calls);
1013
+ target.heritage.push(...src.heritage);
1014
+ target.routes.push(...src.routes);
1015
+ target.constructorBindings.push(...src.constructorBindings);
1016
+ for (const [lang, count] of Object.entries(src.skippedLanguages)) {
1017
+ target.skippedLanguages[lang] = (target.skippedLanguages[lang] || 0) + count;
1018
+ }
1019
+ target.fileCount += src.fileCount;
1020
+ };
1021
+ parentPort.on('message', (msg) => {
1022
+ try {
1023
+ // Sub-batch mode: { type: 'sub-batch', files: [...] }
1024
+ if (msg && msg.type === 'sub-batch') {
1025
+ const result = processBatch(msg.files, (filesProcessed) => {
1026
+ parentPort.postMessage({ type: 'progress', filesProcessed: cumulativeProcessed + filesProcessed });
1027
+ });
1028
+ cumulativeProcessed += result.fileCount;
1029
+ mergeResult(accumulated, result);
1030
+ // Signal ready for next sub-batch
1031
+ parentPort.postMessage({ type: 'sub-batch-done' });
1032
+ return;
1033
+ }
1034
+ // Flush: send accumulated results
1035
+ if (msg && msg.type === 'flush') {
1036
+ parentPort.postMessage({ type: 'result', data: accumulated });
1037
+ // Reset for potential reuse
1038
+ accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0 };
1039
+ cumulativeProcessed = 0;
1040
+ return;
1041
+ }
1042
+ // Legacy single-message mode (backward compat): array of files
1043
+ if (Array.isArray(msg)) {
1044
+ const result = processBatch(msg, (filesProcessed) => {
1045
+ parentPort.postMessage({ type: 'progress', filesProcessed });
1046
+ });
1047
+ parentPort.postMessage({ type: 'result', data: result });
1048
+ return;
1049
+ }
1050
+ }
1051
+ catch (err) {
1052
+ const message = err instanceof Error ? err.message : String(err);
1053
+ parentPort.postMessage({ type: 'error', error: message });
1054
+ }
1055
+ });