gitnexus 1.6.9-rc.10 → 1.6.9-rc.12
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/dist/core/group/cross-trace.js +7 -5
- package/dist/core/group/extractors/http-patterns/go.js +21 -6
- package/dist/core/group/extractors/http-patterns/java.js +7 -0
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -0
- package/dist/core/group/extractors/http-patterns/node.js +53 -2
- package/dist/core/group/extractors/http-patterns/php.js +14 -2
- package/dist/core/group/extractors/http-patterns/python.js +138 -0
- package/dist/core/group/extractors/http-patterns/types.d.ts +14 -0
- package/dist/core/group/extractors/http-route-extractor.js +162 -5
- package/package.json +1 -1
|
@@ -732,18 +732,20 @@ async function stitchCrossRepo(deps, p, fromEp, toEp, groupDir, notes) {
|
|
|
732
732
|
const FILE_BASENAME_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|php|java|kt|kts|rb|cs|rs|swift|dart|scala|c|cc|cpp|cxx|h|hpp|m|mm)$/i;
|
|
733
733
|
/**
|
|
734
734
|
* A provider endpoint's display label. A resolved handler has a real function
|
|
735
|
-
* name; the source-scan fallbacks leave a generic token (`'handler'`/`'fetch'
|
|
736
|
-
* or
|
|
735
|
+
* name; the source-scan fallbacks leave a generic token (`'handler'`/`'fetch'`,
|
|
736
|
+
* or `'route'` for an unresolved named-controller / closure Laravel route) or a
|
|
737
|
+
* file basename. Those are treated as anonymous and shown as
|
|
737
738
|
* `<METHOD /path handler>` so the endpoint is still identifiable by route. When
|
|
738
739
|
* the bridge row carries a resolved `providerUid`, the name IS a real symbol —
|
|
739
|
-
* the
|
|
740
|
-
*
|
|
740
|
+
* the sentinel check is suppressed so a function genuinely named `handler` (or,
|
|
741
|
+
* hypothetically, `route`) is not mislabeled anonymous.
|
|
741
742
|
*/
|
|
742
743
|
function providerLabel(providerName, contractId, providerUid) {
|
|
743
744
|
const resolved = providerUid !== '';
|
|
744
745
|
const generic = providerName === '' ||
|
|
745
746
|
FILE_BASENAME_RE.test(providerName) ||
|
|
746
|
-
(!resolved &&
|
|
747
|
+
(!resolved &&
|
|
748
|
+
(providerName === 'handler' || providerName === 'fetch' || providerName === 'route'));
|
|
747
749
|
return generic
|
|
748
750
|
? { label: `<${contractId} handler>`, anon: true }
|
|
749
751
|
: { label: providerName, anon: false };
|
|
@@ -9,8 +9,11 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
|
|
|
9
9
|
*/
|
|
10
10
|
// ─── Provider: framework routing ──────────────────────────────────────
|
|
11
11
|
// Matches `\w+\.GET(...)` etc. (gin, echo, chi all share this shape).
|
|
12
|
-
// Captures the HTTP method (field name), path literal, and handler
|
|
13
|
-
//
|
|
12
|
+
// Captures the HTTP method (field name), path literal, and the handler —
|
|
13
|
+
// anchored to the LAST argument (`@handler .`) so a variadic middleware
|
|
14
|
+
// chain (`r.GET("/x", mw, handler)`, gin/echo/chi style) binds the real
|
|
15
|
+
// handler, not a middleware identifier (which would otherwise over-match
|
|
16
|
+
// and attach the route to the wrong symbol — see #2276 review).
|
|
14
17
|
const FRAMEWORK_ROUTE_PATTERNS = compilePatterns({
|
|
15
18
|
name: 'go-framework-route',
|
|
16
19
|
language: Go,
|
|
@@ -23,7 +26,8 @@ const FRAMEWORK_ROUTE_PATTERNS = compilePatterns({
|
|
|
23
26
|
field: (field_identifier) @http_method (#match? @http_method "^(GET|POST|PUT|DELETE|PATCH)$"))
|
|
24
27
|
arguments: (argument_list
|
|
25
28
|
(interpreted_string_literal) @path
|
|
26
|
-
(identifier) @handler
|
|
29
|
+
[(identifier) (func_literal)] @handler
|
|
30
|
+
.))
|
|
27
31
|
`,
|
|
28
32
|
},
|
|
29
33
|
],
|
|
@@ -42,7 +46,8 @@ const HANDLE_FUNC_PATTERNS = compilePatterns({
|
|
|
42
46
|
field: (field_identifier) @fn (#eq? @fn "HandleFunc"))
|
|
43
47
|
arguments: (argument_list
|
|
44
48
|
(interpreted_string_literal) @path
|
|
45
|
-
(identifier) @handler
|
|
49
|
+
[(identifier) (func_literal)] @handler
|
|
50
|
+
.))
|
|
46
51
|
`,
|
|
47
52
|
},
|
|
48
53
|
],
|
|
@@ -125,12 +130,18 @@ export const GO_HTTP_PLUGIN = {
|
|
|
125
130
|
const path = unquoteLiteral(pathNode.text);
|
|
126
131
|
if (path === null)
|
|
127
132
|
continue;
|
|
133
|
+
// An inline `func(){…}` handler has no name → emit `name: null` and a
|
|
134
|
+
// `line` so it resolves to its containing/closure symbol by line-span
|
|
135
|
+
// containment (like a consumer). A named identifier handler keeps its
|
|
136
|
+
// name and resolves by name; `line` is harmless there.
|
|
137
|
+
const isInlineHandler = handlerNode?.type === 'func_literal';
|
|
128
138
|
out.push({
|
|
129
139
|
role: 'provider',
|
|
130
140
|
framework: 'go-framework',
|
|
131
141
|
method: methodNode.text.toUpperCase(),
|
|
132
142
|
path,
|
|
133
|
-
name: handlerNode?.text ?? null,
|
|
143
|
+
name: isInlineHandler ? null : (handlerNode?.text ?? null),
|
|
144
|
+
line: (handlerNode ?? pathNode).startPosition.row + 1,
|
|
134
145
|
confidence: 0.8,
|
|
135
146
|
});
|
|
136
147
|
}
|
|
@@ -143,12 +154,16 @@ export const GO_HTTP_PLUGIN = {
|
|
|
143
154
|
const path = unquoteLiteral(pathNode.text);
|
|
144
155
|
if (path === null)
|
|
145
156
|
continue;
|
|
157
|
+
// Inline `func(){…}` handler → resolve by containment (see go-framework
|
|
158
|
+
// note above); a named handler resolves by name.
|
|
159
|
+
const isInlineHandler = handlerNode?.type === 'func_literal';
|
|
146
160
|
out.push({
|
|
147
161
|
role: 'provider',
|
|
148
162
|
framework: 'go-stdlib',
|
|
149
163
|
method: 'GET',
|
|
150
164
|
path,
|
|
151
|
-
name: handlerNode?.text ?? null,
|
|
165
|
+
name: isInlineHandler ? null : (handlerNode?.text ?? null),
|
|
166
|
+
line: (handlerNode ?? pathNode).startPosition.row + 1,
|
|
152
167
|
confidence: 0.8,
|
|
153
168
|
});
|
|
154
169
|
}
|
|
@@ -634,6 +634,13 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
634
634
|
method: route.httpMethod,
|
|
635
635
|
path: joinPath(prefix, route.rawPath),
|
|
636
636
|
name: route.methodName,
|
|
637
|
+
// Spring providers are named controller methods resolved BY NAME, so
|
|
638
|
+
// `line` is inert — a named provider never falls through to line-span
|
|
639
|
+
// containment. Gate it on a present name so a (grammar-impossible)
|
|
640
|
+
// nameless provider degrades to file-level rather than resolving by
|
|
641
|
+
// containment to the enclosing class. Wired for consumer-emit parity
|
|
642
|
+
// and a future inline DSL.
|
|
643
|
+
line: route.methodName ? route.methodNode.startPosition.row + 1 : undefined,
|
|
637
644
|
confidence: 0.8,
|
|
638
645
|
});
|
|
639
646
|
}
|
|
@@ -977,6 +977,13 @@ function buildKotlinPlugin(language) {
|
|
|
977
977
|
method: httpMethod,
|
|
978
978
|
path: joinPath(prefix, rawPath),
|
|
979
979
|
name: nameNode?.text ?? null,
|
|
980
|
+
// Spring providers are named controller methods resolved BY NAME, so
|
|
981
|
+
// `line` is inert — a named provider never falls through to line-span
|
|
982
|
+
// containment. Gate it on a present name so a (grammar-impossible)
|
|
983
|
+
// nameless provider degrades to file-level rather than resolving by
|
|
984
|
+
// containment to the enclosing class. Wired for consumer-emit parity
|
|
985
|
+
// and a future inline DSL.
|
|
986
|
+
line: nameNode?.text ? methodNode.startPosition.row + 1 : undefined,
|
|
980
987
|
confidence: 0.8,
|
|
981
988
|
});
|
|
982
989
|
}
|
|
@@ -270,8 +270,54 @@ function findDecoratedMethod(decoratorNode) {
|
|
|
270
270
|
}
|
|
271
271
|
return null;
|
|
272
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Map each named import's LOCAL binding to its DECLARED export name and source
|
|
275
|
+
* module, by walking the file's `import { x as y } from 'm'` statements. Lets
|
|
276
|
+
* the express handler resolve through an alias (the local `y`) to the real
|
|
277
|
+
* symbol (`x` in `m`) instead of looking up the alias text. Only named imports
|
|
278
|
+
* are mapped — default and namespace imports are left to fall through as
|
|
279
|
+
* locally-scoped identifiers.
|
|
280
|
+
*/
|
|
281
|
+
function buildImportMap(tree) {
|
|
282
|
+
const map = new Map();
|
|
283
|
+
const walk = (node) => {
|
|
284
|
+
if (node.type === 'import_statement') {
|
|
285
|
+
const sourceNode = node.childForFieldName('source');
|
|
286
|
+
const module = sourceNode ? unquoteLiteral(sourceNode.text) : null;
|
|
287
|
+
if (module !== null) {
|
|
288
|
+
const collect = (n) => {
|
|
289
|
+
if (n.type === 'import_specifier') {
|
|
290
|
+
const nameNode = n.childForFieldName('name');
|
|
291
|
+
const aliasNode = n.childForFieldName('alias');
|
|
292
|
+
const local = aliasNode ?? nameNode;
|
|
293
|
+
if (nameNode && local && local.type === 'identifier') {
|
|
294
|
+
map.set(local.text, { name: nameNode.text, module });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
298
|
+
const c = n.namedChild(i);
|
|
299
|
+
if (c)
|
|
300
|
+
collect(c);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
collect(node);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
307
|
+
const c = node.namedChild(i);
|
|
308
|
+
if (c)
|
|
309
|
+
walk(c);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
walk(tree.rootNode);
|
|
313
|
+
return map;
|
|
314
|
+
}
|
|
273
315
|
function scanBundle(bundle, tree) {
|
|
274
316
|
const out = [];
|
|
317
|
+
// Local-binding → { declared export name, module } for the file's named
|
|
318
|
+
// imports, so an express handler that is an imported (possibly aliased)
|
|
319
|
+
// symbol resolves to the real definition rather than its local alias text.
|
|
320
|
+
const importMap = buildImportMap(tree);
|
|
275
321
|
// NestJS: collect `@Controller('prefix')` class decorators, keyed by
|
|
276
322
|
// the `class_declaration` they decorate.
|
|
277
323
|
const prefixByClassId = new Map();
|
|
@@ -343,14 +389,19 @@ function scanBundle(bundle, tree) {
|
|
|
343
389
|
// → `listUsers`) so a named handler resolves by name. For an inline/anonymous
|
|
344
390
|
// handler emit `name: null` (NOT the sentinel `'handler'`) so the resolver
|
|
345
391
|
// does NOT match an unrelated function that happens to be named `handler` —
|
|
346
|
-
// it uses the registration line for containment instead.
|
|
392
|
+
// it uses the registration line for containment instead. When the handler is
|
|
393
|
+
// an imported (possibly aliased) symbol, carry the resolved import so the
|
|
394
|
+
// extractor can pin it to the source module rather than the local alias text.
|
|
347
395
|
const handlerNode = match.captures.handler;
|
|
396
|
+
const localHandler = handlerNode?.type === 'identifier' ? handlerNode.text : null;
|
|
397
|
+
const imported = localHandler !== null ? importMap.get(localHandler) : undefined;
|
|
348
398
|
out.push({
|
|
349
399
|
role: 'provider',
|
|
350
400
|
framework: 'express',
|
|
351
401
|
method: methodNode.text.toUpperCase(),
|
|
352
402
|
path,
|
|
353
|
-
name:
|
|
403
|
+
name: imported ? imported.name : localHandler,
|
|
404
|
+
handlerImport: imported,
|
|
354
405
|
line: (handlerNode ?? pathNode).startPosition.row + 1,
|
|
355
406
|
confidence: 0.8,
|
|
356
407
|
});
|
|
@@ -27,7 +27,9 @@ const LARAVEL_ROUTE_SPEC = {
|
|
|
27
27
|
(scoped_call_expression
|
|
28
28
|
scope: (name) @scope (#eq? @scope "Route")
|
|
29
29
|
name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
|
|
30
|
-
arguments: (arguments
|
|
30
|
+
arguments: (arguments
|
|
31
|
+
. (argument (string) @path)
|
|
32
|
+
(argument [(anonymous_function) (arrow_function)] @closure)?))
|
|
31
33
|
`,
|
|
32
34
|
};
|
|
33
35
|
const HTTP_FACADE_SPEC = {
|
|
@@ -125,12 +127,22 @@ export const PHP_HTTP_PLUGIN = {
|
|
|
125
127
|
const path = phpStringText(pathNode);
|
|
126
128
|
if (path === null)
|
|
127
129
|
continue;
|
|
130
|
+
// A closure handler (`Route::get('/x', function(){…})` / `fn() => …`) has
|
|
131
|
+
// no name → emit `name: null` + the registration line so it resolves to
|
|
132
|
+
// its containing symbol (e.g. a service-provider `boot()` or controller
|
|
133
|
+
// method) by line-span containment. A named-controller route keeps the
|
|
134
|
+
// `'route'` label — resolving its array/string handler to a real method is
|
|
135
|
+
// a separate, graph-backed concern. NOTE: a closure at FILE scope
|
|
136
|
+
// (routes/web.php) has no enclosing function and PHP closures are not yet
|
|
137
|
+
// indexed as symbols, so it still degrades to file-level (see #2276).
|
|
138
|
+
const closureNode = match.captures.closure;
|
|
128
139
|
out.push({
|
|
129
140
|
role: 'provider',
|
|
130
141
|
framework: 'laravel',
|
|
131
142
|
method: methodNode.text.toUpperCase(),
|
|
132
143
|
path,
|
|
133
|
-
name: 'route',
|
|
144
|
+
name: closureNode ? null : 'route',
|
|
145
|
+
line: (closureNode ?? pathNode).startPosition.row + 1,
|
|
134
146
|
confidence: 0.8,
|
|
135
147
|
});
|
|
136
148
|
}
|
|
@@ -66,6 +66,32 @@ const FASTAPI_ROUTER_PATTERNS = compilePatterns({
|
|
|
66
66
|
},
|
|
67
67
|
],
|
|
68
68
|
});
|
|
69
|
+
// ─── Provider: Flask `app.add_url_rule('/path', view_func=handler)` ───
|
|
70
|
+
// The imperative Flask route registration: unlike `@app.route` (whose handler
|
|
71
|
+
// is the decorated function, same-file), `view_func` is frequently an IMPORTED
|
|
72
|
+
// (and sometimes aliased) view, so the handler resolves through the file's
|
|
73
|
+
// imports. `add_url_rule` + a `view_func=` keyword is highly Flask-specific, so
|
|
74
|
+
// the false-positive risk is low. Method(s) come from a `methods=[...]` keyword
|
|
75
|
+
// (default GET), extracted in code from the captured call.
|
|
76
|
+
const FLASK_ADD_URL_RULE_PATTERNS = compilePatterns({
|
|
77
|
+
name: 'python-flask-add-url-rule',
|
|
78
|
+
language: Python,
|
|
79
|
+
patterns: [
|
|
80
|
+
{
|
|
81
|
+
meta: {},
|
|
82
|
+
query: `
|
|
83
|
+
(call
|
|
84
|
+
function: (attribute
|
|
85
|
+
attribute: (identifier) @fn (#eq? @fn "add_url_rule"))
|
|
86
|
+
arguments: (argument_list
|
|
87
|
+
. (string) @path
|
|
88
|
+
(keyword_argument
|
|
89
|
+
name: (identifier) @kw (#eq? @kw "view_func")
|
|
90
|
+
value: (identifier) @handler))) @call
|
|
91
|
+
`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
69
95
|
// ─── include_router(<router_obj>, prefix='/x') across the repo ────────
|
|
70
96
|
// Two shapes are common:
|
|
71
97
|
// app.include_router(assistant.router, prefix='/ai')
|
|
@@ -306,6 +332,79 @@ const WRAPPER_URI_VAR_PATTERNS = compilePatterns({
|
|
|
306
332
|
},
|
|
307
333
|
],
|
|
308
334
|
});
|
|
335
|
+
/**
|
|
336
|
+
* Map each `from <module> import <name> [as <alias>]` binding to its declared
|
|
337
|
+
* name + raw module specifier (the spec keeps the leading dots for relative
|
|
338
|
+
* imports — `.users`, `..pkg.users` — which the extractor resolves to a target
|
|
339
|
+
* file). Lets a Flask `view_func` handler resolve through an alias to the real
|
|
340
|
+
* symbol in its module rather than the local alias text. `import x` / `import x
|
|
341
|
+
* as y` (module imports, not symbol imports) are left out — a route handler is a
|
|
342
|
+
* symbol, addressed via `from … import …`.
|
|
343
|
+
*/
|
|
344
|
+
function buildPythonImportMap(tree) {
|
|
345
|
+
const map = new Map();
|
|
346
|
+
const walk = (node) => {
|
|
347
|
+
if (node.type === 'import_from_statement') {
|
|
348
|
+
const moduleNode = node.childForFieldName('module_name');
|
|
349
|
+
const module = moduleNode?.text ?? null;
|
|
350
|
+
if (module !== null) {
|
|
351
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
352
|
+
const c = node.namedChild(i);
|
|
353
|
+
if (!c || c.id === moduleNode?.id)
|
|
354
|
+
continue;
|
|
355
|
+
if (c.type === 'dotted_name') {
|
|
356
|
+
map.set(c.text, { name: c.text, module });
|
|
357
|
+
}
|
|
358
|
+
else if (c.type === 'aliased_import') {
|
|
359
|
+
const nameNode = c.childForFieldName('name');
|
|
360
|
+
const aliasNode = c.childForFieldName('alias');
|
|
361
|
+
if (nameNode && aliasNode) {
|
|
362
|
+
map.set(aliasNode.text, { name: nameNode.text, module });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
369
|
+
const c = node.namedChild(i);
|
|
370
|
+
if (c)
|
|
371
|
+
walk(c);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
walk(tree.rootNode);
|
|
375
|
+
return map;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* HTTP verbs declared on a Flask `add_url_rule(..., methods=[...])` call, upper-
|
|
379
|
+
* cased. Defaults to `['GET']` when no `methods` keyword is present (Flask's own
|
|
380
|
+
* default). Reads the captured call node directly since the list value is awkward
|
|
381
|
+
* to capture in a tree-sitter query.
|
|
382
|
+
*/
|
|
383
|
+
function extractFlaskMethods(callNode) {
|
|
384
|
+
const args = callNode.childForFieldName('arguments');
|
|
385
|
+
if (args) {
|
|
386
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
387
|
+
const kw = args.namedChild(i);
|
|
388
|
+
if (!kw || kw.type !== 'keyword_argument')
|
|
389
|
+
continue;
|
|
390
|
+
if (kw.childForFieldName('name')?.text !== 'methods')
|
|
391
|
+
continue;
|
|
392
|
+
const list = kw.childForFieldName('value');
|
|
393
|
+
if (!list)
|
|
394
|
+
continue;
|
|
395
|
+
const methods = [];
|
|
396
|
+
for (let j = 0; j < list.namedChildCount; j++) {
|
|
397
|
+
const el = list.namedChild(j);
|
|
398
|
+
const v = el && el.type === 'string' ? unquoteLiteral(el.text) : null;
|
|
399
|
+
if (v)
|
|
400
|
+
methods.push(v.toUpperCase());
|
|
401
|
+
}
|
|
402
|
+
if (methods.length > 0)
|
|
403
|
+
return methods;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return ['GET'];
|
|
407
|
+
}
|
|
309
408
|
// Pre-scan: collect local string assignments (uri = "api/v1/endpoint/")
|
|
310
409
|
function buildLocalStringMap(tree) {
|
|
311
410
|
const map = new Map();
|
|
@@ -846,6 +945,10 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
846
945
|
const out = [];
|
|
847
946
|
const httpxAsyncClients = collectHttpxAsyncClients(tree);
|
|
848
947
|
const ctx = repoContext;
|
|
948
|
+
// Local-binding → { declared name, module } for the file's `from … import …`
|
|
949
|
+
// statements, so an imperatively-registered handler (Flask `view_func`) that
|
|
950
|
+
// is an imported (possibly aliased) symbol resolves to its real definition.
|
|
951
|
+
const importMap = buildPythonImportMap(tree);
|
|
849
952
|
// Providers: FastAPI @app.<verb>("/path") — already absolute path.
|
|
850
953
|
for (const match of runCompiledPatterns(FASTAPI_APP_PATTERNS, tree)) {
|
|
851
954
|
const methodNode = match.captures.method;
|
|
@@ -864,6 +967,12 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
864
967
|
method: httpMethod,
|
|
865
968
|
path,
|
|
866
969
|
name: null,
|
|
970
|
+
// The decorated handler has no captured name → resolve by line-span
|
|
971
|
+
// containment. Best-effort fallback: FastAPI routes are graph-backed
|
|
972
|
+
// (ingestion decorator routes) and the function span starts at `def`
|
|
973
|
+
// (decorators excluded), so this lands the single-decorator case and
|
|
974
|
+
// degrades to file-level for multi-decorator stacks.
|
|
975
|
+
line: pathNode.startPosition.row + 1,
|
|
867
976
|
confidence: 0.8,
|
|
868
977
|
});
|
|
869
978
|
}
|
|
@@ -905,6 +1014,35 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
905
1014
|
method: httpMethod,
|
|
906
1015
|
path: p,
|
|
907
1016
|
name: null,
|
|
1017
|
+
// Best-effort containment fallback — see the @app provider note above.
|
|
1018
|
+
line: pathNode.startPosition.row + 1,
|
|
1019
|
+
confidence: 0.8,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Providers: Flask `app.add_url_rule('/path', view_func=handler, methods=[…])`.
|
|
1024
|
+
// The handler is a `view_func` identifier, frequently an imported (possibly
|
|
1025
|
+
// aliased) view, so resolve it through the file's imports to the declared
|
|
1026
|
+
// symbol + its module for import-pinned resolution downstream.
|
|
1027
|
+
for (const match of runCompiledPatterns(FLASK_ADD_URL_RULE_PATTERNS, tree)) {
|
|
1028
|
+
const pathNode = match.captures.path;
|
|
1029
|
+
const handlerNode = match.captures.handler;
|
|
1030
|
+
const callNode = match.captures.call;
|
|
1031
|
+
if (!pathNode || !handlerNode || !callNode)
|
|
1032
|
+
continue;
|
|
1033
|
+
const path = unquoteLiteral(pathNode.text);
|
|
1034
|
+
if (path === null)
|
|
1035
|
+
continue;
|
|
1036
|
+
const imported = importMap.get(handlerNode.text);
|
|
1037
|
+
for (const method of extractFlaskMethods(callNode)) {
|
|
1038
|
+
out.push({
|
|
1039
|
+
role: 'provider',
|
|
1040
|
+
framework: 'flask',
|
|
1041
|
+
method,
|
|
1042
|
+
path,
|
|
1043
|
+
name: imported ? imported.name : handlerNode.text,
|
|
1044
|
+
handlerImport: imported,
|
|
1045
|
+
line: (imported ? pathNode : handlerNode).startPosition.row + 1,
|
|
908
1046
|
confidence: 0.8,
|
|
909
1047
|
});
|
|
910
1048
|
}
|
|
@@ -42,6 +42,20 @@ export interface HttpDetection {
|
|
|
42
42
|
* not set it falls back to file-level boundary resolution downstream.
|
|
43
43
|
*/
|
|
44
44
|
line?: number;
|
|
45
|
+
/**
|
|
46
|
+
* When the handler is an IMPORTED symbol, the import resolved to its declared
|
|
47
|
+
* (exported) `name` and the `module` specifier it came from. The extractor
|
|
48
|
+
* pins resolution to the import's target file, so an aliased import
|
|
49
|
+
* (`import { listUsers as handleUsers }`) or a name that collides with a local
|
|
50
|
+
* symbol resolves to the right handler instead of a same-named decoy. `name`
|
|
51
|
+
* here is the DECLARED export name (not the local alias); `module` is the raw
|
|
52
|
+
* specifier (e.g. `./handlers/users`). Set only for named imports; omitted for
|
|
53
|
+
* locally-defined or anonymous handlers.
|
|
54
|
+
*/
|
|
55
|
+
handlerImport?: {
|
|
56
|
+
name: string;
|
|
57
|
+
module: string;
|
|
58
|
+
};
|
|
45
59
|
/** Confidence in (0, 1]. Source-scan plugins typically use 0.7–0.8. */
|
|
46
60
|
confidence: number;
|
|
47
61
|
}
|
|
@@ -67,6 +67,65 @@ MATCH (sym:CodeElement)
|
|
|
67
67
|
WHERE sym.filePath = $filePath AND sym.startLine IS NOT NULL AND sym.endLine IS NOT NULL
|
|
68
68
|
RETURN sym.id AS uid, sym.name AS name, sym.filePath AS filePath,
|
|
69
69
|
sym.startLine AS startLine, sym.endLine AS endLine, labels(sym) AS labels`;
|
|
70
|
+
// Repo-wide lookup of a symbol by exact name (label-union, as in
|
|
71
|
+
// manifest-extractor.ts). Used to resolve a provider's named handler when it is
|
|
72
|
+
// defined in a file OTHER than its route registration — and only honored when
|
|
73
|
+
// the result is unique (see resolveSymbolByNameUnique).
|
|
74
|
+
//
|
|
75
|
+
// `n.filePath <> ''` excludes synthetic non-source `CodeElement` nodes that
|
|
76
|
+
// carry no real file — ORM model/table nodes (orm.ts emits `filePath: ''`) and
|
|
77
|
+
// similar — so a handler name colliding with an ORM model neither resolves to a
|
|
78
|
+
// degenerate edge-less node NOR inflates the uniqueness count and masks the real
|
|
79
|
+
// handler. `LIMIT 2` bounds materialization: distinguishing unique (1) from
|
|
80
|
+
// ambiguous (>=2) never needs more than two rows (the count guard stays exact).
|
|
81
|
+
const RESOLVE_BY_NAME_QUERY = `
|
|
82
|
+
MATCH (n:Function|Method|CodeElement)
|
|
83
|
+
WHERE n.name = $name AND n.filePath <> ''
|
|
84
|
+
RETURN n.id AS uid, n.name AS name, n.filePath AS filePath
|
|
85
|
+
LIMIT 2`;
|
|
86
|
+
// Resolve an IMPORTED handler by pinning it to the import's target module: the
|
|
87
|
+
// declared export `$name` whose file is the module the handler was imported from
|
|
88
|
+
// (`$fileDot` matches `mod.ext`, `$fileSlash` matches `mod/index.ext`). This is
|
|
89
|
+
// the precise rung — it survives aliases and local same-name collisions that a
|
|
90
|
+
// repo-wide name lookup cannot, and only resolves on a unique match within that
|
|
91
|
+
// module. `LIMIT 2` keeps the uniqueness count exact (see RESOLVE_BY_NAME_QUERY).
|
|
92
|
+
const RESOLVE_IN_MODULE_QUERY = `
|
|
93
|
+
MATCH (n:Function|Method|CodeElement)
|
|
94
|
+
WHERE n.name = $name AND (n.filePath STARTS WITH $fileDot OR n.filePath STARTS WITH $fileSlash)
|
|
95
|
+
RETURN n.id AS uid, n.name AS name, n.filePath AS filePath
|
|
96
|
+
LIMIT 2`;
|
|
97
|
+
// Source-file extensions an import specifier may resolve to (stripped before
|
|
98
|
+
// building the module file-prefix so `./h/users` and `./h/users.ts` agree).
|
|
99
|
+
const SOURCE_EXT_RE = /\.(?:m|c)?[jt]sx?$/;
|
|
100
|
+
/**
|
|
101
|
+
* Resolve an import specifier to a repo-relative FILE BASE (path without
|
|
102
|
+
* extension) so the target module can be matched by `filePath STARTS WITH`.
|
|
103
|
+
* Handles two relative-import dialects and returns null for bare/absolute
|
|
104
|
+
* imports (which fall back to a repo-wide name lookup):
|
|
105
|
+
* - path-style (JS/TS): `./handlers/users`, `../x` → joined against the
|
|
106
|
+
* importing file's directory.
|
|
107
|
+
* - dotted-relative (Python): `.users`, `..pkg.users` → leading dots are
|
|
108
|
+
* package levels (one dot = the file's own package), the rest dot→slash.
|
|
109
|
+
*/
|
|
110
|
+
function resolveModuleBase(fromFile, module) {
|
|
111
|
+
const dir = path.posix.dirname(fromFile.replace(/\\/g, '/'));
|
|
112
|
+
if (module.includes('/')) {
|
|
113
|
+
// path-style relative import
|
|
114
|
+
if (!module.startsWith('.'))
|
|
115
|
+
return null;
|
|
116
|
+
return path.posix.normalize(path.posix.join(dir, module)).replace(SOURCE_EXT_RE, '');
|
|
117
|
+
}
|
|
118
|
+
if (module.startsWith('.')) {
|
|
119
|
+
// Python dotted-relative import
|
|
120
|
+
const dots = module.length - module.replace(/^\.+/, '').length;
|
|
121
|
+
const rest = module.slice(dots).replace(/\./g, '/');
|
|
122
|
+
let base = dir;
|
|
123
|
+
for (let i = 1; i < dots; i++)
|
|
124
|
+
base = path.posix.dirname(base);
|
|
125
|
+
return rest ? path.posix.normalize(path.posix.join(base, rest)) : base;
|
|
126
|
+
}
|
|
127
|
+
return null; // bare / absolute import — repo-wide fallback
|
|
128
|
+
}
|
|
70
129
|
/**
|
|
71
130
|
* The innermost Function/Method whose `[startLine, endLine]` span contains
|
|
72
131
|
* `line` — i.e. the symbol the HTTP call lives inside. For a consumer this is
|
|
@@ -324,22 +383,120 @@ export class HttpRouteExtractor {
|
|
|
324
383
|
fileSymbolCache.set(filePath, rows);
|
|
325
384
|
return rows;
|
|
326
385
|
};
|
|
386
|
+
// Repo-wide UNAMBIGUOUS resolution for a provider handler defined in a file
|
|
387
|
+
// other than its route registration (e.g. `router.get('/x', listUsers)` with
|
|
388
|
+
// `listUsers` imported from another module). Returns the symbol ONLY when
|
|
389
|
+
// exactly one Function/Method/CodeElement carries that name across the repo.
|
|
390
|
+
// The strict uniqueness guard is intentionally conservative: when a name is
|
|
391
|
+
// shared across files (homonyms like `handler`/`index`), we prefer a
|
|
392
|
+
// false-negative (no attribution → file-level fallback) over a false-positive
|
|
393
|
+
// (wrong symbol).
|
|
394
|
+
//
|
|
395
|
+
// An IMPORTED handler (the common cross-file case) is pinned to its source
|
|
396
|
+
// module first by resolveImportedSymbol, so an alias or a name colliding with
|
|
397
|
+
// a local symbol resolves correctly; this repo-wide-by-name rung is the
|
|
398
|
+
// fallback for non-relative/bare imports and for plugins that supply only a
|
|
399
|
+
// name. Cached by name for the lifetime of this extract().
|
|
400
|
+
const globalNameCache = new Map();
|
|
401
|
+
const toResolvedSymbol = (rows) => {
|
|
402
|
+
const norm = (x) => String(x ?? '');
|
|
403
|
+
const uid = rows.length === 1 ? norm(rows[0].uid ?? rows[0][0]) : '';
|
|
404
|
+
const filePath = uid ? norm(rows[0].filePath ?? rows[0][2]) : '';
|
|
405
|
+
// Reject a unique match that carries no real file (a synthetic ORM /
|
|
406
|
+
// non-source node) so it can never anchor a cross-trace on an edge-less
|
|
407
|
+
// node — defence in depth alongside the queries' filePath predicates.
|
|
408
|
+
return uid && filePath ? { uid, name: norm(rows[0].name ?? rows[0][1]), filePath } : null;
|
|
409
|
+
};
|
|
410
|
+
const resolveSymbolByNameUnique = async (name) => {
|
|
411
|
+
if (!dbExecutor)
|
|
412
|
+
return null;
|
|
413
|
+
const cached = globalNameCache.get(name);
|
|
414
|
+
if (cached !== undefined)
|
|
415
|
+
return cached;
|
|
416
|
+
let rows = [];
|
|
417
|
+
try {
|
|
418
|
+
rows = await dbExecutor(RESOLVE_BY_NAME_QUERY, { name });
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
rows = [];
|
|
422
|
+
}
|
|
423
|
+
const result = toResolvedSymbol(rows);
|
|
424
|
+
globalNameCache.set(name, result);
|
|
425
|
+
return result;
|
|
426
|
+
};
|
|
427
|
+
// Resolve a handler imported from a RELATIVE module to the unique declared
|
|
428
|
+
// symbol of that name inside the import's target file. Returns null for
|
|
429
|
+
// non-relative (bare/aliased-path) imports — those fall back to the repo-wide
|
|
430
|
+
// name lookup. Cached by (target-file-prefix, declared name).
|
|
431
|
+
const importedSymbolCache = new Map();
|
|
432
|
+
const resolveImportedSymbol = async (fromFile, imp) => {
|
|
433
|
+
if (!dbExecutor)
|
|
434
|
+
return null;
|
|
435
|
+
const base = resolveModuleBase(fromFile, imp.module);
|
|
436
|
+
if (base === null)
|
|
437
|
+
return null; // bare/absolute import → repo-wide fallback
|
|
438
|
+
const cacheKey = JSON.stringify([base, imp.name]);
|
|
439
|
+
const cached = importedSymbolCache.get(cacheKey);
|
|
440
|
+
if (cached !== undefined)
|
|
441
|
+
return cached;
|
|
442
|
+
let rows = [];
|
|
443
|
+
try {
|
|
444
|
+
rows = await dbExecutor(RESOLVE_IN_MODULE_QUERY, {
|
|
445
|
+
name: imp.name,
|
|
446
|
+
fileDot: `${base}.`,
|
|
447
|
+
fileSlash: `${base}/`,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
rows = [];
|
|
452
|
+
}
|
|
453
|
+
const result = toResolvedSymbol(rows);
|
|
454
|
+
importedSymbolCache.set(cacheKey, result);
|
|
455
|
+
return result;
|
|
456
|
+
};
|
|
327
457
|
const resolveDetectionSymbol = async (filePath, d) => {
|
|
328
458
|
if (!dbExecutor)
|
|
329
459
|
return null;
|
|
330
460
|
const syms = await loadFileSymbols(filePath);
|
|
331
|
-
if (syms.length === 0)
|
|
332
|
-
return null;
|
|
333
461
|
// Name resolution does NOT need a detection line — a named provider
|
|
334
462
|
// handler (Spring/Go/etc. method name) resolves by name even when the
|
|
335
|
-
// plugin didn't set `line`. Try
|
|
336
|
-
//
|
|
463
|
+
// plugin didn't set `line`. Try the registration file FIRST; then, for a
|
|
464
|
+
// handler defined in another file, the unique repo-wide match. Only the
|
|
465
|
+
// containment fallback requires a line.
|
|
337
466
|
if (d.role === 'provider' && d.name) {
|
|
467
|
+
// IMPORTED handler: pin to the import's target module first. This is the
|
|
468
|
+
// precise rung — it survives aliases and names that collide with a local
|
|
469
|
+
// symbol. The handler is defined ELSEWHERE, so a file-scoped lookup of
|
|
470
|
+
// its (declared) name would be wrong; on a miss go straight to a unique
|
|
471
|
+
// repo-wide match on the declared name, never file-scoped.
|
|
472
|
+
if (d.handlerImport) {
|
|
473
|
+
const byImport = await resolveImportedSymbol(filePath, d.handlerImport);
|
|
474
|
+
if (byImport)
|
|
475
|
+
return byImport;
|
|
476
|
+
const byGlobal = await resolveSymbolByNameUnique(d.handlerImport.name);
|
|
477
|
+
if (byGlobal)
|
|
478
|
+
return byGlobal;
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
338
481
|
const byName = resolveSymbolByName(syms, d.name);
|
|
339
482
|
if (byName)
|
|
340
483
|
return byName;
|
|
484
|
+
const byGlobal = await resolveSymbolByNameUnique(d.name);
|
|
485
|
+
if (byGlobal)
|
|
486
|
+
return byGlobal;
|
|
487
|
+
// A NAMED handler we could not resolve by name (neither file-scoped nor
|
|
488
|
+
// the unique repo-wide match) must NOT fall through to line-span
|
|
489
|
+
// containment: `d.line` is the route REGISTRATION site, so containment
|
|
490
|
+
// would attach the route to the enclosing registrar (e.g. a
|
|
491
|
+
// `setupRoutes()` wrapper) rather than the handler. Leave it empty →
|
|
492
|
+
// file-level boundary fallback, upholding the invariant that a
|
|
493
|
+
// zero/ambiguous name match never yields a wrong-symbol attribution.
|
|
494
|
+
return null;
|
|
341
495
|
}
|
|
342
|
-
|
|
496
|
+
// Consumers (the function making the fetch) and inline-arrow providers
|
|
497
|
+
// (d.name === null) DO resolve by containment — there the enclosing symbol
|
|
498
|
+
// is the right one.
|
|
499
|
+
if (syms.length === 0 || d.line == null)
|
|
343
500
|
return null;
|
|
344
501
|
return resolveContainingSymbol(syms, d.line);
|
|
345
502
|
};
|
package/package.json
CHANGED