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.
@@ -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
- if (!annNode || !node || !valueNode)
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
- scan(tree) {
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)