@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.
- package/README.md +215 -0
- package/dist/cli/ai-context.d.ts +19 -0
- package/dist/cli/ai-context.js +168 -0
- package/dist/cli/analyze.d.ts +7 -0
- package/dist/cli/analyze.js +325 -0
- package/dist/cli/augment.d.ts +7 -0
- package/dist/cli/augment.js +27 -0
- package/dist/cli/clean.d.ts +5 -0
- package/dist/cli/clean.js +56 -0
- package/dist/cli/eval-server.d.ts +25 -0
- package/dist/cli/eval-server.js +365 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +102 -0
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +19 -0
- package/dist/cli/list.d.ts +2 -0
- package/dist/cli/list.js +27 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +35 -0
- package/dist/cli/refresh.d.ts +12 -0
- package/dist/cli/refresh.js +165 -0
- package/dist/cli/serve.d.ts +5 -0
- package/dist/cli/serve.js +8 -0
- package/dist/cli/setup.d.ts +6 -0
- package/dist/cli/setup.js +218 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +33 -0
- package/dist/cli/tool.d.ts +28 -0
- package/dist/cli/tool.js +87 -0
- package/dist/config/ignore-service.d.ts +32 -0
- package/dist/config/ignore-service.js +282 -0
- package/dist/config/supported-languages.d.ts +23 -0
- package/dist/config/supported-languages.js +52 -0
- package/dist/core/augmentation/engine.d.ts +22 -0
- package/dist/core/augmentation/engine.js +232 -0
- package/dist/core/embeddings/embedder.d.ts +35 -0
- package/dist/core/embeddings/embedder.js +171 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +41 -0
- package/dist/core/embeddings/embedding-pipeline.js +402 -0
- package/dist/core/embeddings/index.d.ts +5 -0
- package/dist/core/embeddings/index.js +6 -0
- package/dist/core/embeddings/text-generator.d.ts +20 -0
- package/dist/core/embeddings/text-generator.js +159 -0
- package/dist/core/embeddings/types.d.ts +60 -0
- package/dist/core/embeddings/types.js +23 -0
- package/dist/core/graph/graph.d.ts +4 -0
- package/dist/core/graph/graph.js +65 -0
- package/dist/core/graph/types.d.ts +69 -0
- package/dist/core/graph/types.js +3 -0
- package/dist/core/incremental/child-process.d.ts +8 -0
- package/dist/core/incremental/child-process.js +649 -0
- package/dist/core/incremental/refresh-coordinator.d.ts +32 -0
- package/dist/core/incremental/refresh-coordinator.js +147 -0
- package/dist/core/incremental/types.d.ts +78 -0
- package/dist/core/incremental/types.js +153 -0
- package/dist/core/incremental/watcher.d.ts +63 -0
- package/dist/core/incremental/watcher.js +338 -0
- package/dist/core/ingestion/ast-cache.d.ts +12 -0
- package/dist/core/ingestion/ast-cache.js +34 -0
- package/dist/core/ingestion/call-processor.d.ts +34 -0
- package/dist/core/ingestion/call-processor.js +937 -0
- package/dist/core/ingestion/call-routing.d.ts +40 -0
- package/dist/core/ingestion/call-routing.js +97 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +30 -0
- package/dist/core/ingestion/cluster-enricher.js +151 -0
- package/dist/core/ingestion/community-processor.d.ts +26 -0
- package/dist/core/ingestion/community-processor.js +272 -0
- package/dist/core/ingestion/constants.d.ts +5 -0
- package/dist/core/ingestion/constants.js +8 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +23 -0
- package/dist/core/ingestion/entry-point-scoring.js +317 -0
- package/dist/core/ingestion/export-detection.d.ts +11 -0
- package/dist/core/ingestion/export-detection.js +203 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +18 -0
- package/dist/core/ingestion/filesystem-walker.js +64 -0
- package/dist/core/ingestion/framework-detection.d.ts +42 -0
- package/dist/core/ingestion/framework-detection.js +405 -0
- package/dist/core/ingestion/heritage-processor.d.ts +15 -0
- package/dist/core/ingestion/heritage-processor.js +237 -0
- package/dist/core/ingestion/import-processor.d.ts +31 -0
- package/dist/core/ingestion/import-processor.js +416 -0
- package/dist/core/ingestion/language-config.d.ts +32 -0
- package/dist/core/ingestion/language-config.js +161 -0
- package/dist/core/ingestion/mro-processor.d.ts +32 -0
- package/dist/core/ingestion/mro-processor.js +343 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +51 -0
- package/dist/core/ingestion/named-binding-extraction.js +343 -0
- package/dist/core/ingestion/parsing-processor.d.ts +20 -0
- package/dist/core/ingestion/parsing-processor.js +282 -0
- package/dist/core/ingestion/pipeline.d.ts +3 -0
- package/dist/core/ingestion/pipeline.js +416 -0
- package/dist/core/ingestion/process-processor.d.ts +42 -0
- package/dist/core/ingestion/process-processor.js +357 -0
- package/dist/core/ingestion/resolution-context.d.ts +40 -0
- package/dist/core/ingestion/resolution-context.js +171 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +10 -0
- package/dist/core/ingestion/resolvers/csharp.js +101 -0
- package/dist/core/ingestion/resolvers/go.d.ts +8 -0
- package/dist/core/ingestion/resolvers/go.js +33 -0
- package/dist/core/ingestion/resolvers/index.d.ts +14 -0
- package/dist/core/ingestion/resolvers/index.js +10 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +9 -0
- package/dist/core/ingestion/resolvers/jvm.js +74 -0
- package/dist/core/ingestion/resolvers/php.d.ts +7 -0
- package/dist/core/ingestion/resolvers/php.js +30 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +9 -0
- package/dist/core/ingestion/resolvers/ruby.js +13 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +5 -0
- package/dist/core/ingestion/resolvers/rust.js +62 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +16 -0
- package/dist/core/ingestion/resolvers/standard.js +144 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +18 -0
- package/dist/core/ingestion/resolvers/utils.js +113 -0
- package/dist/core/ingestion/structure-processor.d.ts +4 -0
- package/dist/core/ingestion/structure-processor.js +39 -0
- package/dist/core/ingestion/symbol-table.d.ts +34 -0
- package/dist/core/ingestion/symbol-table.js +48 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +20 -0
- package/dist/core/ingestion/tree-sitter-queries.js +691 -0
- package/dist/core/ingestion/type-env.d.ts +52 -0
- package/dist/core/ingestion/type-env.js +349 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +214 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/csharp.js +224 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/go.js +261 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +20 -0
- package/dist/core/ingestion/type-extractors/index.js +30 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/jvm.js +386 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/php.js +280 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/python.js +175 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +12 -0
- package/dist/core/ingestion/type-extractors/ruby.js +218 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/rust.js +290 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +81 -0
- package/dist/core/ingestion/type-extractors/shared.js +322 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/swift.js +140 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +111 -0
- package/dist/core/ingestion/type-extractors/types.js +4 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/typescript.js +227 -0
- package/dist/core/ingestion/utils.d.ts +73 -0
- package/dist/core/ingestion/utils.js +992 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +99 -0
- package/dist/core/ingestion/workers/parse-worker.js +1055 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
- package/dist/core/ingestion/workers/worker-pool.js +123 -0
- package/dist/core/lbug/csv-generator.d.ts +28 -0
- package/dist/core/lbug/csv-generator.js +355 -0
- package/dist/core/lbug/lbug-adapter.d.ts +96 -0
- package/dist/core/lbug/lbug-adapter.js +753 -0
- package/dist/core/lbug/schema.d.ts +46 -0
- package/dist/core/lbug/schema.js +402 -0
- package/dist/core/search/bm25-index.d.ts +20 -0
- package/dist/core/search/bm25-index.js +123 -0
- package/dist/core/search/hybrid-search.d.ts +32 -0
- package/dist/core/search/hybrid-search.js +131 -0
- package/dist/core/search/query-cache.d.ts +18 -0
- package/dist/core/search/query-cache.js +47 -0
- package/dist/core/search/query-expansion.d.ts +19 -0
- package/dist/core/search/query-expansion.js +75 -0
- package/dist/core/search/reranker.d.ts +29 -0
- package/dist/core/search/reranker.js +122 -0
- package/dist/core/search/types.d.ts +154 -0
- package/dist/core/search/types.js +51 -0
- package/dist/core/semantic/tsgo-service.d.ts +67 -0
- package/dist/core/semantic/tsgo-service.js +355 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +12 -0
- package/dist/core/tree-sitter/parser-loader.js +71 -0
- package/dist/lib/memory-guard.d.ts +35 -0
- package/dist/lib/memory-guard.js +70 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +6 -0
- package/dist/mcp/compatible-stdio-transport.d.ts +32 -0
- package/dist/mcp/compatible-stdio-transport.js +209 -0
- package/dist/mcp/core/embedder.d.ts +24 -0
- package/dist/mcp/core/embedder.js +168 -0
- package/dist/mcp/core/lbug-adapter.d.ts +29 -0
- package/dist/mcp/core/lbug-adapter.js +330 -0
- package/dist/mcp/local/local-backend.d.ts +188 -0
- package/dist/mcp/local/local-backend.js +2759 -0
- package/dist/mcp/resources.d.ts +22 -0
- package/dist/mcp/resources.js +379 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +217 -0
- package/dist/mcp/staleness.d.ts +10 -0
- package/dist/mcp/staleness.js +25 -0
- package/dist/mcp/tools.d.ts +21 -0
- package/dist/mcp/tools.js +202 -0
- package/dist/server/api.d.ts +5 -0
- package/dist/server/api.js +340 -0
- package/dist/server/mcp-http.d.ts +7 -0
- package/dist/server/mcp-http.js +95 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +35 -0
- package/dist/storage/repo-manager.d.ts +87 -0
- package/dist/storage/repo-manager.js +249 -0
- package/dist/types/pipeline.d.ts +35 -0
- package/dist/types/pipeline.js +20 -0
- package/hooks/claude/code-mapper-hook.cjs +238 -0
- package/hooks/claude/pre-tool-use.sh +79 -0
- package/hooks/claude/session-start.sh +42 -0
- package/models/mlx-embedder.py +185 -0
- package/package.json +100 -0
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/vendor/leiden/index.cjs +355 -0
- 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
|
+
});
|