gitnexus 1.6.10-rc.216 → 1.6.10-rc.218
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/extractors/http-patterns/java.js +187 -8
- package/dist/core/group/extractors/http-patterns/node.js +283 -39
- package/dist/core/ingestion/language-provider.d.ts +58 -0
- package/dist/core/ingestion/language-provider.js +21 -0
- package/dist/core/ingestion/languages/java.js +21 -0
- package/dist/core/ingestion/languages/python.js +13 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +9 -2
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +8 -2
- package/dist/core/ingestion/route-extractors/constant-resolver.js +11 -7
- package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +114 -0
- package/dist/core/ingestion/route-extractors/java-const-resolver.js +569 -0
- package/dist/core/ingestion/route-extractors/js-const-resolver.d.ts +192 -0
- package/dist/core/ingestion/route-extractors/js-const-resolver.js +1033 -0
- package/dist/core/ingestion/route-extractors/spring.js +105 -5
- package/dist/core/ingestion/workers/parse-worker.js +13 -6
- package/dist/storage/parse-cache.js +32 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import Java from 'tree-sitter-java';
|
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
3
|
import { springAnnotationHttpMethods, intersectSpringHttpMethods, isRouteMemberKey, findEnclosingClass, joinPath, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
4
|
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
5
|
+
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, parseJavaConstOperands, } from '../../../ingestion/route-extractors/java-const-resolver.js';
|
|
5
6
|
import { extractStaticPathExpression, inferOkHttpMethod, inferHttpClientMethod, okHttpUrlRootsAtBuilder, httpClientUriRootsAtNewBuilder, httpClientChainHasUriCall, } from './java-static-path.js';
|
|
6
7
|
/**
|
|
7
8
|
* Java HTTP plugin. Handles:
|
|
@@ -123,6 +124,34 @@ const JAVA_ROUTE_ANNOTATION_PATTERNS = compilePatterns({
|
|
|
123
124
|
key: (identifier) @key
|
|
124
125
|
value: [(string_literal) @value (element_value_array_initializer (string_literal) @value)]))))
|
|
125
126
|
name: (identifier) @member) @node
|
|
127
|
+
(class_declaration
|
|
128
|
+
(modifiers
|
|
129
|
+
(annotation
|
|
130
|
+
name: [(identifier) (scoped_identifier)] @ann
|
|
131
|
+
arguments: (annotation_argument_list [(identifier) @value_expr (field_access) @value_expr (binary_expression) @value_expr])))) @node
|
|
132
|
+
(class_declaration
|
|
133
|
+
(modifiers
|
|
134
|
+
(annotation
|
|
135
|
+
name: [(identifier) (scoped_identifier)] @ann
|
|
136
|
+
arguments: (annotation_argument_list
|
|
137
|
+
(element_value_pair
|
|
138
|
+
key: (identifier) @key
|
|
139
|
+
value: [(identifier) @value_expr (field_access) @value_expr (binary_expression) @value_expr]))))) @node
|
|
140
|
+
(method_declaration
|
|
141
|
+
(modifiers
|
|
142
|
+
(annotation
|
|
143
|
+
name: [(identifier) (scoped_identifier)] @ann
|
|
144
|
+
arguments: (annotation_argument_list [(identifier) @value_expr (field_access) @value_expr (binary_expression) @value_expr])))
|
|
145
|
+
name: (identifier) @member) @node
|
|
146
|
+
(method_declaration
|
|
147
|
+
(modifiers
|
|
148
|
+
(annotation
|
|
149
|
+
name: [(identifier) (scoped_identifier)] @ann
|
|
150
|
+
arguments: (annotation_argument_list
|
|
151
|
+
(element_value_pair
|
|
152
|
+
key: (identifier) @key
|
|
153
|
+
value: [(identifier) @value_expr (field_access) @value_expr (binary_expression) @value_expr]))))
|
|
154
|
+
name: (identifier) @member) @node
|
|
126
155
|
]
|
|
127
156
|
`,
|
|
128
157
|
},
|
|
@@ -407,6 +436,7 @@ function scanRouteAnnotations(tree) {
|
|
|
407
436
|
// feeds the OpenFeign *consumer* path in scan(). An interface carrying both
|
|
408
437
|
// `@RequestMapping` and `@FeignClient(path)` lands a different value in each.
|
|
409
438
|
const prefixByTypeId = new Map();
|
|
439
|
+
const typesWithUnfoldablePrefix = new Set();
|
|
410
440
|
const feignPrefixByInterfaceId = new Map();
|
|
411
441
|
const httpExchangePrefixByTypeId = new Map();
|
|
412
442
|
const methodRoutes = [];
|
|
@@ -422,7 +452,10 @@ function scanRouteAnnotations(tree) {
|
|
|
422
452
|
const annNode = captures.ann;
|
|
423
453
|
const node = captures.node;
|
|
424
454
|
const valueNode = captures.value;
|
|
425
|
-
|
|
455
|
+
// A non-literal annotation value (constant ref / `+`-concat) is captured
|
|
456
|
+
// as @value_expr instead of @value — one of the two must be present.
|
|
457
|
+
const valueExprNode = captures.value_expr;
|
|
458
|
+
if (!annNode || !node || (!valueNode && !valueExprNode))
|
|
426
459
|
continue;
|
|
427
460
|
// Discrimination is on the trailing segment only (`simpleName`), so a
|
|
428
461
|
// non-Spring annotation whose last segment collides with a route annotation
|
|
@@ -446,7 +479,7 @@ function scanRouteAnnotations(tree) {
|
|
|
446
479
|
const feignHttpMethod = httpMethods.length === 1 ? (httpMethods[0] === '*' ? 'GET' : httpMethods[0]) : null;
|
|
447
480
|
if (!isRouteMemberKey(keyNode))
|
|
448
481
|
continue;
|
|
449
|
-
const rawPath = unquoteLiteral(valueNode.text);
|
|
482
|
+
const rawPath = valueNode ? unquoteLiteral(valueNode.text) : null;
|
|
450
483
|
if (rawPath !== null) {
|
|
451
484
|
for (const httpMethod of httpMethods) {
|
|
452
485
|
methodRoutes.push({
|
|
@@ -458,11 +491,36 @@ function scanRouteAnnotations(tree) {
|
|
|
458
491
|
});
|
|
459
492
|
}
|
|
460
493
|
}
|
|
494
|
+
else {
|
|
495
|
+
// Non-literal path (a constant reference or `+`-concatenation).
|
|
496
|
+
// Defer to scan(): the fold needs the repo-wide constant map built
|
|
497
|
+
// by prepareRepo. Capture the operand list now; resolution happens
|
|
498
|
+
// in scan() against JavaRepoContext, and an unresolvable operand
|
|
499
|
+
// list leaves the route skipped (KTD5 skip floor).
|
|
500
|
+
const operands = parseJavaConstOperands(valueExprNode);
|
|
501
|
+
if (operands !== null) {
|
|
502
|
+
for (const httpMethod of httpMethods) {
|
|
503
|
+
methodRoutes.push({
|
|
504
|
+
methodNode: node,
|
|
505
|
+
methodName: captures.member?.text ?? null,
|
|
506
|
+
httpMethod,
|
|
507
|
+
rawPath: '',
|
|
508
|
+
feignHttpMethod,
|
|
509
|
+
pathOperands: operands,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
461
514
|
}
|
|
462
515
|
else if (ann === 'RequestLine') {
|
|
463
516
|
// Feign packs verb + path in one literal; its only named argument is `value`.
|
|
464
517
|
if (keyNode && keyNode.text !== 'value')
|
|
465
518
|
continue;
|
|
519
|
+
// A constant-valued `@RequestLine` arrives as @value_expr, not @value —
|
|
520
|
+
// `valueNode` is undefined in that shape. Skip rather than dereference
|
|
521
|
+
// (constant folding for Feign verb+path literals is out of scope here).
|
|
522
|
+
if (!valueNode)
|
|
523
|
+
continue;
|
|
466
524
|
const raw = unquoteLiteral(valueNode.text);
|
|
467
525
|
const parsed = raw !== null ? parseRequestLine(raw) : null;
|
|
468
526
|
if (parsed) {
|
|
@@ -479,7 +537,7 @@ function scanRouteAnnotations(tree) {
|
|
|
479
537
|
// (`accept`, `contentType`, …) are not routes.
|
|
480
538
|
if (keyNode && keyNode.text !== 'url' && keyNode.text !== 'value')
|
|
481
539
|
continue;
|
|
482
|
-
const rawPath = unquoteLiteral(valueNode.text);
|
|
540
|
+
const rawPath = valueNode ? unquoteLiteral(valueNode.text) : null;
|
|
483
541
|
if (rawPath !== null) {
|
|
484
542
|
exchangeRoutes.push({
|
|
485
543
|
methodNode: node,
|
|
@@ -496,6 +554,11 @@ function scanRouteAnnotations(tree) {
|
|
|
496
554
|
if (ann === 'RequestMapping') {
|
|
497
555
|
if (!isRouteMemberKey(keyNode))
|
|
498
556
|
continue;
|
|
557
|
+
if (!valueNode) {
|
|
558
|
+
// Constant-valued class prefix — see `typesWithUnfoldablePrefix`.
|
|
559
|
+
typesWithUnfoldablePrefix.add(node.id);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
499
562
|
const prefix = unquoteLiteral(valueNode.text);
|
|
500
563
|
if (prefix !== null) {
|
|
501
564
|
pushPrefix(prefixByTypeId, node.id, prefix);
|
|
@@ -508,7 +571,7 @@ function scanRouteAnnotations(tree) {
|
|
|
508
571
|
// Feign's `name`/`value` identify a service, not a path — only `path` is a prefix.
|
|
509
572
|
if (!keyNode || keyNode.text !== 'path')
|
|
510
573
|
continue;
|
|
511
|
-
const prefix = unquoteLiteral(valueNode.text);
|
|
574
|
+
const prefix = valueNode ? unquoteLiteral(valueNode.text) : null;
|
|
512
575
|
if (prefix !== null)
|
|
513
576
|
pushPrefix(feignPrefixByInterfaceId, node.id, prefix);
|
|
514
577
|
}
|
|
@@ -517,7 +580,7 @@ function scanRouteAnnotations(tree) {
|
|
|
517
580
|
// (or positionally). Applies to its `@(Get|...)Exchange` consumer methods.
|
|
518
581
|
if (keyNode && keyNode.text !== 'url' && keyNode.text !== 'value')
|
|
519
582
|
continue;
|
|
520
|
-
const prefix = unquoteLiteral(valueNode.text);
|
|
583
|
+
const prefix = valueNode ? unquoteLiteral(valueNode.text) : null;
|
|
521
584
|
if (prefix !== null)
|
|
522
585
|
pushPrefix(httpExchangePrefixByTypeId, node.id, prefix);
|
|
523
586
|
}
|
|
@@ -566,6 +629,7 @@ function scanRouteAnnotations(tree) {
|
|
|
566
629
|
}
|
|
567
630
|
return {
|
|
568
631
|
prefixByTypeId,
|
|
632
|
+
typesWithUnfoldablePrefix,
|
|
569
633
|
feignPrefixByInterfaceId,
|
|
570
634
|
httpExchangePrefixByTypeId,
|
|
571
635
|
methodRoutes: constrainedMethodRoutes,
|
|
@@ -608,9 +672,22 @@ function collectImplementedInterfaces(typeNode) {
|
|
|
608
672
|
return out;
|
|
609
673
|
}
|
|
610
674
|
function collectSpringTypes(filePath, tree) {
|
|
611
|
-
const { prefixByTypeId, methodRoutes } = scanRouteAnnotations(tree);
|
|
675
|
+
const { prefixByTypeId, typesWithUnfoldablePrefix, methodRoutes } = scanRouteAnnotations(tree);
|
|
612
676
|
const routesByMethodId = new Map();
|
|
613
677
|
for (const route of methodRoutes) {
|
|
678
|
+
// Constant-valued class prefix: no single prefix string exists here, so the
|
|
679
|
+
// inheritance view would publish this route unprefixed. Skip — same rule as
|
|
680
|
+
// scan() and as ingestion (R4 parity).
|
|
681
|
+
const owner = findEnclosingClass(route.methodNode);
|
|
682
|
+
if (owner && typesWithUnfoldablePrefix.has(owner.id))
|
|
683
|
+
continue;
|
|
684
|
+
// A constant-referencing route still carries `rawPath: ''` here — folding
|
|
685
|
+
// happens in scan() against the repo constant map, which this
|
|
686
|
+
// inheritance-view collector has no access to. Emitting it as an empty
|
|
687
|
+
// path would publish `POST /`-shaped noise into the shared type view;
|
|
688
|
+
// skip instead (ingestion keeps the same skip floor — R4 parity).
|
|
689
|
+
if (route.pathOperands)
|
|
690
|
+
continue;
|
|
614
691
|
const routes = routesByMethodId.get(route.methodNode.id) ?? [];
|
|
615
692
|
routes.push({ method: route.httpMethod, path: route.rawPath });
|
|
616
693
|
routesByMethodId.set(route.methodNode.id, routes);
|
|
@@ -671,19 +748,121 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
671
748
|
hasConsumerSignals(content) {
|
|
672
749
|
return /\brestTemplate\b|\bwebClient\b|Request\.Builder|HttpRequest|HttpMethod\.|new\s+Http(Get|Post|Put|Delete|Patch)\b|@RequestLine|@FeignClient|Exchange/.test(content);
|
|
673
750
|
},
|
|
674
|
-
|
|
751
|
+
prepareRepo(args) {
|
|
752
|
+
// Build the repo-wide Java string-constant map once per extract() run
|
|
753
|
+
// (mirrors the Python binding's cost-gated pre-pass). A cheap content
|
|
754
|
+
// gate keeps literal-only repos at zero parses: only files containing a
|
|
755
|
+
// `static final String` declaration are parsed for constants.
|
|
756
|
+
try {
|
|
757
|
+
// The orchestrator hands over a bare Parser (no language set yet);
|
|
758
|
+
// bind Java explicitly — Python's prepareRepo does the same — otherwise
|
|
759
|
+
// parseSourceSafe spins to its 15 s budget per file.
|
|
760
|
+
args.parser.setLanguage(Java);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
// fall through: a parser that rejects binding cannot produce a constant
|
|
764
|
+
// map; per-file try/catch below then skips everything harmlessly.
|
|
765
|
+
}
|
|
766
|
+
const constants = new Map();
|
|
767
|
+
for (const rel of args.files) {
|
|
768
|
+
if (!rel.endsWith('.java'))
|
|
769
|
+
continue;
|
|
770
|
+
try {
|
|
771
|
+
const src = args.readFile(rel);
|
|
772
|
+
// Cheap content gate: only constant-DEFINITION candidates get parsed
|
|
773
|
+
// here (~hundreds of files). Import-only files (every controller)
|
|
774
|
+
// are deliberately NOT parsed in this pass — scan() lazily extracts
|
|
775
|
+
// the importing file's own import table from the tree it already
|
|
776
|
+
// holds when a constant-referencing route actually needs the fold.
|
|
777
|
+
// A gate that also matched `import ...;` would parse the entire
|
|
778
|
+
// repository here (tens of thousands of files) just to build import
|
|
779
|
+
// tables the fold can derive per-file on demand.
|
|
780
|
+
//
|
|
781
|
+
// The predicate is the SHARED one the ingestion provider uses, so the
|
|
782
|
+
// two subsystems agree on which files define constants. Its previous
|
|
783
|
+
// local spelling missed `final static String` and lowercase interface
|
|
784
|
+
// names, and admitted an interface that ingestion's gate rejected.
|
|
785
|
+
if (!src || !isJavaConstantFile(src)) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const tree = args.parseSource(args.parser, src);
|
|
789
|
+
if (!tree)
|
|
790
|
+
continue;
|
|
791
|
+
const mc = extractJavaModuleConstants(tree);
|
|
792
|
+
if (mc.literals.size > 0 || mc.exprs.size > 0 || mc.imports.size > 0) {
|
|
793
|
+
constants.set(rel, mc);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
// Per-file resilience: one unreadable/oversized/ill-formed file must
|
|
798
|
+
// not forfeit the whole repo's constant map (a missing constants
|
|
799
|
+
// class only degrades refs that pointed at it).
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return { constants };
|
|
804
|
+
},
|
|
805
|
+
scan(tree, repoContext, fileRel) {
|
|
675
806
|
const out = [];
|
|
807
|
+
const javaCtx = repoContext;
|
|
676
808
|
// ─── Spring providers + OpenFeign consumers (one query pass) ────
|
|
677
809
|
// `scanRouteAnnotations` resolves every route-defining annotation —
|
|
678
810
|
// class/interface prefixes, method shortcut/`@RequestMapping`s and native
|
|
679
811
|
// `@RequestLine`s — from a single `matches()` pass over the tree.
|
|
680
|
-
const { prefixByTypeId, feignPrefixByInterfaceId, httpExchangePrefixByTypeId, methodRoutes, requestLines, exchangeRoutes, } = scanRouteAnnotations(tree);
|
|
812
|
+
const { prefixByTypeId, typesWithUnfoldablePrefix, feignPrefixByInterfaceId, httpExchangePrefixByTypeId, methodRoutes, requestLines, exchangeRoutes, } = scanRouteAnnotations(tree);
|
|
681
813
|
// A `@(Get|...)Mapping` inside a `@FeignClient` interface is an OpenFeign
|
|
682
814
|
// *consumer* (it describes a remote call); the same annotation inside a
|
|
683
815
|
// class is a Spring *provider*. A mapping on a non-Feign interface has no
|
|
684
816
|
// enclosing class and is dropped here — interface→controller inheritance is
|
|
685
817
|
// handled by `scanProject`.
|
|
818
|
+
// Lazy per-file constants view. prepareRepo only indexes constant-
|
|
819
|
+
// DEFINING files (cheap gate); an importing controller is absent from
|
|
820
|
+
// that map. When a route actually references a constant, extract THIS
|
|
821
|
+
// file's import table from the tree scan() already holds (zero extra
|
|
822
|
+
// parses) and overlay it for the fold. Files whose routes are all
|
|
823
|
+
// literal — the overwhelming majority — never pay this cost.
|
|
824
|
+
let foldConstants;
|
|
825
|
+
const getFoldConstants = () => {
|
|
826
|
+
if (foldConstants !== undefined)
|
|
827
|
+
return foldConstants;
|
|
828
|
+
foldConstants = javaCtx?.constants;
|
|
829
|
+
if (!javaCtx?.constants || !fileRel)
|
|
830
|
+
return foldConstants;
|
|
831
|
+
if (javaCtx.constants.has(fileRel))
|
|
832
|
+
return foldConstants;
|
|
833
|
+
try {
|
|
834
|
+
const mc = extractJavaModuleConstants(tree);
|
|
835
|
+
if (mc.imports.size > 0) {
|
|
836
|
+
const merged = new Map(javaCtx.constants);
|
|
837
|
+
merged.set(fileRel, mc);
|
|
838
|
+
foldConstants = merged;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
// fold falls back to the repo-wide map (imports stay unresolved)
|
|
843
|
+
}
|
|
844
|
+
return foldConstants;
|
|
845
|
+
};
|
|
686
846
|
for (const route of methodRoutes) {
|
|
847
|
+
// A constant-valued CLASS prefix cannot be folded here, so every method
|
|
848
|
+
// route under such a class is suppressed rather than emitted at a wrong
|
|
849
|
+
// (unprefixed) path — the same rule `classesWithArrayPrefix` already
|
|
850
|
+
// encodes for the array form, and the same rule ingestion applies.
|
|
851
|
+
const owner = findEnclosingClass(route.methodNode);
|
|
852
|
+
if (owner && typesWithUnfoldablePrefix.has(owner.id))
|
|
853
|
+
continue;
|
|
854
|
+
// Non-literal route path: fold the operand list against the repo-wide
|
|
855
|
+
// constant map. Skip (never a guessed path) when the fold fails or the
|
|
856
|
+
// repo context is absent (context-less fallback scanning).
|
|
857
|
+
if (route.pathOperands && javaCtx && fileRel) {
|
|
858
|
+
const resolved = foldJavaOperands(fileRel, route.pathOperands, getFoldConstants());
|
|
859
|
+
if (resolved === null)
|
|
860
|
+
continue;
|
|
861
|
+
route.rawPath = resolved;
|
|
862
|
+
}
|
|
863
|
+
else if (route.pathOperands) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
687
866
|
const enclosingInterface = findEnclosingInterface(route.methodNode);
|
|
688
867
|
if (enclosingInterface && hasAnnotation(enclosingInterface, 'FeignClient')) {
|
|
689
868
|
if (!route.feignHttpMethod)
|