graph-indexer 2.1.0 → 3.0.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/CHANGELOG.md +260 -1
- package/README.md +232 -312
- package/SECURITY.md +37 -59
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +124 -0
- package/package.json +31 -105
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +356 -0
- package/src/cli/init.mjs +255 -0
- package/src/cli/resident.mjs +175 -0
- package/src/index/discover.mjs +130 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +331 -0
- package/src/index/resolver.mjs +954 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +973 -0
- package/src/parse/arity.mjs +152 -0
- package/src/parse/extract.mjs +1031 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +343 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +680 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +119 -0
- package/src/query/textsearch.mjs +160 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/paths.mjs +24 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- package/watch-daemon.mjs +0 -289
package/parse/routes.mjs
DELETED
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file parse/routes.mjs
|
|
3
|
-
* @description HTTP route extraction from AST (Express/NestJS/FastAPI/Flask/Spring Boot).
|
|
4
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
5
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
6
|
-
* @license MIT
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// ─── HTTP route extraction ────────────────────────────────────────────────────
|
|
10
|
-
// The call graph records that a function was called, but cannot connect an HTTP
|
|
11
|
-
// verb+path (`GET /api/users`) to the handler that serves it. extractRoutes mines
|
|
12
|
-
// that mapping at index time from the four common shapes:
|
|
13
|
-
// • decorator-on-method — NestJS (@Get/@Post on a class method)
|
|
14
|
-
// • decorator-on-function — FastAPI/Flask (@app.get / @app.route)
|
|
15
|
-
// • annotation-on-method — Spring Boot (@GetMapping / @RequestMapping)
|
|
16
|
-
// • functional registration — Express/Koa (router.get('/x', handler))
|
|
17
|
-
// Each route resolves its handler to a chunk id (by name, case-sensitive) so an
|
|
18
|
-
// agent can jump straight from an endpoint to get_chunk / get_call_graph on the
|
|
19
|
-
// handler. No framework names are hardcoded beyond the verb sets — detection is by
|
|
20
|
-
// AST node type, mirroring extractDecorators / extractCallSites.
|
|
21
|
-
|
|
22
|
-
const HTTP_VERBS = new Set(['get', 'post', 'put', 'delete', 'patch', 'all', 'head', 'options']);
|
|
23
|
-
const ROUTE_JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
24
|
-
|
|
25
|
-
/** Unquote a string-literal node across grammars (JS string_fragment, Python
|
|
26
|
-
* string_content, Java string_fragment, …). Returns null for non-string nodes. */
|
|
27
|
-
function _routeLiteral(node) {
|
|
28
|
-
if (!node) return null;
|
|
29
|
-
const STR = new Set(['string', 'template_string', 'string_literal',
|
|
30
|
-
'interpreted_string_literal', 'raw_string_literal']);
|
|
31
|
-
if (!STR.has(node.type)) return null;
|
|
32
|
-
const frag = node.namedChildren?.find(c => /fragment|content/.test(c.type));
|
|
33
|
-
if (frag) return frag.text;
|
|
34
|
-
return node.text.replace(/^[`'"]+|[`'"]+$/g, '');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Join a controller/class path prefix to a method path. Empty prefix → path as-is
|
|
38
|
-
* (spec: "if no Controller prefix, leave it as-is"). Collapses duplicate slashes. */
|
|
39
|
-
function _joinRoutePath(prefix, sub) {
|
|
40
|
-
prefix = (prefix || '').trim();
|
|
41
|
-
sub = (sub || '').trim();
|
|
42
|
-
if (!prefix) return sub;
|
|
43
|
-
if (!sub) return prefix;
|
|
44
|
-
return (prefix.replace(/\/+$/, '') + '/' + sub.replace(/^\/+/, '')).replace(/\/{2,}/g, '/');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Decorators that lead a node as siblings (mirrors extractDecorators' sibling walk).
|
|
48
|
-
* NestJS method decorators sit as previous siblings inside the class_body. */
|
|
49
|
-
function _leadingDecorators(node) {
|
|
50
|
-
const out = [];
|
|
51
|
-
let prev = node.previousSibling;
|
|
52
|
-
while (prev) {
|
|
53
|
-
if (prev.type === 'decorator') out.push(prev);
|
|
54
|
-
else if (prev.isNamed && prev.type !== 'comment') break;
|
|
55
|
-
prev = prev.previousSibling;
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Parse a TS/JS decorator node into { name, path } — name is the callee (`Get`,
|
|
61
|
-
* `Controller`), path is its first string argument (or null for bare `@Get()`). */
|
|
62
|
-
function _parseTsDecorator(decoNode) {
|
|
63
|
-
const call = decoNode.namedChildren?.find(c => c.type === 'call_expression');
|
|
64
|
-
if (!call) {
|
|
65
|
-
const id = decoNode.namedChildren?.find(c => c.type === 'identifier');
|
|
66
|
-
return { name: id?.text || decoNode.text.replace(/^@/, '').split(/[\s(]/)[0], path: null };
|
|
67
|
-
}
|
|
68
|
-
const fn = call.childForFieldName?.('function');
|
|
69
|
-
let name = '';
|
|
70
|
-
if (fn?.type === 'identifier') name = fn.text;
|
|
71
|
-
else if (fn?.type === 'member_expression') name = fn.childForFieldName?.('property')?.text || '';
|
|
72
|
-
const args = call.childForFieldName?.('arguments');
|
|
73
|
-
const firstStr = args?.namedChildren?.find(c => _routeLiteral(c) != null);
|
|
74
|
-
return { name, path: _routeLiteral(firstStr) };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** NestJS: @Get/@Post/… on class methods, with the enclosing
|
|
78
|
-
* @Controller(prefix) prepended. */
|
|
79
|
-
function _extractNestRoutes(rootNode, emit) {
|
|
80
|
-
const stack = [rootNode];
|
|
81
|
-
while (stack.length) {
|
|
82
|
-
const n = stack.pop();
|
|
83
|
-
if (n.type === 'class_declaration' || n.type === 'abstract_class_declaration') {
|
|
84
|
-
// For `export class` the @Controller decorator is a leading sibling inside
|
|
85
|
-
// export_statement; for bare/abstract class it is a direct child before class_body.
|
|
86
|
-
const classDecos = [..._leadingDecorators(n)];
|
|
87
|
-
for (let i = 0; i < n.namedChildCount; i++) {
|
|
88
|
-
const c = n.namedChild(i);
|
|
89
|
-
if (c.type === 'class_body') break;
|
|
90
|
-
if (c.type === 'decorator') classDecos.push(c);
|
|
91
|
-
}
|
|
92
|
-
let prefix = '';
|
|
93
|
-
for (const d of classDecos) {
|
|
94
|
-
const { name, path } = _parseTsDecorator(d);
|
|
95
|
-
if (name === 'Controller' && path != null) { prefix = path; break; }
|
|
96
|
-
}
|
|
97
|
-
const body = n.childForFieldName?.('body')
|
|
98
|
-
|| n.namedChildren?.find(c => c.type === 'class_body');
|
|
99
|
-
if (body) {
|
|
100
|
-
for (let i = 0; i < body.namedChildCount; i++) {
|
|
101
|
-
const member = body.namedChild(i);
|
|
102
|
-
if (member.type !== 'method_definition') continue;
|
|
103
|
-
const handler = member.childForFieldName?.('name')?.text
|
|
104
|
-
|| member.namedChildren?.find(c => c.type === 'property_identifier')?.text;
|
|
105
|
-
for (const d of _leadingDecorators(member)) {
|
|
106
|
-
const { name, path } = _parseTsDecorator(d);
|
|
107
|
-
if (!name || !HTTP_VERBS.has(name.toLowerCase())) continue;
|
|
108
|
-
emit(name, _joinRoutePath(prefix, path), handler, member.startPosition.row + 1, 'nestjs');
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Derive a handler name from an Express argument node: a bare identifier, a member
|
|
118
|
-
* expression (`ctrl.getThing` → 'getThing', `this.handler` → 'handler'), or a
|
|
119
|
-
* `.bind()` call (`handler.bind(this)` → 'handler'). Arrow/inline fn → 'anonymous'. */
|
|
120
|
-
function _jsHandlerName(node) {
|
|
121
|
-
if (!node) return 'anonymous';
|
|
122
|
-
if (node.type === 'identifier') return node.text;
|
|
123
|
-
if (node.type === 'member_expression') {
|
|
124
|
-
return node.childForFieldName?.('property')?.text || 'anonymous';
|
|
125
|
-
}
|
|
126
|
-
if (node.type === 'call_expression') {
|
|
127
|
-
const callee = node.childForFieldName?.('function');
|
|
128
|
-
if (callee?.type === 'member_expression') {
|
|
129
|
-
const obj = callee.childForFieldName?.('object');
|
|
130
|
-
if (obj?.type === 'identifier') return obj.text; // handler.bind(this) → handler
|
|
131
|
-
if (obj?.type === 'member_expression') return obj.childForFieldName?.('property')?.text || 'anonymous';
|
|
132
|
-
return callee.childForFieldName?.('property')?.text || 'anonymous';
|
|
133
|
-
}
|
|
134
|
-
if (callee?.type === 'identifier') return callee.text; // makeHandler() → makeHandler
|
|
135
|
-
}
|
|
136
|
-
return 'anonymous'; // arrow_function / function / function_expression / unknown
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Express / Koa: `<router|app>.<verb>(<path>, …, <handler>)` call sites. The route
|
|
140
|
-
* path must be rooted ('/…') or a wildcard ('*') — this matches Express/Koa
|
|
141
|
-
* convention and rejects look-alike calls (`cache.get('key', cb)`, `map.get(k)`). */
|
|
142
|
-
function _extractExpressRoutes(rootNode, emit) {
|
|
143
|
-
const stack = [rootNode];
|
|
144
|
-
while (stack.length) {
|
|
145
|
-
const n = stack.pop();
|
|
146
|
-
if (n.type === 'call_expression') {
|
|
147
|
-
const fn = n.childForFieldName?.('function');
|
|
148
|
-
if (fn?.type === 'member_expression') {
|
|
149
|
-
const verb = fn.childForFieldName?.('property')?.text || '';
|
|
150
|
-
const args = n.childForFieldName?.('arguments');
|
|
151
|
-
if (verb && HTTP_VERBS.has(verb.toLowerCase()) && args) {
|
|
152
|
-
const named = args.namedChildren || [];
|
|
153
|
-
const routePath = _routeLiteral(named[0]);
|
|
154
|
-
if (routePath != null && /^[/*]/.test(routePath) && named.length >= 2) {
|
|
155
|
-
// The handler is the LAST argument — preceding args may be middleware.
|
|
156
|
-
const handler = _jsHandlerName(named[named.length - 1]);
|
|
157
|
-
emit(verb, routePath, handler, n.startPosition.row + 1, 'express');
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** FastAPI / Flask: @app.get / @router.post / @app.route(path, methods=[…]). */
|
|
167
|
-
function _extractPyRoutes(rootNode, emit) {
|
|
168
|
-
const stack = [rootNode];
|
|
169
|
-
while (stack.length) {
|
|
170
|
-
const n = stack.pop();
|
|
171
|
-
if (n.type === 'decorated_definition') {
|
|
172
|
-
const def = n.namedChildren?.find(c => c.type === 'function_definition');
|
|
173
|
-
const handler = def?.childForFieldName?.('name')?.text
|
|
174
|
-
|| def?.namedChildren?.find(c => c.type === 'identifier')?.text;
|
|
175
|
-
const line = (def || n).startPosition.row + 1;
|
|
176
|
-
for (const d of n.namedChildren || []) {
|
|
177
|
-
if (d.type !== 'decorator') continue;
|
|
178
|
-
const call = d.namedChildren?.find(c => c.type === 'call');
|
|
179
|
-
const fn = call?.childForFieldName?.('function');
|
|
180
|
-
if (fn?.type !== 'attribute') continue;
|
|
181
|
-
const verb = fn.childForFieldName?.('attribute')?.text || '';
|
|
182
|
-
const args = call.childForFieldName?.('arguments');
|
|
183
|
-
// FastAPI uses a positional path string; Flask uses `path=`/`rule=` keyword argument.
|
|
184
|
-
let routePath = _routeLiteral(args?.namedChildren?.find(c => c.type === 'string'));
|
|
185
|
-
if (routePath == null) {
|
|
186
|
-
const kw = args?.namedChildren?.find(c => c.type === 'keyword_argument'
|
|
187
|
-
&& ['path', 'rule'].includes(c.childForFieldName?.('name')?.text));
|
|
188
|
-
routePath = _routeLiteral(kw?.childForFieldName?.('value'));
|
|
189
|
-
}
|
|
190
|
-
// Require a rooted path string to reject look-alike decorators such as
|
|
191
|
-
// `@cache.get("key")` or `@retry.post(n=3)` that share verb names.
|
|
192
|
-
const isRoute = routePath != null && routePath.startsWith('/');
|
|
193
|
-
if (HTTP_VERBS.has(verb.toLowerCase())) {
|
|
194
|
-
if (isRoute) emit(verb, routePath, handler, line, 'fastapi');
|
|
195
|
-
} else if (verb === 'route' && isRoute) {
|
|
196
|
-
// Flask `@app.route` can declare multiple HTTP methods; default GET when `methods` is omitted.
|
|
197
|
-
const kw = args?.namedChildren?.find(c => c.type === 'keyword_argument'
|
|
198
|
-
&& c.childForFieldName?.('name')?.text === 'methods');
|
|
199
|
-
const list = kw?.childForFieldName?.('value');
|
|
200
|
-
const methods = list
|
|
201
|
-
? (list.namedChildren || []).map(_routeLiteral).filter(Boolean)
|
|
202
|
-
: [];
|
|
203
|
-
for (const m of (methods.length ? methods : ['GET'])) {
|
|
204
|
-
emit(m, routePath, handler, line, 'flask');
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Spring Boot: @GetMapping/@PostMapping/… and @RequestMapping(value=…, method=…)
|
|
214
|
-
* on methods, with class-level @RequestMapping as a prefix. */
|
|
215
|
-
function _springAnnotation(annNode) {
|
|
216
|
-
// annotation > identifier <name> + annotation_argument_list
|
|
217
|
-
const name = annNode.namedChildren?.find(c => c.type === 'identifier')?.text || '';
|
|
218
|
-
const argList = annNode.namedChildren?.find(c => c.type === 'annotation_argument_list');
|
|
219
|
-
const verbOf = (t) => (t || '').split('.').pop(); // RequestMethod.POST → POST
|
|
220
|
-
let path = null, methods = [];
|
|
221
|
-
if (argList) {
|
|
222
|
-
const positional = argList.namedChildren?.find(c => _routeLiteral(c) != null);
|
|
223
|
-
if (positional) path = _routeLiteral(positional);
|
|
224
|
-
for (const pair of argList.namedChildren || []) {
|
|
225
|
-
if (pair.type !== 'element_value_pair') continue;
|
|
226
|
-
const key = pair.namedChildren?.[0]?.text;
|
|
227
|
-
const valNode = pair.namedChildren?.[1];
|
|
228
|
-
if (key === 'value' || key === 'path') path = _routeLiteral(valNode) ?? path;
|
|
229
|
-
else if (key === 'method') {
|
|
230
|
-
// `method` may be a single value or an array literal — handle both shapes.
|
|
231
|
-
methods = (valNode?.type === 'element_value_array_initializer'
|
|
232
|
-
? (valNode.namedChildren || []).map(c => verbOf(c.text))
|
|
233
|
-
: [verbOf(valNode?.text)]).filter(Boolean);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return { name, path, methods };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const SPRING_VERB = {
|
|
241
|
-
GetMapping: 'GET', PostMapping: 'POST', PutMapping: 'PUT',
|
|
242
|
-
DeleteMapping: 'DELETE', PatchMapping: 'PATCH',
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
function _extractSpringRoutes(rootNode, emit) {
|
|
246
|
-
const stack = [rootNode];
|
|
247
|
-
while (stack.length) {
|
|
248
|
-
const n = stack.pop();
|
|
249
|
-
if (n.type === 'class_declaration') {
|
|
250
|
-
const mods = n.namedChildren?.find(c => c.type === 'modifiers');
|
|
251
|
-
let prefix = '';
|
|
252
|
-
for (const a of (mods?.namedChildren || [])) {
|
|
253
|
-
if (a.type !== 'annotation') continue;
|
|
254
|
-
const { name, path } = _springAnnotation(a);
|
|
255
|
-
if (name === 'RequestMapping' && path != null) { prefix = path; break; }
|
|
256
|
-
}
|
|
257
|
-
const body = n.namedChildren?.find(c => c.type === 'class_body');
|
|
258
|
-
for (const member of (body?.namedChildren || [])) {
|
|
259
|
-
if (member.type !== 'method_declaration') continue;
|
|
260
|
-
const handler = member.childForFieldName?.('name')?.text;
|
|
261
|
-
const mMods = member.namedChildren?.find(c => c.type === 'modifiers');
|
|
262
|
-
for (const a of (mMods?.namedChildren || [])) {
|
|
263
|
-
if (a.type !== 'annotation' && a.type !== 'marker_annotation') continue;
|
|
264
|
-
const { name, path, methods } = _springAnnotation(a);
|
|
265
|
-
let verbs;
|
|
266
|
-
if (SPRING_VERB[name]) verbs = [SPRING_VERB[name]];
|
|
267
|
-
else if (name === 'RequestMapping') verbs = methods.length ? methods : ['ALL'];
|
|
268
|
-
else continue;
|
|
269
|
-
// @RequestMapping(method = {GET, POST}) → one route per verb.
|
|
270
|
-
for (const verb of verbs) {
|
|
271
|
-
emit(verb, _joinRoutePath(prefix, path), handler, member.startPosition.row + 1, 'spring');
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Map HTTP routes to their handlers for one parsed file.
|
|
282
|
-
*
|
|
283
|
-
* @param {object} rootNode parsed AST root
|
|
284
|
-
* @param {string} relPath repo-relative file path (stored on each route)
|
|
285
|
-
* @param {object[]} chunks the file's already-extracted chunks (handler→id lookup)
|
|
286
|
-
* @param {string} ext dotted file extension
|
|
287
|
-
* @returns {Array<{method,path,handler_name,handler_chunk_id,file_path,line,framework}>}
|
|
288
|
-
*/
|
|
289
|
-
export function extractRoutes(rootNode, relPath, chunks, ext) {
|
|
290
|
-
const routes = [];
|
|
291
|
-
if (!rootNode) return routes;
|
|
292
|
-
|
|
293
|
-
// Case-sensitive; first definition wins to match the chunk extraction order.
|
|
294
|
-
const idByName = new Map();
|
|
295
|
-
for (const c of (chunks || [])) {
|
|
296
|
-
if (c && c.name && !idByName.has(c.name)) idByName.set(c.name, c.id);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const emit = (method, routePath, handlerName, line, framework) => {
|
|
300
|
-
if (!method) return;
|
|
301
|
-
// NestJS controller-prefixed paths omit the leading slash ('users/:id'); normalise
|
|
302
|
-
// so a '/'-prefix query matches uniformly across all frameworks.
|
|
303
|
-
let p = routePath == null ? '' : String(routePath);
|
|
304
|
-
if (p && !p.startsWith('/')) p = '/' + p;
|
|
305
|
-
routes.push({
|
|
306
|
-
method: String(method).toUpperCase(),
|
|
307
|
-
path: p,
|
|
308
|
-
handler_name: handlerName || 'anonymous',
|
|
309
|
-
handler_chunk_id: (handlerName && idByName.has(handlerName)) ? idByName.get(handlerName) : null,
|
|
310
|
-
file_path: relPath,
|
|
311
|
-
line,
|
|
312
|
-
framework,
|
|
313
|
-
});
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
if (ROUTE_JS_LIKE.includes(ext)) {
|
|
318
|
-
_extractNestRoutes(rootNode, emit);
|
|
319
|
-
_extractExpressRoutes(rootNode, emit);
|
|
320
|
-
} else if (ext === '.py') {
|
|
321
|
-
_extractPyRoutes(rootNode, emit);
|
|
322
|
-
} else if (ext === '.java') {
|
|
323
|
-
_extractSpringRoutes(rootNode, emit);
|
|
324
|
-
}
|
|
325
|
-
} catch { /* best-effort — never fail indexing */ }
|
|
326
|
-
return routes;
|
|
327
|
-
}
|