gitnexus 1.6.10-rc.216 → 1.6.10-rc.217
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/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 +6 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +1 -1
- 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/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)
|
|
@@ -23,6 +23,7 @@ import type { CfgVisitor } from './cfg/types.js';
|
|
|
23
23
|
import type { NodeLabel } from '../../_shared/index.js';
|
|
24
24
|
import type { ExtractedRoute } from './route-extractors/laravel.js';
|
|
25
25
|
import type { SharedSpringType } from './route-extractors/spring-shared.js';
|
|
26
|
+
import type { ModuleConstants, Operand, RepoConstants } from './route-extractors/constant-resolver.js';
|
|
26
27
|
import type Parser from 'tree-sitter';
|
|
27
28
|
import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
|
|
28
29
|
/** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
|
|
@@ -39,6 +40,18 @@ export interface AstFrameworkPatternConfig {
|
|
|
39
40
|
* Required fields must be explicitly set; optional fields have defaults
|
|
40
41
|
* applied by defineLanguage().
|
|
41
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* Should the parse worker run {@link LanguageProviderConfig.extractModuleConstants}
|
|
45
|
+
* on this file?
|
|
46
|
+
*
|
|
47
|
+
* Exported so the DECISION is testable without booting a worker. It encodes the
|
|
48
|
+
* one rule that is easy to get backwards: a provider that declares no
|
|
49
|
+
* `moduleConstantHeuristic` harvests unconditionally. Writing the gate as
|
|
50
|
+
* `provider.moduleConstantHeuristic?.(content)` reads `undefined` as "skip" and
|
|
51
|
+
* silently disables the hook for every provider without a heuristic — which is
|
|
52
|
+
* exactly how Python's already-shipped harvest was turned off (#2391/#2980).
|
|
53
|
+
*/
|
|
54
|
+
export declare function shouldHarvestModuleConstants(provider: Pick<LanguageProvider, 'extractModuleConstants' | 'moduleConstantHeuristic'>, content: string): boolean;
|
|
42
55
|
interface LanguageProviderConfig {
|
|
43
56
|
readonly id: SupportedLanguages;
|
|
44
57
|
/** File extensions that map to this language (e.g., ['.ts', '.tsx']) */
|
|
@@ -266,6 +279,51 @@ interface LanguageProviderConfig {
|
|
|
266
279
|
* Default: undefined (no interface-inheritance route resolution).
|
|
267
280
|
*/
|
|
268
281
|
readonly extractRouteInheritanceTypes?: (tree: Parser.Tree, filePath: string) => SharedSpringType[];
|
|
282
|
+
/**
|
|
283
|
+
* Harvest this file's module-level string constants (#2391 core, #2980 Java
|
|
284
|
+
* parity) into the language-agnostic {@link ModuleConstants} shape, so the
|
|
285
|
+
* parse phase can resolve non-literal decorator route paths cross-file.
|
|
286
|
+
*
|
|
287
|
+
* The worker calls this when BOTH hold:
|
|
288
|
+
* - the provider declares no `moduleConstantHeuristic`, or the one it
|
|
289
|
+
* declares matched — syntax-driven, e.g. a `static final String` field or
|
|
290
|
+
* a constants-bearing import; NEVER a class-name pattern like
|
|
291
|
+
* `*Constants`, which silently drops route constants living in classes
|
|
292
|
+
* named e.g. `ApiPaths`/`Routes`, and
|
|
293
|
+
* - the extraction yields something resolvable (a literal, an expression, or
|
|
294
|
+
* an import binding), keeping the aggregate bounded on large repos.
|
|
295
|
+
*
|
|
296
|
+
* Default: undefined (no constant harvest; non-literal route paths of this
|
|
297
|
+
* language floor to skip).
|
|
298
|
+
*/
|
|
299
|
+
readonly extractModuleConstants?: (tree: Parser.Tree) => ModuleConstants;
|
|
300
|
+
/**
|
|
301
|
+
* Cheap content heuristic deciding whether the worker should run
|
|
302
|
+
* {@link extractModuleConstants} on a file. Guards the harvest cost on huge
|
|
303
|
+
* repos: files that cannot contribute (no constant-bearing syntax) are not
|
|
304
|
+
* walked. Must be syntax-driven (field/import shape), not identifier
|
|
305
|
+
* pattern-matching on class names.
|
|
306
|
+
*
|
|
307
|
+
* Default: undefined — harvest EVERY file of this language. A gate is opt-in
|
|
308
|
+
* because getting it wrong silently drops routes that already resolve, and a
|
|
309
|
+
* missed gate only costs time. Declare one only where the cost bites (Java's
|
|
310
|
+
* Maven monorepos) and only after checking it against every shape
|
|
311
|
+
* {@link extractModuleConstants} accepts.
|
|
312
|
+
*/
|
|
313
|
+
readonly moduleConstantHeuristic?: (content: string) => boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Fold one file's non-literal route-path operand list
|
|
316
|
+
* (`routePathExpr`/`routePathOperands` of an `ExtractedDecoratorRoute`)
|
|
317
|
+
* against the repo-wide, file-path-keyed constant map, or null when it cannot
|
|
318
|
+
* be fully folded (skip floor — never a phantom path). Languages whose
|
|
319
|
+
* qualified refs resolve through class imports (`Outer.CONST`,
|
|
320
|
+
* `com.example.ApiPaths.USERS`) need this hook because the shared fold has no
|
|
321
|
+
* notion of qualified names; Python's bare-name refs use the shared default.
|
|
322
|
+
*
|
|
323
|
+
* Default: undefined (the parse phase falls back to the shared
|
|
324
|
+
* language-agnostic operand fold).
|
|
325
|
+
*/
|
|
326
|
+
readonly foldRoutePathOperands?: (filePath: string, operands: readonly Operand[], repo: RepoConstants) => string | null;
|
|
269
327
|
/** Built-in/stdlib names that should be filtered from the call graph for this language.
|
|
270
328
|
* Default: undefined (no language-specific filtering). */
|
|
271
329
|
readonly builtInNames?: ReadonlySet<string>;
|
|
@@ -8,6 +8,27 @@
|
|
|
8
8
|
* The providers table in `languages/index.ts` uses `satisfies Record<SupportedLanguages, LanguageProvider>`
|
|
9
9
|
* so adding a language to the enum without creating a provider is a compiler error.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* Everything a language needs to provide.
|
|
13
|
+
* Required fields must be explicitly set; optional fields have defaults
|
|
14
|
+
* applied by defineLanguage().
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Should the parse worker run {@link LanguageProviderConfig.extractModuleConstants}
|
|
18
|
+
* on this file?
|
|
19
|
+
*
|
|
20
|
+
* Exported so the DECISION is testable without booting a worker. It encodes the
|
|
21
|
+
* one rule that is easy to get backwards: a provider that declares no
|
|
22
|
+
* `moduleConstantHeuristic` harvests unconditionally. Writing the gate as
|
|
23
|
+
* `provider.moduleConstantHeuristic?.(content)` reads `undefined` as "skip" and
|
|
24
|
+
* silently disables the hook for every provider without a heuristic — which is
|
|
25
|
+
* exactly how Python's already-shipped harvest was turned off (#2391/#2980).
|
|
26
|
+
*/
|
|
27
|
+
export function shouldHarvestModuleConstants(provider, content) {
|
|
28
|
+
if (!provider.extractModuleConstants)
|
|
29
|
+
return false;
|
|
30
|
+
return !provider.moduleConstantHeuristic || provider.moduleConstantHeuristic(content);
|
|
31
|
+
}
|
|
11
32
|
const DEFAULTS = {
|
|
12
33
|
mroStrategy: 'first-wins',
|
|
13
34
|
};
|
|
@@ -13,6 +13,7 @@ import { defineLanguage } from '../language-provider.js';
|
|
|
13
13
|
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
14
14
|
import { javaTypeConfig } from '../type-extractors/jvm.js';
|
|
15
15
|
import { extractSpringRoutes, extractSpringTypes } from '../route-extractors/spring.js';
|
|
16
|
+
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, } from '../route-extractors/java-const-resolver.js';
|
|
16
17
|
import { javaExportChecker } from '../export-detection.js';
|
|
17
18
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
18
19
|
import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
|
|
@@ -186,4 +187,24 @@ export const javaProvider = defineLanguage({
|
|
|
186
187
|
// ── Route extraction ──
|
|
187
188
|
extractDecoratorRoutes: extractSpringRoutes,
|
|
188
189
|
extractRouteInheritanceTypes: extractSpringTypes,
|
|
190
|
+
// ── #2980: constant harvest + qualified-ref fold for non-literal mapping
|
|
191
|
+
// paths (`@PostMapping(ApiPaths.SAVE_V1)`) — kept behind provider hooks so
|
|
192
|
+
// the shared ingestion layers stay language-agnostic. The heuristic is
|
|
193
|
+
// SYNTAX-driven (field/import shape), never a class-name pattern: constant
|
|
194
|
+
// classes are routinely named `ApiPaths`/`Routes`/`Paths`, which a
|
|
195
|
+
// `*Constants`-style gate would silently drop (review round-2 High finding).
|
|
196
|
+
extractModuleConstants: extractJavaModuleConstants,
|
|
197
|
+
// One gate, shared with the group side's `prepareRepo` pre-pass so the two
|
|
198
|
+
// subsystems cannot disagree about which files define constants (see
|
|
199
|
+
// JAVA_CONSTANT_FILE_RE — the previous divergence dropped constant
|
|
200
|
+
// INTERFACES on this side only, which cost the graph its Route nodes while
|
|
201
|
+
// the group still published the contract).
|
|
202
|
+
moduleConstantHeuristic: (content) => isJavaConstantFile(content) ||
|
|
203
|
+
// `import com.winning.opt.common.ApiPaths;` — ANY class import can bind a
|
|
204
|
+
// constant ref (`ApiPaths.X` at an annotation site), so gate on the
|
|
205
|
+
// general import shape, not on the imported name. Ingestion-only: this
|
|
206
|
+
// side needs the importing controller's own import table, which the group
|
|
207
|
+
// side instead derives lazily from the tree it already holds.
|
|
208
|
+
/\bimport\s+(?:static\s+)?[\w.]+\s*;/.test(content),
|
|
209
|
+
foldRoutePathOperands: foldJavaOperands,
|
|
189
210
|
});
|
|
@@ -28,6 +28,7 @@ import { createPythonCfgVisitor } from '../cfg/visitors/python.js';
|
|
|
28
28
|
import { emitPythonScopeCaptures, pythonFunctionDefinitionLabel, interpretPythonImport, interpretPythonTypeBinding, pythonArityCompatibility, pythonBindingScopeFor, pythonImportOwningScope, pythonMergeBindings, pythonReceiverBinding, resolvePythonImportTarget, } from './python/index.js';
|
|
29
29
|
import { extractDjangoRoutes } from '../route-extractors/django.js';
|
|
30
30
|
import { discoverDjangoRootUrls } from '../route-extractors/django-root-discovery.js';
|
|
31
|
+
import { extractPythonModuleConstants } from '../route-extractors/python-const-resolver.js';
|
|
31
32
|
const BUILT_INS = new Set([
|
|
32
33
|
'print',
|
|
33
34
|
'len',
|
|
@@ -134,4 +135,16 @@ export const pythonProvider = defineLanguage({
|
|
|
134
135
|
receiverBinding: pythonReceiverBinding,
|
|
135
136
|
arityCompatibility: pythonArityCompatibility,
|
|
136
137
|
resolveImportTarget: resolvePythonImportTarget,
|
|
138
|
+
// ── #2391 constant harvest, provider-hook form (#2980): module-level string
|
|
139
|
+
// constants + from-imports for non-literal decorator route paths. Bare-name
|
|
140
|
+
// refs fold through the shared resolver (no foldRoutePathOperands needed).
|
|
141
|
+
// No `moduleConstantHeuristic`: Python harvests unconditionally, exactly as
|
|
142
|
+
// #2391 shipped it. A content gate was tried here and removed on review — it
|
|
143
|
+
// required `NAME` immediately followed by `=`, so it silently dropped the two
|
|
144
|
+
// idiomatic typed-FastAPI shapes (`API: str = "/api"`,
|
|
145
|
+
// `API: Final[str] = "/api"`) and every composed constant whose RHS starts
|
|
146
|
+
// with an identifier (`USERS = BASE + "/users"`), i.e. it REGRESSED routes
|
|
147
|
+
// that already resolve on main. The worker treats a missing heuristic as
|
|
148
|
+
// default-open; only Java opts into a gate, where the cost actually bites.
|
|
149
|
+
extractModuleConstants: extractPythonModuleConstants,
|
|
137
150
|
});
|
|
@@ -25,7 +25,7 @@ import { getLanguageFromFilename, } from '../../../_shared/index.js';
|
|
|
25
25
|
import { readFileContents } from '../filesystem-walker.js';
|
|
26
26
|
import { isLanguageAvailable, isGrammarRuntimeSkipped, createParserForLanguage, } from '../../tree-sitter/parser-loader.js';
|
|
27
27
|
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
28
|
-
import { getProvider, providers } from '../languages/index.js';
|
|
28
|
+
import { getProvider, getProviderForFile, providers } from '../languages/index.js';
|
|
29
29
|
import { SCOPE_RESOLVERS } from '../scope-resolution/pipeline/registry.js';
|
|
30
30
|
import { DATA_ROUTE_TABLE_SOURCE } from '../route-extractors/data-route-table.js';
|
|
31
31
|
import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
|
|
@@ -1058,8 +1058,15 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1058
1058
|
resolvedRoutes.push(dr);
|
|
1059
1059
|
continue;
|
|
1060
1060
|
}
|
|
1061
|
+
// Provider-driven fold (#2980): languages with qualified-ref semantics
|
|
1062
|
+
// (Java `ApiPaths.X` / `com.example.ApiPaths.X`) fold through their
|
|
1063
|
+
// provider hook; everything else uses the shared language-agnostic
|
|
1064
|
+
// operand fold. No language names in the shared layer.
|
|
1065
|
+
const fold = getProviderForFile(dr.filePath)?.foldRoutePathOperands;
|
|
1061
1066
|
const value = dr.routePathOperands
|
|
1062
|
-
?
|
|
1067
|
+
? fold
|
|
1068
|
+
? fold(dr.filePath, dr.routePathOperands, repoConstants)
|
|
1069
|
+
: resolveOperands(dr.filePath, dr.routePathOperands, repoConstants)
|
|
1063
1070
|
: null;
|
|
1064
1071
|
if (value === null) {
|
|
1065
1072
|
skipped++;
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
* (intra-procedural, function-local, def→use reachability — not module-level
|
|
24
24
|
* cross-file value folding).
|
|
25
25
|
*/
|
|
26
|
+
/** Max length of a folded path. Real route paths are well under this; a fold that
|
|
27
|
+
* exceeds it is a pathological self-multiplying concat (`X = A + A; A = B + B; …`)
|
|
28
|
+
* whose true value is genuinely huge — building it risks a `RangeError`/heap OOM,
|
|
29
|
+
* so we floor to `null` (skip) instead (#2393). The depth cap bounds recursion but
|
|
30
|
+
* NOT output size, which grows multiplicatively; this bounds the output. */
|
|
31
|
+
export declare const MAX_FOLD_LENGTH = 8192;
|
|
26
32
|
/**
|
|
27
33
|
* One term of a constant's right-hand side. A `+`-concatenation
|
|
28
34
|
* (`A + "/b" + C`) becomes an ordered `Operand[]`; a bare literal is a
|
|
@@ -31,7 +31,7 @@ const MAX_RESOLVE_DEPTH = 8;
|
|
|
31
31
|
* whose true value is genuinely huge — building it risks a `RangeError`/heap OOM,
|
|
32
32
|
* so we floor to `null` (skip) instead (#2393). The depth cap bounds recursion but
|
|
33
33
|
* NOT output size, which grows multiplicatively; this bounds the output. */
|
|
34
|
-
const MAX_FOLD_LENGTH = 8192;
|
|
34
|
+
export const MAX_FOLD_LENGTH = 8192;
|
|
35
35
|
/**
|
|
36
36
|
* Fold an operand list to its concatenated literal, or `null` if any operand is
|
|
37
37
|
* unresolvable (an unknown name, a non-string term, a cycle, or a depth overrun).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Java binding for the language-agnostic constant resolver (#2391 core).
|
|
3
|
+
*
|
|
4
|
+
* Supplies the two Java-specific pieces the shared fold in
|
|
5
|
+
* `constant-resolver.ts` needs — {@link resolveJavaImport} (import-specifier →
|
|
6
|
+
* file, honoring JVM package/classpath rules) and
|
|
7
|
+
* {@link extractJavaModuleConstants} (tree → {@link ModuleConstants}) — plus a
|
|
8
|
+
* pre-bound {@link resolveJavaConstant} wrapper so callers stay
|
|
9
|
+
* language-oblivious. The reusable fold, the cycle guard, and the depth cap
|
|
10
|
+
* all live in the agnostic core.
|
|
11
|
+
*
|
|
12
|
+
* Java constant shape (one per type declaration; nested classes flatten into
|
|
13
|
+
* the same file-level namespace, mirroring how `Outer.CONST` and a top-level
|
|
14
|
+
* `CONST` are indistinguishable at the fold layer):
|
|
15
|
+
*
|
|
16
|
+
* public class ApiPathConstants {
|
|
17
|
+
* public static final String DIAGNOSIS_SAVE_V1 = "/api/v1/diagnosis/add";
|
|
18
|
+
* public static final String API_CIS_SAVE_SUMMARY = API_CIS_V1 + "summary/save";
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* Reference shapes at annotation sites this binding resolves:
|
|
22
|
+
* @PostMapping(ApiPathConstants.DIAGNOSIS_SAVE_V1) // qualified
|
|
23
|
+
* @PostMapping(com.winning.opt.X.ApiPathConstants.Y) // FQN-qualified
|
|
24
|
+
* @PostMapping(DIAGNOSIS_SAVE_V1) // static-imported
|
|
25
|
+
* @PostMapping(API_CIS_V1 + "summary/save") // inline concat
|
|
26
|
+
*
|
|
27
|
+
* Which ANNOTATIONS count as routes is a separate question this module has no
|
|
28
|
+
* say in: `spring-shared.ts` holds an exact-name map, so a vendor alias like
|
|
29
|
+
* `@WinPostMapping` yields no route on this base regardless of how its value
|
|
30
|
+
* folds (#2883). Folding and alias recognition compose; neither implies the
|
|
31
|
+
* other.
|
|
32
|
+
*
|
|
33
|
+
* Import shapes consumed:
|
|
34
|
+
* import com.winning.opt.diagnosis.api.constants.ApiPathConstants;
|
|
35
|
+
* import static com.winning.opt.diagnosis.api.constants.ApiPathConstants.API_CIS_V1;
|
|
36
|
+
*
|
|
37
|
+
* Keying (KTD4 parity with the Python binding): the repo map is keyed by
|
|
38
|
+
* unique POSIX file path. A Java import `com.a.b.CONSTS` resolves to the file
|
|
39
|
+
* whose path ends with `com/a/b/CONSTS.java`; when 2+ files share that suffix
|
|
40
|
+
* the import is ambiguous and returns null (skip floor), never a wrong path.
|
|
41
|
+
*/
|
|
42
|
+
import type Parser from 'tree-sitter';
|
|
43
|
+
import { type ImportResolver, type ModuleConstants, type Operand, type RepoConstants } from './constant-resolver.js';
|
|
44
|
+
export type { ImportBinding, ModuleConstants, Operand, RepoConstants, } from './constant-resolver.js';
|
|
45
|
+
export declare function isJavaConstantFile(source: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The Java {@link ImportResolver}: map a fully-qualified import specifier to
|
|
48
|
+
* the unique file key it refers to, or null when it cannot be pinned to
|
|
49
|
+
* exactly one file.
|
|
50
|
+
*
|
|
51
|
+
* `com.winning.opt.X.ApiPathConstants` → the file key ending in
|
|
52
|
+
* `com/winning/opt/X/ApiPathConstants.java`. Because the repo map is
|
|
53
|
+
* file-path-keyed and Maven multi-module trees repeat package roots across
|
|
54
|
+
* modules (`winning-opt-a/.../api/constants/ApiPathConstants.java` and
|
|
55
|
+
* `winning-opt-b/.../api/constants/ApiPathConstants.java`), suffix matching
|
|
56
|
+
* stays UNIQUE-suffix: an import whose full package+class path matches N files
|
|
57
|
+
* in N different modules cannot be pinned, so it returns null — the skip floor
|
|
58
|
+
* this module promises, never a wrong path.
|
|
59
|
+
*
|
|
60
|
+
* A nearest-shared-directory tie-break was tried here and removed on review:
|
|
61
|
+
* javac resolves duplicate FQNs by CLASSPATH ORDER, not directory proximity, so
|
|
62
|
+
* a `src/test` fixture copy or a module that merely sits closer in the tree can
|
|
63
|
+
* outrank the real dependency and yield a silently wrong literal. In a resolver
|
|
64
|
+
* whose whole contract is skip-or-correct, a plausible guess is the one answer
|
|
65
|
+
* that cannot be allowed.
|
|
66
|
+
*/
|
|
67
|
+
export declare const resolveJavaImport: ImportResolver;
|
|
68
|
+
/**
|
|
69
|
+
* Parse a Java constant initializer into an operand list, or null when it is
|
|
70
|
+
* not a foldable string expression. Handles a bare string literal, a bare
|
|
71
|
+
* identifier (`X = Y`), qualified/static-import-free references
|
|
72
|
+
* (`X = CONSTS.Y` — recorded as ONE ref named `CONSTS.Y`), and
|
|
73
|
+
* left-associative `+` chains of the three. Everything else — numbers, calls,
|
|
74
|
+
* ternaries, method refs, `String.format`, enum constants — returns null,
|
|
75
|
+
* which makes the constant unresolvable (→ skip floor), never a wrong value.
|
|
76
|
+
*/
|
|
77
|
+
export declare function parseJavaConstOperands(node: Parser.SyntaxNode | null | undefined, depth?: number): Operand[] | null;
|
|
78
|
+
/**
|
|
79
|
+
* Extract the file-level string constants and import bindings of one parsed
|
|
80
|
+
* Java file into the {@link ModuleConstants} shape the resolver consumes.
|
|
81
|
+
*
|
|
82
|
+
* Constants: every `static final String NAME = …` field of every type
|
|
83
|
+
* declaration in the file (nested classes included — their simple names
|
|
84
|
+
* would collide at the fold layer, but qualified refs carry the class name
|
|
85
|
+
* so nesting only matters for same-name fields, which flatten last-wins).
|
|
86
|
+
* Interface constants (`String NAME = "…"`) are implicitly static final and
|
|
87
|
+
* are collected too.
|
|
88
|
+
*
|
|
89
|
+
* References to OTHER constants via qualified names (`ApiPathConstants.X`)
|
|
90
|
+
* are stored as refs named `ApiPathConstants.X`; at the fold layer such a ref
|
|
91
|
+
* resolves through the import map (`ApiPathConstants` → module) followed by
|
|
92
|
+
* field lookup in the target file's OWN class-name-qualified namespace. To
|
|
93
|
+
* support that, constant names are ALSO recorded under
|
|
94
|
+
* `<DeclaringClass>.<FIELD>` (both spellings share one entry).
|
|
95
|
+
*
|
|
96
|
+
* Last-wins in source order; a non-foldable rebind (`X = compute()`) drops X
|
|
97
|
+
* to unresolvable rather than keeping a stale literal.
|
|
98
|
+
*/
|
|
99
|
+
export declare function extractJavaModuleConstants(tree: Parser.Tree): ModuleConstants;
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a single Java constant referenced in `fileKey` to its literal string
|
|
102
|
+
* value, folding `+` concatenation and following import chains via
|
|
103
|
+
* {@link resolveJavaImport}, or null when it cannot be fully folded.
|
|
104
|
+
*
|
|
105
|
+
* `name` may be simple (`DIAGNOSIS_SAVE_V1`, resolved via static import or
|
|
106
|
+
* same-file constant) or qualified (`ApiPathConstants.DIAGNOSIS_SAVE_V1`,
|
|
107
|
+
* resolved via the class import + the target file's qualified alias).
|
|
108
|
+
*/
|
|
109
|
+
export declare function resolveJavaConstant(fileKey: string, name: string, repo: RepoConstants, depth?: number): string | null;
|
|
110
|
+
/**
|
|
111
|
+
* Fold an inline operand list (e.g. `API_CIS_V1 + "summary/save"`) against
|
|
112
|
+
* `fileKey`, or null when any piece is unresolvable (skip floor).
|
|
113
|
+
*/
|
|
114
|
+
export declare function foldJavaOperands(fileKey: string, operands: readonly Operand[], repo: RepoConstants): string | null;
|