gitnexus 1.6.9-rc.2 → 1.6.9-rc.4
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 +223 -195
- package/dist/core/group/extractors/http-patterns/kotlin.js +575 -81
- package/dist/core/group/extractors/http-patterns/python.js +221 -1
- package/dist/core/group/extractors/http-patterns/spring-consumer-shared.d.ts +137 -0
- package/dist/core/group/extractors/http-patterns/spring-consumer-shared.js +230 -0
- package/dist/core/group/matching.js +23 -7
- package/dist/core/ingestion/language-provider.d.ts +23 -2
- package/dist/core/ingestion/languages/python.js +9 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +19 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +125 -1
- package/dist/core/ingestion/pipeline-phases/routes.js +5 -0
- package/dist/core/ingestion/route-extractors/django-root-discovery.d.ts +25 -0
- package/dist/core/ingestion/route-extractors/django-root-discovery.js +217 -0
- package/dist/core/ingestion/route-extractors/django.d.ts +4 -0
- package/dist/core/ingestion/route-extractors/django.js +425 -0
- package/package.json +1 -1
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
import Java from 'tree-sitter-java';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
3
|
import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
|
+
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix, joinPath, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
5
|
+
/**
|
|
6
|
+
* Java HTTP plugin. Handles:
|
|
7
|
+
* - Spring `@RequestMapping` class prefixes + `@(Get|Post|...)Mapping` method annotations
|
|
8
|
+
* - Spring `RestTemplate.getForObject/...`, `exchange(...)`
|
|
9
|
+
* - Spring `WebClient.method(HttpMethod.X, ...)`, `WebClient.get().uri(...)`
|
|
10
|
+
* - OkHttp `new Request.Builder().url("...")`
|
|
11
|
+
* - OpenFeign interfaces with Spring MVC method annotations or
|
|
12
|
+
* native `@RequestLine("METHOD /path")` annotations
|
|
13
|
+
* - Java / Apache HttpClient literal request construction
|
|
14
|
+
*
|
|
15
|
+
* Every route-defining annotation (class/interface `@RequestMapping`
|
|
16
|
+
* prefixes, `@FeignClient(path)` prefixes, `@(Get|...)Mapping` method
|
|
17
|
+
* routes and native `@RequestLine`s) is matched by a single consolidated
|
|
18
|
+
* query (`JAVA_ROUTE_ANNOTATION_PATTERNS`) in one pass via
|
|
19
|
+
* `scanRouteAnnotations`. The `scan` function then walks up from each
|
|
20
|
+
* matched method to its enclosing class/interface to combine the prefix
|
|
21
|
+
* with the method path. Call-site consumers (RestTemplate, WebClient,
|
|
22
|
+
* OkHttp, Java/Apache HttpClient) keep their own focused queries.
|
|
23
|
+
*/
|
|
24
|
+
// Each route-defining annotation has two AST shapes — a positional argument
|
|
25
|
+
// and a named one — that must both be matched:
|
|
26
|
+
// @RequestMapping("/api") → (annotation_argument_list (string_literal))
|
|
27
|
+
// @RequestMapping(path = "/api") → (annotation_argument_list (element_value_pair key:(identifier) value:(string_literal)))
|
|
28
|
+
// @RequestMapping(value = "/api") → same as above
|
|
29
|
+
// For named arguments only the route member keys (`path`/`value`) carry a URL;
|
|
30
|
+
// non-route attributes (`produces`, `consumes`, `headers`, `name`, `params`)
|
|
31
|
+
// would otherwise be mis-extracted (e.g. `produces = "application/json"` would
|
|
32
|
+
// corrupt every route). That key filtering is done in `isRouteMemberKey`, and
|
|
33
|
+
// all of these annotations are matched by the one `JAVA_ROUTE_ANNOTATION_PATTERNS`
|
|
34
|
+
// query below (see its header for why the filtering lives in JS, not the query).
|
|
35
|
+
// The Spring class/interface view (`SharedSpringType`) and the interface-based
|
|
36
|
+
// controller inheritance algorithm (`scanSpringInheritanceProject`) are shared
|
|
37
|
+
// with kotlin.ts via spring-consumer-shared.ts so both plugins emit identical
|
|
38
|
+
// provider contracts. `collectSpringTypes` below produces that shared shape.
|
|
4
39
|
// ─── Route-defining annotations (one generic query, one pass) ─────────
|
|
5
40
|
// Every Java route-mapper annotation shares one shape: an annotation carrying a
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// `
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// new
|
|
41
|
+
// string argument — positional `"..."` or named `key = "..."`, each also in its
|
|
42
|
+
// array form `{"..."}` / `key = {"..."}` (Spring's `path`/`value` are
|
|
43
|
+
// `String[]`) — on a class, interface, or method. This SINGLE query matches that
|
|
44
|
+
// shape generically; the `@value` capture is an alternation over a bare
|
|
45
|
+
// `string_literal` and one nested in an `element_value_array_initializer`, so a
|
|
46
|
+
// single-element array yields the same `@value` (a multi-element array yields one
|
|
47
|
+
// match per element). `scanRouteAnnotations` then reads the annotation NAME
|
|
48
|
+
// (`@ann`) and declaration kind (`@node.type`) in its for-loop to decide what
|
|
49
|
+
// each match means. Adding a new framework annotation that follows this shape is
|
|
50
|
+
// a change to that loop (and the lookup maps), not to this query.
|
|
15
51
|
//
|
|
16
52
|
// Captures (shared across all branches; intentionally framework-agnostic):
|
|
17
53
|
// @ann → the annotation name identifier (RequestMapping, GetMapping, RequestLine, …)
|
|
@@ -27,6 +63,18 @@ import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, } from
|
|
|
27
63
|
// silently dropping sibling-branch matches. Keeping the query predicate-free
|
|
28
64
|
// sidesteps that hazard entirely; all name/key discrimination lives in the
|
|
29
65
|
// for-loop, where it reads as straight-line code.
|
|
66
|
+
//
|
|
67
|
+
// KNOWN LIMITATION — fully-qualified route annotations are not matched. `@ann`
|
|
68
|
+
// binds `name: (identifier)`, but a FQN annotation (`@org.springframework…
|
|
69
|
+
// GetMapping("/x")`) parses its name as a `scoped_identifier`, which this query
|
|
70
|
+
// does not match, so its route is not extracted. (The class is still recognized
|
|
71
|
+
// as a controller — `hasAnnotation` trailing-segment-matches the FQN — only the
|
|
72
|
+
// route-string extraction is missed.) In practice annotations are imported and
|
|
73
|
+
// written by simple name, so this is rare. It is a minor asymmetry with the
|
|
74
|
+
// Kotlin plugin, whose grammar models a FQN as separate `type_identifier`
|
|
75
|
+
// segments that its route queries DO match. Aligning Java would mean matching
|
|
76
|
+
// `scoped_identifier` too; that is deferred to avoid re-keying existing Java
|
|
77
|
+
// contracts via the predicate hazard above. Pinned by an anti-overreach test.
|
|
30
78
|
const JAVA_ROUTE_ANNOTATION_PATTERNS = compilePatterns({
|
|
31
79
|
name: 'java-route-annotation',
|
|
32
80
|
language: Java,
|
|
@@ -39,12 +87,12 @@ const JAVA_ROUTE_ANNOTATION_PATTERNS = compilePatterns({
|
|
|
39
87
|
(modifiers
|
|
40
88
|
(annotation
|
|
41
89
|
name: (identifier) @ann
|
|
42
|
-
arguments: (annotation_argument_list (string_literal) @value)))) @node
|
|
90
|
+
arguments: (annotation_argument_list [(string_literal) @value (element_value_array_initializer (string_literal) @value)])))) @node
|
|
43
91
|
(interface_declaration
|
|
44
92
|
(modifiers
|
|
45
93
|
(annotation
|
|
46
94
|
name: (identifier) @ann
|
|
47
|
-
arguments: (annotation_argument_list (string_literal) @value)))) @node
|
|
95
|
+
arguments: (annotation_argument_list [(string_literal) @value (element_value_array_initializer (string_literal) @value)])))) @node
|
|
48
96
|
(class_declaration
|
|
49
97
|
(modifiers
|
|
50
98
|
(annotation
|
|
@@ -52,7 +100,7 @@ const JAVA_ROUTE_ANNOTATION_PATTERNS = compilePatterns({
|
|
|
52
100
|
arguments: (annotation_argument_list
|
|
53
101
|
(element_value_pair
|
|
54
102
|
key: (identifier) @key
|
|
55
|
-
value: (string_literal) @value))))) @node
|
|
103
|
+
value: [(string_literal) @value (element_value_array_initializer (string_literal) @value)]))))) @node
|
|
56
104
|
(interface_declaration
|
|
57
105
|
(modifiers
|
|
58
106
|
(annotation
|
|
@@ -60,12 +108,12 @@ const JAVA_ROUTE_ANNOTATION_PATTERNS = compilePatterns({
|
|
|
60
108
|
arguments: (annotation_argument_list
|
|
61
109
|
(element_value_pair
|
|
62
110
|
key: (identifier) @key
|
|
63
|
-
value: (string_literal) @value))))) @node
|
|
111
|
+
value: [(string_literal) @value (element_value_array_initializer (string_literal) @value)]))))) @node
|
|
64
112
|
(method_declaration
|
|
65
113
|
(modifiers
|
|
66
114
|
(annotation
|
|
67
115
|
name: (identifier) @ann
|
|
68
|
-
arguments: (annotation_argument_list (string_literal) @value)))
|
|
116
|
+
arguments: (annotation_argument_list [(string_literal) @value (element_value_array_initializer (string_literal) @value)])))
|
|
69
117
|
name: (identifier) @member) @node
|
|
70
118
|
(method_declaration
|
|
71
119
|
(modifiers
|
|
@@ -74,7 +122,7 @@ const JAVA_ROUTE_ANNOTATION_PATTERNS = compilePatterns({
|
|
|
74
122
|
arguments: (annotation_argument_list
|
|
75
123
|
(element_value_pair
|
|
76
124
|
key: (identifier) @key
|
|
77
|
-
value: (string_literal) @value))))
|
|
125
|
+
value: [(string_literal) @value (element_value_array_initializer (string_literal) @value)]))))
|
|
78
126
|
name: (identifier) @member) @node
|
|
79
127
|
]
|
|
80
128
|
`,
|
|
@@ -96,60 +144,6 @@ const SPRING_TYPE_DECLARATION_PATTERNS = compilePatterns({
|
|
|
96
144
|
},
|
|
97
145
|
],
|
|
98
146
|
});
|
|
99
|
-
// ─── Consumer: OpenFeign `@RequestLine("METHOD /path")` parsing ───────
|
|
100
|
-
// OpenFeign's native annotation pairs an HTTP method and path in a single
|
|
101
|
-
// string literal — see https://github.com/OpenFeign/feign#interface-annotations.
|
|
102
|
-
// It is method-level only and is mutually exclusive with Spring MVC
|
|
103
|
-
// `@GetMapping` / `@PostMapping` etc. on the same method (mixing them
|
|
104
|
-
// requires a different Feign Contract — they are not combined). The match
|
|
105
|
-
// itself comes from `JAVA_ROUTE_ANNOTATION_PATTERNS`; this regex splits the
|
|
106
|
-
// verb from the path of the captured literal.
|
|
107
|
-
//
|
|
108
|
-
// Examples:
|
|
109
|
-
// @RequestLine("GET /users/{id}")
|
|
110
|
-
// @RequestLine("POST /users?status=active")
|
|
111
|
-
const REQUEST_LINE_VERB_RE = /^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\S.*?)\s*$/i;
|
|
112
|
-
/**
|
|
113
|
-
* Parse a Feign `@RequestLine` value into a method + path pair.
|
|
114
|
-
*
|
|
115
|
-
* `@RequestLine("METHOD /path[?query]")` packs both fields in one string;
|
|
116
|
-
* the query portion is dropped because contract IDs are method+path only
|
|
117
|
-
* (consistent with how other consumers like RestTemplate/WebClient drop
|
|
118
|
-
* query strings when their values are inline literals).
|
|
119
|
-
*
|
|
120
|
-
* Returns null if the value is not a recognized HTTP verb followed by a
|
|
121
|
-
* path beginning with `/`.
|
|
122
|
-
*/
|
|
123
|
-
function parseRequestLine(raw) {
|
|
124
|
-
const match = REQUEST_LINE_VERB_RE.exec(raw);
|
|
125
|
-
if (!match)
|
|
126
|
-
return null;
|
|
127
|
-
const [, verb, rest] = match;
|
|
128
|
-
if (typeof verb !== 'string' || typeof rest !== 'string')
|
|
129
|
-
return null;
|
|
130
|
-
const queryIdx = rest.indexOf('?');
|
|
131
|
-
const pathOnly = (queryIdx >= 0 ? rest.slice(0, queryIdx) : rest).trim();
|
|
132
|
-
if (!pathOnly.startsWith('/'))
|
|
133
|
-
return null;
|
|
134
|
-
return { method: verb.toUpperCase(), path: pathOnly };
|
|
135
|
-
}
|
|
136
|
-
// ─── Consumer: Spring RestTemplate (object-named + method-named) ──────
|
|
137
|
-
// RestTemplate.getForObject / getForEntity → GET
|
|
138
|
-
// RestTemplate.postForObject / postForEntity → POST
|
|
139
|
-
// RestTemplate.put → PUT
|
|
140
|
-
// RestTemplate.delete → DELETE
|
|
141
|
-
// RestTemplate.patchForObject → PATCH
|
|
142
|
-
// Source-scan only: receiver must be named exactly `restTemplate`.
|
|
143
|
-
// Fields, `this.restTemplate`, aliases, and other injection names are deferred.
|
|
144
|
-
const REST_TEMPLATE_TO_HTTP = {
|
|
145
|
-
getForObject: 'GET',
|
|
146
|
-
getForEntity: 'GET',
|
|
147
|
-
postForObject: 'POST',
|
|
148
|
-
postForEntity: 'POST',
|
|
149
|
-
put: 'PUT',
|
|
150
|
-
delete: 'DELETE',
|
|
151
|
-
patchForObject: 'PATCH',
|
|
152
|
-
};
|
|
153
147
|
const REST_TEMPLATE_PATTERNS = compilePatterns({
|
|
154
148
|
name: 'java-rest-template',
|
|
155
149
|
language: Java,
|
|
@@ -184,13 +178,6 @@ const REST_TEMPLATE_EXCHANGE_PATTERNS = compilePatterns({
|
|
|
184
178
|
},
|
|
185
179
|
],
|
|
186
180
|
});
|
|
187
|
-
const WEB_CLIENT_SHORT_TO_HTTP = {
|
|
188
|
-
get: 'GET',
|
|
189
|
-
post: 'POST',
|
|
190
|
-
put: 'PUT',
|
|
191
|
-
delete: 'DELETE',
|
|
192
|
-
patch: 'PATCH',
|
|
193
|
-
};
|
|
194
181
|
const WEB_CLIENT_SHORT_FORM_PATTERNS = compilePatterns({
|
|
195
182
|
name: 'java-web-client-short-form',
|
|
196
183
|
language: Java,
|
|
@@ -209,6 +196,36 @@ const WEB_CLIENT_SHORT_FORM_PATTERNS = compilePatterns({
|
|
|
209
196
|
},
|
|
210
197
|
],
|
|
211
198
|
});
|
|
199
|
+
// ─── Consumer: WebClient long form `webClient.method(HttpMethod.X).uri("/y")` ─
|
|
200
|
+
// The fluent long form carries the verb as a `HttpMethod.X` field access through
|
|
201
|
+
// `.method(...)` and the path on a separate `.uri(...)` hop. A single structural
|
|
202
|
+
// query matches the whole chain (the same field-access shape used by
|
|
203
|
+
// REST_TEMPLATE_EXCHANGE_PATTERNS) — the earlier "intentionally deferred" note
|
|
204
|
+
// predated the Kotlin plugin proving the structural query is enough. Variable-
|
|
205
|
+
// bound verbs (`webClient.method(verb).uri(...)`) do NOT match: the value carries
|
|
206
|
+
// a bare `identifier`, not a `HttpMethod.X` field access — source-scan can't
|
|
207
|
+
// follow the binding (anti-overreach test pins this, parity with Kotlin).
|
|
208
|
+
const WEB_CLIENT_LONG_FORM_PATTERNS = compilePatterns({
|
|
209
|
+
name: 'java-web-client-long-form',
|
|
210
|
+
language: Java,
|
|
211
|
+
patterns: [
|
|
212
|
+
{
|
|
213
|
+
meta: {},
|
|
214
|
+
query: `
|
|
215
|
+
(method_invocation
|
|
216
|
+
object: (method_invocation
|
|
217
|
+
object: (identifier) @obj (#eq? @obj "webClient")
|
|
218
|
+
name: (identifier) @method_call (#eq? @method_call "method")
|
|
219
|
+
arguments: (argument_list
|
|
220
|
+
(field_access
|
|
221
|
+
object: (identifier) @httpMethodCls (#eq? @httpMethodCls "HttpMethod")
|
|
222
|
+
field: (identifier) @verb)))
|
|
223
|
+
name: (identifier) @uri_method (#eq? @uri_method "uri")
|
|
224
|
+
arguments: (argument_list . (string_literal) @path))
|
|
225
|
+
`,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
});
|
|
212
229
|
// ─── Consumer: OkHttp `new Request.Builder().url("path")` ─────────────
|
|
213
230
|
// Note: `Request.Builder` is a `scoped_type_identifier` whose text includes
|
|
214
231
|
// the dot, so `#eq?` against the literal string matches cleanly (no need
|
|
@@ -287,32 +304,6 @@ function findEnclosingInterface(node) {
|
|
|
287
304
|
}
|
|
288
305
|
return null;
|
|
289
306
|
}
|
|
290
|
-
/**
|
|
291
|
-
* Join a class-level prefix and a method-level path into a single URL
|
|
292
|
-
* path. Mirrors the semantics of the original regex implementation:
|
|
293
|
-
* strip trailing slashes on the prefix, then ensure a single slash
|
|
294
|
-
* between prefix and method path.
|
|
295
|
-
*/
|
|
296
|
-
function joinPath(prefix, methodPath) {
|
|
297
|
-
const cleanPrefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
298
|
-
const cleanSub = methodPath.replace(/^\/+/, '');
|
|
299
|
-
if (!cleanPrefix)
|
|
300
|
-
return `/${cleanSub}`;
|
|
301
|
-
return `/${cleanPrefix}/${cleanSub}`;
|
|
302
|
-
}
|
|
303
|
-
function joinInheritedSpringPath(controllerPrefix, inheritedPath, inheritedOwnerPrefix = '') {
|
|
304
|
-
const joined = joinPath(controllerPrefix, inheritedPath);
|
|
305
|
-
const cleanPrefix = controllerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
306
|
-
const cleanOwnerPrefix = inheritedOwnerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
307
|
-
const cleanInherited = inheritedPath.replace(/^\/+/, '');
|
|
308
|
-
if (!cleanPrefix)
|
|
309
|
-
return joined;
|
|
310
|
-
if (cleanPrefix === cleanOwnerPrefix &&
|
|
311
|
-
(cleanInherited === cleanPrefix || cleanInherited.startsWith(`${cleanPrefix}/`))) {
|
|
312
|
-
return `/${cleanInherited}`;
|
|
313
|
-
}
|
|
314
|
-
return joined;
|
|
315
|
-
}
|
|
316
307
|
function getNodeName(node) {
|
|
317
308
|
return node.childForFieldName('name')?.text ?? null;
|
|
318
309
|
}
|
|
@@ -353,11 +344,15 @@ function scanRouteAnnotations(tree) {
|
|
|
353
344
|
// `@RequestMapping` and `@FeignClient(path)` lands a different value in each.
|
|
354
345
|
const prefixByTypeId = new Map();
|
|
355
346
|
const feignPrefixByInterfaceId = new Map();
|
|
347
|
+
const httpExchangePrefixByTypeId = new Map();
|
|
356
348
|
const methodRoutes = [];
|
|
357
349
|
const requestLines = [];
|
|
350
|
+
const exchangeRoutes = [];
|
|
358
351
|
// Interface `@RequestMapping` prefixes rank below `@FeignClient(path)`;
|
|
359
352
|
// collect them and apply only after the FeignClient pass below.
|
|
360
353
|
const interfaceRequestMappingPrefixes = [];
|
|
354
|
+
// `pushPrefix` (the de-duping accumulator) is shared from
|
|
355
|
+
// spring-consumer-shared.ts so Java and Kotlin build prefix maps identically.
|
|
361
356
|
for (const { captures } of matches) {
|
|
362
357
|
const annNode = captures.ann;
|
|
363
358
|
const node = captures.node;
|
|
@@ -396,6 +391,22 @@ function scanRouteAnnotations(tree) {
|
|
|
396
391
|
});
|
|
397
392
|
}
|
|
398
393
|
}
|
|
394
|
+
else if (EXCHANGE_ANNOTATION_TO_HTTP[ann]) {
|
|
395
|
+
// Spring 6 HTTP Interface `@(Get|...)Exchange` — the path lives in the
|
|
396
|
+
// `url` or `value` attribute (or positionally); other attributes
|
|
397
|
+
// (`accept`, `contentType`, …) are not routes.
|
|
398
|
+
if (keyNode && keyNode.text !== 'url' && keyNode.text !== 'value')
|
|
399
|
+
continue;
|
|
400
|
+
const rawPath = unquoteLiteral(valueNode.text);
|
|
401
|
+
if (rawPath !== null) {
|
|
402
|
+
exchangeRoutes.push({
|
|
403
|
+
methodNode: node,
|
|
404
|
+
methodName: captures.member?.text ?? null,
|
|
405
|
+
httpMethod: EXCHANGE_ANNOTATION_TO_HTTP[ann],
|
|
406
|
+
rawPath,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
399
410
|
continue;
|
|
400
411
|
}
|
|
401
412
|
// Type-level (class or interface): a Spring `@RequestMapping` URL prefix, or
|
|
@@ -405,7 +416,7 @@ function scanRouteAnnotations(tree) {
|
|
|
405
416
|
continue;
|
|
406
417
|
const prefix = unquoteLiteral(valueNode.text);
|
|
407
418
|
if (prefix !== null) {
|
|
408
|
-
prefixByTypeId
|
|
419
|
+
pushPrefix(prefixByTypeId, node.id, prefix);
|
|
409
420
|
if (node.type === 'interface_declaration') {
|
|
410
421
|
interfaceRequestMappingPrefixes.push({ id: node.id, prefix });
|
|
411
422
|
}
|
|
@@ -416,16 +427,33 @@ function scanRouteAnnotations(tree) {
|
|
|
416
427
|
if (!keyNode || keyNode.text !== 'path')
|
|
417
428
|
continue;
|
|
418
429
|
const prefix = unquoteLiteral(valueNode.text);
|
|
419
|
-
if (prefix !== null
|
|
420
|
-
feignPrefixByInterfaceId
|
|
421
|
-
|
|
430
|
+
if (prefix !== null)
|
|
431
|
+
pushPrefix(feignPrefixByInterfaceId, node.id, prefix);
|
|
432
|
+
}
|
|
433
|
+
else if (ann === 'HttpExchange') {
|
|
434
|
+
// Spring HTTP Interface type-level prefix: the path lives in `url`/`value`
|
|
435
|
+
// (or positionally). Applies to its `@(Get|...)Exchange` consumer methods.
|
|
436
|
+
if (keyNode && keyNode.text !== 'url' && keyNode.text !== 'value')
|
|
437
|
+
continue;
|
|
438
|
+
const prefix = unquoteLiteral(valueNode.text);
|
|
439
|
+
if (prefix !== null)
|
|
440
|
+
pushPrefix(httpExchangePrefixByTypeId, node.id, prefix);
|
|
422
441
|
}
|
|
423
442
|
}
|
|
443
|
+
// `@RequestMapping` on a Feign interface is the fallback prefix, but only when
|
|
444
|
+
// the interface has no `@FeignClient(path)` of its own (path wins).
|
|
424
445
|
for (const { id, prefix } of interfaceRequestMappingPrefixes) {
|
|
425
446
|
if (!feignPrefixByInterfaceId.has(id))
|
|
426
|
-
feignPrefixByInterfaceId
|
|
447
|
+
pushPrefix(feignPrefixByInterfaceId, id, prefix);
|
|
427
448
|
}
|
|
428
|
-
return {
|
|
449
|
+
return {
|
|
450
|
+
prefixByTypeId,
|
|
451
|
+
feignPrefixByInterfaceId,
|
|
452
|
+
httpExchangePrefixByTypeId,
|
|
453
|
+
methodRoutes,
|
|
454
|
+
requestLines,
|
|
455
|
+
exchangeRoutes,
|
|
456
|
+
};
|
|
429
457
|
}
|
|
430
458
|
function collectDirectMethods(typeNode) {
|
|
431
459
|
const out = [];
|
|
@@ -486,7 +514,7 @@ function collectSpringTypes(filePath, tree) {
|
|
|
486
514
|
filePath,
|
|
487
515
|
kind,
|
|
488
516
|
name: typeNameNode.text,
|
|
489
|
-
|
|
517
|
+
classPrefixes: prefixByTypeId.get(typeNode.id) ?? [],
|
|
490
518
|
implementedInterfaces: kind === 'class' ? collectImplementedInterfaces(typeNode) : [],
|
|
491
519
|
isController: kind === 'class' && hasAnnotation(typeNode, ['RestController', 'Controller']),
|
|
492
520
|
methods,
|
|
@@ -494,63 +522,11 @@ function collectSpringTypes(filePath, tree) {
|
|
|
494
522
|
}
|
|
495
523
|
return out;
|
|
496
524
|
}
|
|
525
|
+
// The interface-based-controller inheritance algorithm is shared with kotlin.ts
|
|
526
|
+
// (`scanSpringInheritanceProject`); this collects the `SharedSpringType` view and
|
|
527
|
+
// delegates so Java and Kotlin emit byte-identical provider contracts.
|
|
497
528
|
function scanSpringProject(files) {
|
|
498
|
-
|
|
499
|
-
const interfaceRoutes = new Map();
|
|
500
|
-
for (const type of types) {
|
|
501
|
-
if (type.kind !== 'interface')
|
|
502
|
-
continue;
|
|
503
|
-
if (interfaceRoutes.has(type.name)) {
|
|
504
|
-
interfaceRoutes.set(type.name, null);
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
const methodMap = new Map();
|
|
508
|
-
for (const method of type.methods) {
|
|
509
|
-
const routes = method.routes.map((route) => ({
|
|
510
|
-
method: route.method,
|
|
511
|
-
path: type.classPrefix ? joinPath(type.classPrefix, route.path) : route.path,
|
|
512
|
-
ownerPrefix: type.classPrefix,
|
|
513
|
-
}));
|
|
514
|
-
if (routes.length > 0)
|
|
515
|
-
methodMap.set(method.name, routes);
|
|
516
|
-
}
|
|
517
|
-
interfaceRoutes.set(type.name, methodMap);
|
|
518
|
-
}
|
|
519
|
-
const detectionsByFile = new Map();
|
|
520
|
-
for (const type of types) {
|
|
521
|
-
if (type.kind !== 'class' || !type.isController)
|
|
522
|
-
continue;
|
|
523
|
-
for (const method of type.methods) {
|
|
524
|
-
if (method.routes.length > 0)
|
|
525
|
-
continue;
|
|
526
|
-
const inheritedRoutes = type.implementedInterfaces.flatMap((interfaceName) => {
|
|
527
|
-
const routeMap = interfaceRoutes.get(interfaceName);
|
|
528
|
-
if (!routeMap)
|
|
529
|
-
return [];
|
|
530
|
-
const routes = routeMap.get(method.name) ?? [];
|
|
531
|
-
return routes.map((route) => ({
|
|
532
|
-
method: route.method,
|
|
533
|
-
path: joinInheritedSpringPath(type.classPrefix, route.path, route.ownerPrefix),
|
|
534
|
-
}));
|
|
535
|
-
});
|
|
536
|
-
for (const route of inheritedRoutes) {
|
|
537
|
-
const detections = detectionsByFile.get(type.filePath) ?? [];
|
|
538
|
-
detections.push({
|
|
539
|
-
role: 'provider',
|
|
540
|
-
framework: 'spring',
|
|
541
|
-
method: route.method,
|
|
542
|
-
path: route.path,
|
|
543
|
-
name: method.name,
|
|
544
|
-
confidence: 0.8,
|
|
545
|
-
});
|
|
546
|
-
detectionsByFile.set(type.filePath, detections);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return [...detectionsByFile.entries()].map(([filePath, detections]) => ({
|
|
551
|
-
filePath,
|
|
552
|
-
detections,
|
|
553
|
-
}));
|
|
529
|
+
return scanSpringInheritanceProject(files.flatMap((file) => collectSpringTypes(file.filePath, file.tree)));
|
|
554
530
|
}
|
|
555
531
|
export const JAVA_HTTP_PLUGIN = {
|
|
556
532
|
name: 'java-http',
|
|
@@ -561,7 +537,7 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
561
537
|
// `scanRouteAnnotations` resolves every route-defining annotation —
|
|
562
538
|
// class/interface prefixes, method `@(Get|...)Mapping`s and native
|
|
563
539
|
// `@RequestLine`s — from a single `matches()` pass over the tree.
|
|
564
|
-
const { prefixByTypeId, feignPrefixByInterfaceId, methodRoutes, requestLines } = scanRouteAnnotations(tree);
|
|
540
|
+
const { prefixByTypeId, feignPrefixByInterfaceId, httpExchangePrefixByTypeId, methodRoutes, requestLines, exchangeRoutes, } = scanRouteAnnotations(tree);
|
|
565
541
|
// A `@(Get|...)Mapping` inside a `@FeignClient` interface is an OpenFeign
|
|
566
542
|
// *consumer* (it describes a remote call); the same annotation inside a
|
|
567
543
|
// class is a Spring *provider*. A mapping on a non-Feign interface has no
|
|
@@ -570,29 +546,35 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
570
546
|
for (const route of methodRoutes) {
|
|
571
547
|
const enclosingInterface = findEnclosingInterface(route.methodNode);
|
|
572
548
|
if (enclosingInterface && hasAnnotation(enclosingInterface, 'FeignClient')) {
|
|
573
|
-
const
|
|
549
|
+
const prefixes = feignPrefixByInterfaceId.get(enclosingInterface.id) ?? [''];
|
|
550
|
+
for (const prefix of prefixes) {
|
|
551
|
+
out.push({
|
|
552
|
+
role: 'consumer',
|
|
553
|
+
framework: OPENFEIGN_FRAMEWORK,
|
|
554
|
+
method: route.httpMethod,
|
|
555
|
+
path: joinPath(prefix, route.rawPath),
|
|
556
|
+
name: route.methodName,
|
|
557
|
+
confidence: FEIGN_CONFIDENCE,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const enclosingClass = findEnclosingClass(route.methodNode);
|
|
563
|
+
if (!enclosingClass)
|
|
564
|
+
continue;
|
|
565
|
+
// A multi-element class `@RequestMapping({"/a","/b"})` registers the method
|
|
566
|
+
// under each prefix — emit one provider per (prefix × this route).
|
|
567
|
+
const prefixes = prefixByTypeId.get(enclosingClass.id) ?? [''];
|
|
568
|
+
for (const prefix of prefixes) {
|
|
574
569
|
out.push({
|
|
575
|
-
role: '
|
|
576
|
-
framework: '
|
|
570
|
+
role: 'provider',
|
|
571
|
+
framework: 'spring',
|
|
577
572
|
method: route.httpMethod,
|
|
578
573
|
path: joinPath(prefix, route.rawPath),
|
|
579
574
|
name: route.methodName,
|
|
580
|
-
confidence: 0.
|
|
575
|
+
confidence: 0.8,
|
|
581
576
|
});
|
|
582
|
-
continue;
|
|
583
577
|
}
|
|
584
|
-
const enclosingClass = findEnclosingClass(route.methodNode);
|
|
585
|
-
if (!enclosingClass)
|
|
586
|
-
continue;
|
|
587
|
-
const prefix = prefixByTypeId.get(enclosingClass.id) ?? '';
|
|
588
|
-
out.push({
|
|
589
|
-
role: 'provider',
|
|
590
|
-
framework: 'spring',
|
|
591
|
-
method: route.httpMethod,
|
|
592
|
-
path: joinPath(prefix, route.rawPath),
|
|
593
|
-
name: route.methodName,
|
|
594
|
-
confidence: 0.8,
|
|
595
|
-
});
|
|
596
578
|
}
|
|
597
579
|
// Native OpenFeign `@RequestLine("METHOD /path")`. Method-level only and
|
|
598
580
|
// always declared on an interface (Feign builds a proxy from the interface).
|
|
@@ -608,15 +590,36 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
608
590
|
const enclosingInterface = findEnclosingInterface(requestLine.methodNode);
|
|
609
591
|
if (!enclosingInterface)
|
|
610
592
|
continue;
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
593
|
+
const prefixes = feignPrefixByInterfaceId.get(enclosingInterface.id) ?? [''];
|
|
594
|
+
for (const prefix of prefixes) {
|
|
595
|
+
out.push({
|
|
596
|
+
role: 'consumer',
|
|
597
|
+
framework: OPENFEIGN_FRAMEWORK,
|
|
598
|
+
method: requestLine.parsed.method,
|
|
599
|
+
path: joinPath(prefix, requestLine.parsed.path),
|
|
600
|
+
name: requestLine.methodName,
|
|
601
|
+
confidence: REQUEST_LINE_CONFIDENCE,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// ─── Consumers: Spring HTTP Interface @(Get|...)Exchange ────────
|
|
606
|
+
// Declarative client interfaces proxied by `HttpServiceProxyFactory`
|
|
607
|
+
// (over RestClient / WebClient / RestTemplate). Always a consumer — no
|
|
608
|
+
// provider ambiguity — with an optional type-level `@HttpExchange(url)`
|
|
609
|
+
// prefix. The verb comes from the annotation name (`@GetExchange` → GET).
|
|
610
|
+
for (const route of exchangeRoutes) {
|
|
611
|
+
const enclosing = findEnclosingInterface(route.methodNode) ?? findEnclosingClass(route.methodNode);
|
|
612
|
+
const prefixes = enclosing ? (httpExchangePrefixByTypeId.get(enclosing.id) ?? ['']) : [''];
|
|
613
|
+
for (const prefix of prefixes) {
|
|
614
|
+
out.push({
|
|
615
|
+
role: 'consumer',
|
|
616
|
+
framework: HTTP_INTERFACE_FRAMEWORK,
|
|
617
|
+
method: route.httpMethod,
|
|
618
|
+
path: joinPath(prefix, route.rawPath),
|
|
619
|
+
name: route.methodName,
|
|
620
|
+
confidence: EXCHANGE_CONFIDENCE,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
620
623
|
}
|
|
621
624
|
// ─── Consumers: RestTemplate ────────────────────────────────────
|
|
622
625
|
for (const match of runCompiledPatterns(REST_TEMPLATE_PATTERNS, tree)) {
|
|
@@ -657,9 +660,9 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
657
660
|
});
|
|
658
661
|
}
|
|
659
662
|
// ─── Consumers: WebClient.get().uri("path") short form ─────────
|
|
660
|
-
// Source-scan only: receiver must be named exactly `webClient`.
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
+
// Source-scan only: receiver must be named exactly `webClient`. The
|
|
664
|
+
// long-form chain `webClient.method(HttpMethod.X).uri("/x")` is handled
|
|
665
|
+
// separately below by WEB_CLIENT_LONG_FORM_PATTERNS.
|
|
663
666
|
for (const match of runCompiledPatterns(WEB_CLIENT_SHORT_FORM_PATTERNS, tree)) {
|
|
664
667
|
const verbNode = match.captures.verb;
|
|
665
668
|
const pathNode = match.captures.path;
|
|
@@ -680,6 +683,31 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
680
683
|
confidence: 0.7,
|
|
681
684
|
});
|
|
682
685
|
}
|
|
686
|
+
// ─── Consumers: WebClient.method(HttpMethod.X).uri("path") long form ─
|
|
687
|
+
// The verb is captured as the literal `HttpMethod.X` field name; gate it on
|
|
688
|
+
// the shared verb regex (HEAD/OPTIONS/TRACE excluded, matching the short
|
|
689
|
+
// form). The short-form query requires an empty inner argument list, so it
|
|
690
|
+
// cannot also fire on this chain — no double-emit.
|
|
691
|
+
for (const match of runCompiledPatterns(WEB_CLIENT_LONG_FORM_PATTERNS, tree)) {
|
|
692
|
+
const verbNode = match.captures.verb;
|
|
693
|
+
const pathNode = match.captures.path;
|
|
694
|
+
if (!verbNode || !pathNode)
|
|
695
|
+
continue;
|
|
696
|
+
const verbText = verbNode.text;
|
|
697
|
+
if (!WEB_CLIENT_LONG_VERB_RE.test(verbText))
|
|
698
|
+
continue;
|
|
699
|
+
const path = unquoteLiteral(pathNode.text);
|
|
700
|
+
if (path === null)
|
|
701
|
+
continue;
|
|
702
|
+
out.push({
|
|
703
|
+
role: 'consumer',
|
|
704
|
+
framework: 'spring-web-client',
|
|
705
|
+
method: verbText,
|
|
706
|
+
path,
|
|
707
|
+
name: null,
|
|
708
|
+
confidence: 0.7,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
683
711
|
// ─── Consumers: OkHttp Request.Builder().url("path") ────────────
|
|
684
712
|
for (const match of runCompiledPatterns(OK_HTTP_PATTERNS, tree)) {
|
|
685
713
|
const pathNode = match.captures.path;
|