gitnexus 1.6.6-rc.84 → 1.6.6-rc.85
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpLanguagePlugin } from './types.js';
|
|
2
|
-
export type { HttpDetection, HttpLanguagePlugin, HttpRole } from './types.js';
|
|
2
|
+
export type { HttpDetection, HttpFileDetections, HttpLanguagePlugin, HttpRole, HttpScanInput, } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Glob for files worth scanning for HTTP routes. Kept alongside the
|
|
5
5
|
* registry so adding a new language widens the glob in one edit.
|
|
@@ -22,45 +22,64 @@ const METHOD_ANNOTATION_TO_HTTP = {
|
|
|
22
22
|
DeleteMapping: 'DELETE',
|
|
23
23
|
PatchMapping: 'PATCH',
|
|
24
24
|
};
|
|
25
|
-
// ─── Provider: Spring class-level @RequestMapping prefix
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// @RequestMapping("/api") → (annotation_argument_list (string_literal))
|
|
29
|
-
// @RequestMapping(path = "/api") → (annotation_argument_list (element_value_pair key:(identifier) value:(string_literal)))
|
|
30
|
-
// @RequestMapping(value = "/api") → same as above
|
|
31
|
-
//
|
|
32
|
-
// The named-argument pattern MUST constrain the `key` field to the route
|
|
33
|
-
// member names (`path`/`value`); without it, the query also captures
|
|
34
|
-
// non-route attributes such as `produces`, `consumes`, `headers`, `name`,
|
|
35
|
-
// `params` (their right-hand string literals would be mis-extracted as
|
|
36
|
-
// route prefixes — e.g. `produces = "application/json"` would corrupt
|
|
37
|
-
// every method route under that controller). The sibling
|
|
38
|
-
// `topic-patterns/java.ts` uses the same `key:` constraint approach.
|
|
39
|
-
const SPRING_CLASS_PREFIX_PATTERNS = compilePatterns({
|
|
40
|
-
name: 'java-spring-class-prefix',
|
|
25
|
+
// ─── Provider: Spring class/interface-level @RequestMapping prefix ───
|
|
26
|
+
const SPRING_TYPE_PREFIX_PATTERNS = compilePatterns({
|
|
27
|
+
name: 'java-spring-type-prefix',
|
|
41
28
|
language: Java,
|
|
42
29
|
patterns: [
|
|
43
30
|
{
|
|
44
31
|
meta: {},
|
|
45
32
|
query: `
|
|
46
|
-
|
|
47
|
-
(
|
|
48
|
-
(
|
|
49
|
-
|
|
50
|
-
|
|
33
|
+
[
|
|
34
|
+
(class_declaration
|
|
35
|
+
(modifiers
|
|
36
|
+
(annotation
|
|
37
|
+
name: (identifier) @ann (#eq? @ann "RequestMapping")
|
|
38
|
+
arguments: (annotation_argument_list (string_literal) @prefix)))) @type
|
|
39
|
+
(interface_declaration
|
|
40
|
+
(modifiers
|
|
41
|
+
(annotation
|
|
42
|
+
name: (identifier) @ann (#eq? @ann "RequestMapping")
|
|
43
|
+
arguments: (annotation_argument_list (string_literal) @prefix)))) @type
|
|
44
|
+
]
|
|
51
45
|
`,
|
|
52
46
|
},
|
|
53
47
|
{
|
|
54
48
|
meta: {},
|
|
55
49
|
query: `
|
|
56
|
-
|
|
57
|
-
(
|
|
58
|
-
(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
[
|
|
51
|
+
(class_declaration
|
|
52
|
+
(modifiers
|
|
53
|
+
(annotation
|
|
54
|
+
name: (identifier) @ann (#eq? @ann "RequestMapping")
|
|
55
|
+
arguments: (annotation_argument_list
|
|
56
|
+
(element_value_pair
|
|
57
|
+
key: (identifier) @key (#match? @key "^(path|value)$")
|
|
58
|
+
value: (string_literal) @prefix))))) @type
|
|
59
|
+
(interface_declaration
|
|
60
|
+
(modifiers
|
|
61
|
+
(annotation
|
|
62
|
+
name: (identifier) @ann (#eq? @ann "RequestMapping")
|
|
63
|
+
arguments: (annotation_argument_list
|
|
64
|
+
(element_value_pair
|
|
65
|
+
key: (identifier) @key (#match? @key "^(path|value)$")
|
|
66
|
+
value: (string_literal) @prefix))))) @type
|
|
67
|
+
]
|
|
68
|
+
`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
const SPRING_TYPE_DECLARATION_PATTERNS = compilePatterns({
|
|
73
|
+
name: 'java-spring-type-declaration',
|
|
74
|
+
language: Java,
|
|
75
|
+
patterns: [
|
|
76
|
+
{
|
|
77
|
+
meta: {},
|
|
78
|
+
query: `
|
|
79
|
+
[
|
|
80
|
+
(class_declaration name: (identifier) @type_name) @type
|
|
81
|
+
(interface_declaration name: (identifier) @type_name) @type
|
|
82
|
+
]
|
|
64
83
|
`,
|
|
65
84
|
},
|
|
66
85
|
],
|
|
@@ -289,9 +308,9 @@ const APACHE_HTTP_CLIENT_PATTERNS = compilePatterns({
|
|
|
289
308
|
],
|
|
290
309
|
});
|
|
291
310
|
/**
|
|
292
|
-
* Find the nearest enclosing
|
|
293
|
-
* null if the node is top-level. Tree-sitter's
|
|
294
|
-
* one level at a time.
|
|
311
|
+
* Find the nearest enclosing class/interface declaration ancestor for
|
|
312
|
+
* a node, or null if the node is top-level. Tree-sitter's
|
|
313
|
+
* SyntaxNode.parent walks one level at a time.
|
|
295
314
|
*/
|
|
296
315
|
function findEnclosingClass(node) {
|
|
297
316
|
let cur = node.parent;
|
|
@@ -311,23 +330,6 @@ function findEnclosingInterface(node) {
|
|
|
311
330
|
}
|
|
312
331
|
return null;
|
|
313
332
|
}
|
|
314
|
-
function hasAnnotation(node, annotationName) {
|
|
315
|
-
for (const child of node.namedChildren) {
|
|
316
|
-
if (child.type !== 'modifiers')
|
|
317
|
-
continue;
|
|
318
|
-
for (const modifier of child.namedChildren) {
|
|
319
|
-
if (modifier.type !== 'annotation')
|
|
320
|
-
continue;
|
|
321
|
-
const nameNode = modifier.childForFieldName('name');
|
|
322
|
-
if (!nameNode)
|
|
323
|
-
continue;
|
|
324
|
-
const simpleName = nameNode.text.split('.').pop();
|
|
325
|
-
if (nameNode.text === annotationName || simpleName === annotationName)
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return false;
|
|
330
|
-
}
|
|
331
333
|
/**
|
|
332
334
|
* Join a class-level prefix and a method-level path into a single URL
|
|
333
335
|
* path. Mirrors the semantics of the original regex implementation:
|
|
@@ -341,22 +343,186 @@ function joinPath(prefix, methodPath) {
|
|
|
341
343
|
return `/${cleanSub}`;
|
|
342
344
|
return `/${cleanPrefix}/${cleanSub}`;
|
|
343
345
|
}
|
|
346
|
+
function getNodeName(node) {
|
|
347
|
+
return node.childForFieldName('name')?.text ?? null;
|
|
348
|
+
}
|
|
349
|
+
function hasAnnotation(node, names) {
|
|
350
|
+
const modifiers = node.namedChildren.find((child) => child.type === 'modifiers');
|
|
351
|
+
if (!modifiers)
|
|
352
|
+
return false;
|
|
353
|
+
const allowed = new Set(typeof names === 'string' ? [names] : names);
|
|
354
|
+
const stack = [...modifiers.namedChildren];
|
|
355
|
+
while (stack.length > 0) {
|
|
356
|
+
const cur = stack.pop();
|
|
357
|
+
const annotationName = cur.childForFieldName('name')?.text ?? '';
|
|
358
|
+
const simpleName = annotationName.split('.').pop() ?? annotationName;
|
|
359
|
+
if ((cur.type === 'annotation' || cur.type === 'marker_annotation') &&
|
|
360
|
+
(allowed.has(annotationName) || allowed.has(simpleName))) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
stack.push(...cur.namedChildren);
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
function collectTypePrefixes(tree) {
|
|
368
|
+
const prefixByTypeId = new Map();
|
|
369
|
+
for (const match of runCompiledPatterns(SPRING_TYPE_PREFIX_PATTERNS, tree)) {
|
|
370
|
+
const prefixNode = match.captures.prefix;
|
|
371
|
+
const typeNode = match.captures.type;
|
|
372
|
+
if (!prefixNode || !typeNode)
|
|
373
|
+
continue;
|
|
374
|
+
const prefix = unquoteLiteral(prefixNode.text);
|
|
375
|
+
if (prefix !== null)
|
|
376
|
+
prefixByTypeId.set(typeNode.id, prefix);
|
|
377
|
+
}
|
|
378
|
+
return prefixByTypeId;
|
|
379
|
+
}
|
|
380
|
+
function collectMethodRoutes(tree) {
|
|
381
|
+
const routesByMethodId = new Map();
|
|
382
|
+
for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
|
|
383
|
+
const annNode = match.captures.ann;
|
|
384
|
+
const pathNode = match.captures.path;
|
|
385
|
+
const methodNode = match.captures.method;
|
|
386
|
+
if (!annNode || !pathNode || !methodNode)
|
|
387
|
+
continue;
|
|
388
|
+
const httpMethod = METHOD_ANNOTATION_TO_HTTP[annNode.text];
|
|
389
|
+
if (!httpMethod)
|
|
390
|
+
continue;
|
|
391
|
+
const rawPath = unquoteLiteral(pathNode.text);
|
|
392
|
+
if (rawPath === null)
|
|
393
|
+
continue;
|
|
394
|
+
const routes = routesByMethodId.get(methodNode.id) ?? [];
|
|
395
|
+
routes.push({ method: httpMethod, path: rawPath });
|
|
396
|
+
routesByMethodId.set(methodNode.id, routes);
|
|
397
|
+
}
|
|
398
|
+
return routesByMethodId;
|
|
399
|
+
}
|
|
400
|
+
function collectDirectMethods(typeNode) {
|
|
401
|
+
const out = [];
|
|
402
|
+
const visit = (node) => {
|
|
403
|
+
for (const child of node.namedChildren) {
|
|
404
|
+
if (child.type === 'method_declaration') {
|
|
405
|
+
out.push(child);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (child !== typeNode &&
|
|
409
|
+
(child.type === 'class_declaration' || child.type === 'interface_declaration')) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
visit(child);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
visit(typeNode);
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
function collectImplementedInterfaces(typeNode) {
|
|
419
|
+
const interfacesNode = typeNode.childForFieldName('interfaces');
|
|
420
|
+
if (!interfacesNode)
|
|
421
|
+
return [];
|
|
422
|
+
const out = [];
|
|
423
|
+
const visit = (node) => {
|
|
424
|
+
if (node.type === 'type_identifier' || node.type === 'scoped_type_identifier') {
|
|
425
|
+
out.push(node.text.split('.').pop() ?? node.text);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
for (const child of node.namedChildren)
|
|
429
|
+
visit(child);
|
|
430
|
+
};
|
|
431
|
+
visit(interfacesNode);
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
function collectSpringTypes(filePath, tree) {
|
|
435
|
+
const prefixByTypeId = collectTypePrefixes(tree);
|
|
436
|
+
const routesByMethodId = collectMethodRoutes(tree);
|
|
437
|
+
const out = [];
|
|
438
|
+
for (const match of runCompiledPatterns(SPRING_TYPE_DECLARATION_PATTERNS, tree)) {
|
|
439
|
+
const typeNode = match.captures.type;
|
|
440
|
+
const typeNameNode = match.captures.type_name;
|
|
441
|
+
if (!typeNode || !typeNameNode)
|
|
442
|
+
continue;
|
|
443
|
+
const kind = typeNode.type === 'interface_declaration' ? 'interface' : 'class';
|
|
444
|
+
const methods = collectDirectMethods(typeNode)
|
|
445
|
+
.map((methodNode) => ({
|
|
446
|
+
name: getNodeName(methodNode),
|
|
447
|
+
routes: routesByMethodId.get(methodNode.id) ?? [],
|
|
448
|
+
}))
|
|
449
|
+
.filter((method) => method.name !== null);
|
|
450
|
+
out.push({
|
|
451
|
+
filePath,
|
|
452
|
+
kind,
|
|
453
|
+
name: typeNameNode.text,
|
|
454
|
+
classPrefix: prefixByTypeId.get(typeNode.id) ?? '',
|
|
455
|
+
implementedInterfaces: kind === 'class' ? collectImplementedInterfaces(typeNode) : [],
|
|
456
|
+
isController: kind === 'class' && hasAnnotation(typeNode, ['RestController', 'Controller']),
|
|
457
|
+
methods,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
function scanSpringProject(files) {
|
|
463
|
+
const types = files.flatMap((file) => collectSpringTypes(file.filePath, file.tree));
|
|
464
|
+
const interfaceRoutes = new Map();
|
|
465
|
+
for (const type of types) {
|
|
466
|
+
if (type.kind !== 'interface')
|
|
467
|
+
continue;
|
|
468
|
+
if (interfaceRoutes.has(type.name)) {
|
|
469
|
+
interfaceRoutes.set(type.name, null);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const methodMap = new Map();
|
|
473
|
+
for (const method of type.methods) {
|
|
474
|
+
const routes = method.routes.map((route) => ({
|
|
475
|
+
method: route.method,
|
|
476
|
+
path: type.classPrefix ? joinPath(type.classPrefix, route.path) : route.path,
|
|
477
|
+
}));
|
|
478
|
+
if (routes.length > 0)
|
|
479
|
+
methodMap.set(method.name, routes);
|
|
480
|
+
}
|
|
481
|
+
interfaceRoutes.set(type.name, methodMap);
|
|
482
|
+
}
|
|
483
|
+
const detectionsByFile = new Map();
|
|
484
|
+
for (const type of types) {
|
|
485
|
+
if (type.kind !== 'class' || !type.isController)
|
|
486
|
+
continue;
|
|
487
|
+
for (const method of type.methods) {
|
|
488
|
+
if (method.routes.length > 0)
|
|
489
|
+
continue;
|
|
490
|
+
const inheritedRoutes = type.implementedInterfaces.flatMap((interfaceName) => {
|
|
491
|
+
const routeMap = interfaceRoutes.get(interfaceName);
|
|
492
|
+
if (!routeMap)
|
|
493
|
+
return [];
|
|
494
|
+
const routes = routeMap.get(method.name) ?? [];
|
|
495
|
+
return routes.map((route) => ({
|
|
496
|
+
method: route.method,
|
|
497
|
+
path: joinPath(type.classPrefix, route.path),
|
|
498
|
+
}));
|
|
499
|
+
});
|
|
500
|
+
for (const route of inheritedRoutes) {
|
|
501
|
+
const detections = detectionsByFile.get(type.filePath) ?? [];
|
|
502
|
+
detections.push({
|
|
503
|
+
role: 'provider',
|
|
504
|
+
framework: 'spring',
|
|
505
|
+
method: route.method,
|
|
506
|
+
path: route.path,
|
|
507
|
+
name: method.name,
|
|
508
|
+
confidence: 0.8,
|
|
509
|
+
});
|
|
510
|
+
detectionsByFile.set(type.filePath, detections);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return [...detectionsByFile.entries()].map(([filePath, detections]) => ({
|
|
515
|
+
filePath,
|
|
516
|
+
detections,
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
344
519
|
export const JAVA_HTTP_PLUGIN = {
|
|
345
520
|
name: 'java-http',
|
|
346
521
|
language: Java,
|
|
347
522
|
scan(tree) {
|
|
348
523
|
const out = [];
|
|
349
524
|
// ─── Providers: Spring class prefix + method annotations ────────
|
|
350
|
-
const
|
|
351
|
-
for (const match of runCompiledPatterns(SPRING_CLASS_PREFIX_PATTERNS, tree)) {
|
|
352
|
-
const prefixNode = match.captures.prefix;
|
|
353
|
-
const classNode = match.captures.class;
|
|
354
|
-
if (!prefixNode || !classNode)
|
|
355
|
-
continue;
|
|
356
|
-
const prefix = unquoteLiteral(prefixNode.text);
|
|
357
|
-
if (prefix !== null)
|
|
358
|
-
prefixByClassId.set(classNode.id, prefix);
|
|
359
|
-
}
|
|
525
|
+
const prefixByTypeId = collectTypePrefixes(tree);
|
|
360
526
|
const feignPrefixByInterfaceId = new Map();
|
|
361
527
|
for (const match of runCompiledPatterns(FEIGN_INTERFACE_PREFIX_PATTERNS, tree)) {
|
|
362
528
|
const prefixNode = match.captures.prefix;
|
|
@@ -395,7 +561,9 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
395
561
|
continue;
|
|
396
562
|
}
|
|
397
563
|
const enclosingClass = findEnclosingClass(methodNode);
|
|
398
|
-
|
|
564
|
+
if (!enclosingClass)
|
|
565
|
+
continue;
|
|
566
|
+
const prefix = prefixByTypeId.get(enclosingClass.id) ?? '';
|
|
399
567
|
const fullPath = joinPath(prefix, rawPath);
|
|
400
568
|
out.push({
|
|
401
569
|
role: 'provider',
|
|
@@ -528,4 +696,5 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
528
696
|
}
|
|
529
697
|
return out;
|
|
530
698
|
},
|
|
699
|
+
scanProject: scanSpringProject,
|
|
531
700
|
};
|
|
@@ -36,6 +36,14 @@ export interface HttpDetection {
|
|
|
36
36
|
/** Confidence in (0, 1]. Source-scan plugins typically use 0.7–0.8. */
|
|
37
37
|
confidence: number;
|
|
38
38
|
}
|
|
39
|
+
export interface HttpScanInput {
|
|
40
|
+
filePath: string;
|
|
41
|
+
tree: Parser.Tree;
|
|
42
|
+
}
|
|
43
|
+
export interface HttpFileDetections {
|
|
44
|
+
filePath: string;
|
|
45
|
+
detections: HttpDetection[];
|
|
46
|
+
}
|
|
39
47
|
/**
|
|
40
48
|
* One language-scoped HTTP plugin. The plugin owns the tree-sitter
|
|
41
49
|
* grammar and the `scan` function that translates a parsed tree into
|
|
@@ -90,4 +98,10 @@ export interface HttpLanguagePlugin {
|
|
|
90
98
|
* single-file plugins can keep their unary `scan(tree)` shape.
|
|
91
99
|
*/
|
|
92
100
|
scan(tree: Parser.Tree, repoContext?: RepoContext, fileRel?: string): HttpDetection[];
|
|
101
|
+
/**
|
|
102
|
+
* Optional project-level scan hook for language rules that require
|
|
103
|
+
* multiple files, such as Java controllers inheriting Spring mappings
|
|
104
|
+
* from annotated interfaces.
|
|
105
|
+
*/
|
|
106
|
+
scanProject?(files: readonly HttpScanInput[]): HttpFileDetections[];
|
|
93
107
|
}
|
|
@@ -4,7 +4,7 @@ import Parser from 'tree-sitter';
|
|
|
4
4
|
import { createIgnoreFilter } from '../../../config/ignore-service.js';
|
|
5
5
|
import { readSafe } from './fs-utils.js';
|
|
6
6
|
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
7
|
-
import { getPluginForFile, HTTP_SCAN_GLOB } from './http-patterns/index.js';
|
|
7
|
+
import { getPluginForFile, HTTP_SCAN_GLOB, } from './http-patterns/index.js';
|
|
8
8
|
/**
|
|
9
9
|
* Language-agnostic orchestrator for HTTP route (provider + consumer)
|
|
10
10
|
* contract extraction. Two strategies, in order of preference per role:
|
|
@@ -141,6 +141,9 @@ export class HttpRouteExtractor {
|
|
|
141
141
|
// both graph-assisted enrichment and source-scan emission.
|
|
142
142
|
const parser = new Parser();
|
|
143
143
|
const cachedDetections = new Map();
|
|
144
|
+
const cachedInputs = new Map();
|
|
145
|
+
const projectDetections = new Map();
|
|
146
|
+
let projectScanComplete = false;
|
|
144
147
|
// Per-plugin cross-file context (e.g. Python's FastAPI router →
|
|
145
148
|
// include_router(prefix=...) map). Built lazily on first
|
|
146
149
|
// `getDetections` call for a file the plugin handles, scoped to the
|
|
@@ -169,33 +172,45 @@ export class HttpRouteExtractor {
|
|
|
169
172
|
return undefined;
|
|
170
173
|
}
|
|
171
174
|
};
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return cached;
|
|
175
|
+
const getScanInput = async (rel) => {
|
|
176
|
+
if (cachedInputs.has(rel))
|
|
177
|
+
return cachedInputs.get(rel) ?? null;
|
|
176
178
|
const plugin = getPluginForFile(rel);
|
|
177
179
|
if (!plugin) {
|
|
178
|
-
|
|
179
|
-
return
|
|
180
|
+
cachedInputs.set(rel, null);
|
|
181
|
+
return null;
|
|
180
182
|
}
|
|
181
183
|
const repoContext = await ensureRepoContext(plugin);
|
|
182
184
|
const content = readSafe(repoPath, rel);
|
|
183
185
|
if (!content) {
|
|
184
|
-
|
|
185
|
-
return
|
|
186
|
+
cachedInputs.set(rel, null);
|
|
187
|
+
return null;
|
|
186
188
|
}
|
|
187
189
|
try {
|
|
188
190
|
parser.setLanguage(plugin.language);
|
|
189
191
|
const tree = parseSourceSafe(parser, content);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
const input = { filePath: rel, tree };
|
|
193
|
+
const item = { plugin, input, repoContext };
|
|
194
|
+
cachedInputs.set(rel, item);
|
|
195
|
+
return item;
|
|
193
196
|
}
|
|
194
197
|
catch {
|
|
195
|
-
|
|
196
|
-
return
|
|
198
|
+
cachedInputs.set(rel, null);
|
|
199
|
+
return null;
|
|
197
200
|
}
|
|
198
201
|
};
|
|
202
|
+
const getDetections = async (rel) => {
|
|
203
|
+
const cached = cachedDetections.get(rel);
|
|
204
|
+
if (cached)
|
|
205
|
+
return cached;
|
|
206
|
+
const scanInput = await getScanInput(rel);
|
|
207
|
+
const ownDetections = scanInput
|
|
208
|
+
? scanInput.plugin.scan(scanInput.input.tree, scanInput.repoContext, rel)
|
|
209
|
+
: [];
|
|
210
|
+
const detections = [...ownDetections, ...(projectDetections.get(rel) ?? [])];
|
|
211
|
+
cachedDetections.set(rel, detections);
|
|
212
|
+
return detections;
|
|
213
|
+
};
|
|
199
214
|
// Glob the source-scan file list at most once per extract() —
|
|
200
215
|
// both provider and consumer fallback paths share the same list.
|
|
201
216
|
let scannedFiles = null;
|
|
@@ -205,12 +220,36 @@ export class HttpRouteExtractor {
|
|
|
205
220
|
scannedFiles = await this.scanFiles(repoPath);
|
|
206
221
|
return scannedFiles;
|
|
207
222
|
};
|
|
223
|
+
const collectProjectDetections = async (files) => {
|
|
224
|
+
if (projectScanComplete)
|
|
225
|
+
return;
|
|
226
|
+
projectScanComplete = true;
|
|
227
|
+
const byPlugin = new Map();
|
|
228
|
+
for (const rel of files) {
|
|
229
|
+
const scanInput = await getScanInput(rel);
|
|
230
|
+
if (!scanInput?.plugin.scanProject)
|
|
231
|
+
continue;
|
|
232
|
+
const items = byPlugin.get(scanInput.plugin) ?? [];
|
|
233
|
+
items.push(scanInput.input);
|
|
234
|
+
byPlugin.set(scanInput.plugin, items);
|
|
235
|
+
}
|
|
236
|
+
for (const [plugin, inputs] of byPlugin) {
|
|
237
|
+
const results = plugin.scanProject?.(inputs) ?? [];
|
|
238
|
+
for (const result of results) {
|
|
239
|
+
const existing = projectDetections.get(result.filePath) ?? [];
|
|
240
|
+
projectDetections.set(result.filePath, [...existing, ...result.detections]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
cachedDetections.clear();
|
|
244
|
+
};
|
|
245
|
+
const files = await getScannedFiles();
|
|
246
|
+
await collectProjectDetections(files);
|
|
208
247
|
const graphProviders = dbExecutor != null ? await this.extractProvidersGraph(dbExecutor, getDetections) : [];
|
|
209
248
|
// Source scan always runs to capture routes in languages/files not covered
|
|
210
249
|
// by graph edges; the glob and per-file parse results are cached above.
|
|
211
|
-
const providers = this.mergeGraphAndSourceContracts(graphProviders, await this.extractProvidersSourceScan(
|
|
250
|
+
const providers = this.mergeGraphAndSourceContracts(graphProviders, await this.extractProvidersSourceScan(files, getDetections));
|
|
212
251
|
const graphConsumers = dbExecutor != null ? await this.extractConsumersGraph(dbExecutor, getDetections) : [];
|
|
213
|
-
const consumers = this.mergeGraphAndSourceContracts(graphConsumers, await this.extractConsumersSourceScan(
|
|
252
|
+
const consumers = this.mergeGraphAndSourceContracts(graphConsumers, await this.extractConsumersSourceScan(files, getDetections));
|
|
214
253
|
return [...providers, ...consumers];
|
|
215
254
|
}
|
|
216
255
|
async scanFiles(repoPath) {
|
package/package.json
CHANGED