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
|
@@ -3,6 +3,7 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
|
|
|
3
3
|
/**
|
|
4
4
|
* Python HTTP plugin. Handles:
|
|
5
5
|
* - FastAPI `@app.get("/path")` provider decorators
|
|
6
|
+
* - Django `path("route/", view)` provider calls
|
|
6
7
|
* - `requests.get/post/...("url")` consumer calls
|
|
7
8
|
* - Generic `requests.request("METHOD", "url")` consumer calls
|
|
8
9
|
* - `httpx.AsyncClient` instances calling `.get/.post/...("url")`, including
|
|
@@ -41,6 +42,13 @@ const FASTAPI_APP_PATTERNS = compilePatterns({
|
|
|
41
42
|
},
|
|
42
43
|
],
|
|
43
44
|
});
|
|
45
|
+
// NOTE: Django providers are NOT extracted by this per-file source scan.
|
|
46
|
+
// A standalone scan of `path()`/`re_path()` calls cannot tell a route from an
|
|
47
|
+
// `include()` mount point, nor compose the include() prefix across files, so it
|
|
48
|
+
// emitted bogus fragments (e.g. `/api` for a mount and `/items` un-prefixed
|
|
49
|
+
// instead of the real `/api/items`). Django provider contracts come from the
|
|
50
|
+
// graph Route nodes, which the ingestion route extractor builds with the
|
|
51
|
+
// includes already composed.
|
|
44
52
|
const FASTAPI_ROUTER_PATTERNS = compilePatterns({
|
|
45
53
|
name: 'python-fastapi-router',
|
|
46
54
|
language: Python,
|
|
@@ -168,7 +176,7 @@ const FROM_IMPORT_MODULE_PATTERNS = compilePatterns({
|
|
|
168
176
|
},
|
|
169
177
|
],
|
|
170
178
|
});
|
|
171
|
-
// ─── Consumer: requests.get/post/...
|
|
179
|
+
// ─── Consumer: requests.get/post/...("literal") ──────────────────────
|
|
172
180
|
const REQUESTS_VERB_PATTERNS = compilePatterns({
|
|
173
181
|
name: 'python-requests-verb',
|
|
174
182
|
language: Python,
|
|
@@ -185,6 +193,26 @@ const REQUESTS_VERB_PATTERNS = compilePatterns({
|
|
|
185
193
|
},
|
|
186
194
|
],
|
|
187
195
|
});
|
|
196
|
+
// ─── Consumer: requests.get/post/...(url=VALUE) keyword ──────────────
|
|
197
|
+
const REQUESTS_KEYWORD_URL_PATTERNS = compilePatterns({
|
|
198
|
+
name: 'python-requests-keyword-url',
|
|
199
|
+
language: Python,
|
|
200
|
+
patterns: [
|
|
201
|
+
{
|
|
202
|
+
meta: {},
|
|
203
|
+
query: `
|
|
204
|
+
(call
|
|
205
|
+
function: (attribute
|
|
206
|
+
object: (identifier) @obj (#eq? @obj "requests")
|
|
207
|
+
attribute: (identifier) @method (#match? @method "^(get|post|put|delete|patch)$"))
|
|
208
|
+
arguments: (argument_list
|
|
209
|
+
(keyword_argument
|
|
210
|
+
name: (identifier) @kw (#eq? @kw "url")
|
|
211
|
+
value: (string) @path)))
|
|
212
|
+
`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
188
216
|
// ─── Consumer: requests.request("METHOD", "url") ─────────────────────
|
|
189
217
|
const REQUESTS_GENERIC_PATTERNS = compilePatterns({
|
|
190
218
|
name: 'python-requests-generic',
|
|
@@ -202,6 +230,97 @@ const REQUESTS_GENERIC_PATTERNS = compilePatterns({
|
|
|
202
230
|
},
|
|
203
231
|
],
|
|
204
232
|
});
|
|
233
|
+
// ─── Consumer: wrapper classes with uri= or url= keyword argument ──────
|
|
234
|
+
// Common pattern: wrapper classes like RequestFetch that accept URL via
|
|
235
|
+
// named argument instead of positional argument:
|
|
236
|
+
// obj.fetch(uri="api/v1/camera/info/")
|
|
237
|
+
// obj.get(url="api/v1/camera/info/")
|
|
238
|
+
// obj.post(uri="api/v1/config/update/")
|
|
239
|
+
const WRAPPER_URI_PATTERNS = compilePatterns({
|
|
240
|
+
name: 'python-http-wrapper-uri',
|
|
241
|
+
language: Python,
|
|
242
|
+
patterns: [
|
|
243
|
+
{
|
|
244
|
+
meta: {},
|
|
245
|
+
// Match any method call where keyword argument is `uri` or `url`
|
|
246
|
+
query: `
|
|
247
|
+
(call
|
|
248
|
+
function: (attribute
|
|
249
|
+
object: (_) @client
|
|
250
|
+
attribute: (identifier) @method)
|
|
251
|
+
arguments: (argument_list
|
|
252
|
+
(keyword_argument
|
|
253
|
+
name: (identifier) @kw (#match? @kw "^(uri|url)$")
|
|
254
|
+
value: (string) @path)))
|
|
255
|
+
`,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
// Map wrapper method names to HTTP verbs
|
|
260
|
+
const WRAPPER_METHOD_TO_HTTP = {
|
|
261
|
+
get: 'GET',
|
|
262
|
+
post: 'POST',
|
|
263
|
+
put: 'PUT',
|
|
264
|
+
delete: 'DELETE',
|
|
265
|
+
patch: 'PATCH',
|
|
266
|
+
fetch: 'GET',
|
|
267
|
+
request: 'GET',
|
|
268
|
+
};
|
|
269
|
+
// ─── Variable-to-string propagation patterns ─────────────────────────
|
|
270
|
+
// Many repos assign URL paths to local variables then pass them as
|
|
271
|
+
// keyword arguments: uri = "api/v1/endpoint/"; obj.fetch(uri=uri, body)
|
|
272
|
+
// These patterns + buildLocalStringMap resolve the variable → literal chain.
|
|
273
|
+
// Track local string constants: uri = "api/v1/endpoint/"
|
|
274
|
+
const LOCAL_STRING_ASSIGNMENTS = compilePatterns({
|
|
275
|
+
name: 'python-local-string-assign',
|
|
276
|
+
language: Python,
|
|
277
|
+
patterns: [
|
|
278
|
+
{
|
|
279
|
+
meta: {},
|
|
280
|
+
query: `
|
|
281
|
+
(assignment
|
|
282
|
+
left: (identifier) @var_name
|
|
283
|
+
right: (string) @var_value)
|
|
284
|
+
`,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
// Match method calls where uri=/url= value is a variable that was previously
|
|
289
|
+
// assigned a string literal
|
|
290
|
+
const WRAPPER_URI_VAR_PATTERNS = compilePatterns({
|
|
291
|
+
name: 'python-http-wrapper-uri-var',
|
|
292
|
+
language: Python,
|
|
293
|
+
patterns: [
|
|
294
|
+
{
|
|
295
|
+
meta: {},
|
|
296
|
+
query: `
|
|
297
|
+
(call
|
|
298
|
+
function: (attribute
|
|
299
|
+
object: (_) @client
|
|
300
|
+
attribute: (identifier) @method)
|
|
301
|
+
arguments: (argument_list
|
|
302
|
+
(keyword_argument
|
|
303
|
+
name: (identifier) @kw (#match? @kw "^(uri|url)$")
|
|
304
|
+
value: (identifier) @path_var)))
|
|
305
|
+
`,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
});
|
|
309
|
+
// Pre-scan: collect local string assignments (uri = "api/v1/endpoint/")
|
|
310
|
+
function buildLocalStringMap(tree) {
|
|
311
|
+
const map = new Map();
|
|
312
|
+
for (const match of runCompiledPatterns(LOCAL_STRING_ASSIGNMENTS, tree)) {
|
|
313
|
+
const varNode = match.captures.var_name;
|
|
314
|
+
const valNode = match.captures.var_value;
|
|
315
|
+
if (!varNode || !valNode)
|
|
316
|
+
continue;
|
|
317
|
+
const val = unquoteLiteral(valNode.text);
|
|
318
|
+
if (val === null)
|
|
319
|
+
continue;
|
|
320
|
+
map.set(varNode.text, val);
|
|
321
|
+
}
|
|
322
|
+
return map;
|
|
323
|
+
}
|
|
205
324
|
// ─── Consumer: httpx.AsyncClient assignments ────────────────────────
|
|
206
325
|
// Module-scope clients are only matched
|
|
207
326
|
// at module scope; calls inside functions require a function/class-local tracked
|
|
@@ -734,6 +853,9 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
734
853
|
confidence: 0.8,
|
|
735
854
|
});
|
|
736
855
|
}
|
|
856
|
+
// Django providers come from the graph Route nodes (includes composed by
|
|
857
|
+
// the ingestion route extractor), not a per-file source scan — see the note
|
|
858
|
+
// at the top of this file.
|
|
737
859
|
// Providers: FastAPI @router.<verb>("/path") — must be joined
|
|
738
860
|
// with the prefix(es) declared at the include_router site. When
|
|
739
861
|
// no prefix is found we still emit the unprefixed path so this
|
|
@@ -791,6 +913,24 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
791
913
|
confidence: 0.7,
|
|
792
914
|
});
|
|
793
915
|
}
|
|
916
|
+
// Consumers: requests.<verb>(url="literal") keyword
|
|
917
|
+
for (const match of runCompiledPatterns(REQUESTS_KEYWORD_URL_PATTERNS, tree)) {
|
|
918
|
+
const methodNode = match.captures.method;
|
|
919
|
+
const pathNode = match.captures.path;
|
|
920
|
+
if (!methodNode || !pathNode)
|
|
921
|
+
continue;
|
|
922
|
+
const path = unquoteLiteral(pathNode.text);
|
|
923
|
+
if (path === null)
|
|
924
|
+
continue;
|
|
925
|
+
out.push({
|
|
926
|
+
role: 'consumer',
|
|
927
|
+
framework: 'python-requests',
|
|
928
|
+
method: methodNode.text.toUpperCase(),
|
|
929
|
+
path,
|
|
930
|
+
name: null,
|
|
931
|
+
confidence: 0.7,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
794
934
|
// Consumers: requests.request("METHOD", "url")
|
|
795
935
|
for (const match of runCompiledPatterns(REQUESTS_GENERIC_PATTERNS, tree)) {
|
|
796
936
|
const methodNode = match.captures.http_method;
|
|
@@ -853,6 +993,86 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
853
993
|
confidence: 0.7,
|
|
854
994
|
});
|
|
855
995
|
}
|
|
996
|
+
// Consumers: wrapper classes with uri= or url= keyword argument
|
|
997
|
+
// obj.fetch(uri="api/v1/camera/info/")
|
|
998
|
+
// obj.post(url="api/v1/config/update/")
|
|
999
|
+
const seenUriDetections = new Set(); // node byte ranges, to avoid duplicates
|
|
1000
|
+
for (const match of runCompiledPatterns(WRAPPER_URI_PATTERNS, tree)) {
|
|
1001
|
+
const methodNode = match.captures.method;
|
|
1002
|
+
const pathNode = match.captures.path;
|
|
1003
|
+
if (!methodNode || !pathNode)
|
|
1004
|
+
continue;
|
|
1005
|
+
const path = unquoteLiteral(pathNode.text);
|
|
1006
|
+
if (path === null)
|
|
1007
|
+
continue;
|
|
1008
|
+
// Deduplicate: the two pattern branches can match the same call. Key on
|
|
1009
|
+
// node byte offsets, not line arithmetic (lineNum*1000+row can collide in
|
|
1010
|
+
// files over 1000 lines, and miss a real dup when a node straddles a line).
|
|
1011
|
+
const dedupKey = `${pathNode.startIndex}:${methodNode.startIndex}`;
|
|
1012
|
+
if (seenUriDetections.has(dedupKey))
|
|
1013
|
+
continue;
|
|
1014
|
+
seenUriDetections.add(dedupKey);
|
|
1015
|
+
const methodName = methodNode.text.toLowerCase();
|
|
1016
|
+
// Map wrapper method name to HTTP verb (fetch, request → GET)
|
|
1017
|
+
const httpMethod = WRAPPER_METHOD_TO_HTTP[methodName] ?? 'GET';
|
|
1018
|
+
out.push({
|
|
1019
|
+
role: 'consumer',
|
|
1020
|
+
framework: 'python-http-wrapper',
|
|
1021
|
+
method: httpMethod,
|
|
1022
|
+
path,
|
|
1023
|
+
name: null,
|
|
1024
|
+
confidence: 0.65,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
// Variable propagation: uri = "api/v1/endpoint/"; obj.fetch(uri=uri)
|
|
1028
|
+
// Many repos assign URL paths to local vars then pass as keyword args.
|
|
1029
|
+
const localStrings = buildLocalStringMap(tree);
|
|
1030
|
+
const seenVarDetections = new Set();
|
|
1031
|
+
for (const match of runCompiledPatterns(WRAPPER_URI_VAR_PATTERNS, tree)) {
|
|
1032
|
+
const methodNode = match.captures.method;
|
|
1033
|
+
const pathVarNode = match.captures.path_var;
|
|
1034
|
+
if (!methodNode || !pathVarNode)
|
|
1035
|
+
continue;
|
|
1036
|
+
const dedupKey = `${pathVarNode.startPosition.row}:${methodNode.startPosition.row}`;
|
|
1037
|
+
if (seenVarDetections.has(dedupKey))
|
|
1038
|
+
continue;
|
|
1039
|
+
seenVarDetections.add(dedupKey);
|
|
1040
|
+
const resolved = localStrings.get(pathVarNode.text);
|
|
1041
|
+
if (!resolved)
|
|
1042
|
+
continue;
|
|
1043
|
+
const normalized = normalizeConsumerPath(resolved);
|
|
1044
|
+
if (normalized === '/')
|
|
1045
|
+
continue;
|
|
1046
|
+
const httpMethod = WRAPPER_METHOD_TO_HTTP[methodNode.text.toLowerCase()] ?? 'GET';
|
|
1047
|
+
out.push({
|
|
1048
|
+
role: 'consumer',
|
|
1049
|
+
framework: 'python-http-wrapper',
|
|
1050
|
+
method: httpMethod,
|
|
1051
|
+
path: normalized,
|
|
1052
|
+
name: null,
|
|
1053
|
+
confidence: 0.6,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
856
1056
|
return out;
|
|
857
1057
|
},
|
|
858
1058
|
};
|
|
1059
|
+
/** Normalize consumer path: strip host, template literals, numeric segments → {param} */
|
|
1060
|
+
function normalizeConsumerPath(url) {
|
|
1061
|
+
let s = url.replace(/\$\{[^}]+\}/g, '{param}').trim();
|
|
1062
|
+
if (/^https?:\/\//i.test(s)) {
|
|
1063
|
+
try {
|
|
1064
|
+
s = new URL(s).pathname;
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
s = s.replace(/^https?:\/\/[^/]+/i, '');
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (!s.startsWith('/'))
|
|
1071
|
+
s = '/' + s;
|
|
1072
|
+
const segments = s
|
|
1073
|
+
.split('/')
|
|
1074
|
+
.filter(Boolean)
|
|
1075
|
+
.map((seg) => (/^\d+$/.test(seg) ? '{param}' : seg));
|
|
1076
|
+
s = '/' + segments.join('/');
|
|
1077
|
+
return s.replace(/\/+$/, '') || '/';
|
|
1078
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, language-agnostic primitives for Spring / OpenFeign / Spring-HTTP-Interface
|
|
3
|
+
* HTTP *consumer* extraction, used by BOTH the Java (`java.ts`) and Kotlin
|
|
4
|
+
* (`kotlin.ts`) group-layer HTTP plugins so the two cannot drift apart.
|
|
5
|
+
*
|
|
6
|
+
* These are pure value maps + string helpers — no tree-sitter AST knowledge.
|
|
7
|
+
* Each language plugin keeps its own grammar-specific queries and walkers and
|
|
8
|
+
* funnels the extracted (verb, path, prefix) facts through these helpers, so a
|
|
9
|
+
* polyglot repo emits byte-identical contract IDs from `.java` and `.kt`.
|
|
10
|
+
*
|
|
11
|
+
* The provider-side annotation→verb map (`METHOD_ANNOTATION_TO_HTTP`),
|
|
12
|
+
* `isRouteMemberKey`, and `findEnclosingClass` live in the lower
|
|
13
|
+
* `ingestion/route-extractors/spring-shared.ts` (shared with the ingestion
|
|
14
|
+
* route extractor). This module is the consumer-side counterpart and lives in
|
|
15
|
+
* the group layer beside the plugins that use it.
|
|
16
|
+
*/
|
|
17
|
+
import type { HttpFileDetections } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* RestTemplate method-name → HTTP verb. Source-scan only: the receiver must be
|
|
20
|
+
* named exactly `restTemplate` (the per-language query enforces that).
|
|
21
|
+
*/
|
|
22
|
+
export declare const REST_TEMPLATE_TO_HTTP: Record<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* Reactive WebClient short-form verb helper → HTTP verb
|
|
25
|
+
* (`webClient.get().uri("/x")`, `.post()`, ...). HEAD/OPTIONS/TRACE are
|
|
26
|
+
* intentionally excluded for symmetry across the plugins.
|
|
27
|
+
*/
|
|
28
|
+
export declare const WEB_CLIENT_SHORT_TO_HTTP: Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* Accepted HTTP verbs for the WebClient long form
|
|
31
|
+
* `webClient.method(HttpMethod.X).uri("/y")`. The verb is captured as the
|
|
32
|
+
* literal `HttpMethod.X` field name (`GET`, `POST`, …); HEAD/OPTIONS/TRACE are
|
|
33
|
+
* intentionally excluded for symmetry with the short form
|
|
34
|
+
* (`WEB_CLIENT_SHORT_TO_HTTP`). Shared so the Java and Kotlin long-form scans
|
|
35
|
+
* gate verbs identically.
|
|
36
|
+
*/
|
|
37
|
+
export declare const WEB_CLIENT_LONG_VERB_RE: RegExp;
|
|
38
|
+
/**
|
|
39
|
+
* Spring 6 declarative HTTP Interface shortcut annotation → HTTP verb.
|
|
40
|
+
*
|
|
41
|
+
* `@GetExchange`/`@PostExchange`/… on a service interface proxied by
|
|
42
|
+
* `HttpServiceProxyFactory` (over RestClient / WebClient / RestTemplate)
|
|
43
|
+
* describe an OUTBOUND call — i.e. a CONSUMER, the modern analogue of an
|
|
44
|
+
* OpenFeign `@(Get|Post|...)Mapping` interface method. The path lives in the
|
|
45
|
+
* annotation's `url` (or `value`) attribute, or positionally.
|
|
46
|
+
*
|
|
47
|
+
* The base `@HttpExchange(method = "GET", url = "...")` form carries its verb
|
|
48
|
+
* in an attribute rather than the annotation name; the shortcut annotations
|
|
49
|
+
* above are the overwhelmingly common case and the only ones mapped here.
|
|
50
|
+
*/
|
|
51
|
+
export declare const EXCHANGE_ANNOTATION_TO_HTTP: Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Accumulate a route prefix (de-duped) under a class/interface declaration node
|
|
54
|
+
* id. A Spring route attribute is `String[]`; a multi-element array (`["/a","/b"]`,
|
|
55
|
+
* `arrayOf("/a","/b")`, `{"/a","/b"}`) yields one query match per element, so
|
|
56
|
+
* prefixes accumulate rather than overwrite. Shared by the Java and Kotlin plugins
|
|
57
|
+
* so both build their prefix maps identically.
|
|
58
|
+
*/
|
|
59
|
+
export declare const pushPrefix: (map: Map<number, string[]>, id: number, prefix: string) => void;
|
|
60
|
+
/** Framework tags emitted on consumer detections (stable contract metadata). */
|
|
61
|
+
export declare const OPENFEIGN_FRAMEWORK = "openfeign";
|
|
62
|
+
export declare const HTTP_INTERFACE_FRAMEWORK = "spring-http-interface";
|
|
63
|
+
/** Consumer-detection confidences, shared so `.java` and `.kt` agree. */
|
|
64
|
+
export declare const FEIGN_CONFIDENCE = 0.7;
|
|
65
|
+
export declare const REQUEST_LINE_CONFIDENCE = 0.75;
|
|
66
|
+
export declare const EXCHANGE_CONFIDENCE = 0.75;
|
|
67
|
+
/**
|
|
68
|
+
* OpenFeign's native `@RequestLine("METHOD /path[?query]")` packs an HTTP
|
|
69
|
+
* method and path in a single string literal — see
|
|
70
|
+
* https://github.com/OpenFeign/feign#interface-annotations. This regex splits
|
|
71
|
+
* the verb from the path of that literal.
|
|
72
|
+
*/
|
|
73
|
+
export declare const REQUEST_LINE_VERB_RE: RegExp;
|
|
74
|
+
/**
|
|
75
|
+
* Parse a Feign `@RequestLine` value into a method + path pair. The query
|
|
76
|
+
* portion is dropped because contract IDs are method+path only (consistent
|
|
77
|
+
* with how RestTemplate/WebClient consumers drop inline query strings).
|
|
78
|
+
*
|
|
79
|
+
* Returns null if the value is not a recognized HTTP verb followed by a path
|
|
80
|
+
* beginning with `/`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseRequestLine(raw: string): {
|
|
83
|
+
method: string;
|
|
84
|
+
path: string;
|
|
85
|
+
} | null;
|
|
86
|
+
/**
|
|
87
|
+
* Join a class/interface-level prefix and a method-level path into a single
|
|
88
|
+
* URL path: strip leading/trailing slashes on the prefix and leading slashes
|
|
89
|
+
* on the method path, then ensure exactly one slash between them.
|
|
90
|
+
*/
|
|
91
|
+
export declare function joinPath(prefix: string, methodPath: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* Join a controller's own class prefix with a route inherited from an interface
|
|
94
|
+
* (interface-based controllers, #1743). The inherited path already has the
|
|
95
|
+
* interface's own class prefix (`inheritedOwnerPrefix`) baked in; when the
|
|
96
|
+
* controller repeats that same prefix we must NOT prepend it twice (#2057).
|
|
97
|
+
* Shared by both plugins' `scanProject` so Java and Kotlin agree.
|
|
98
|
+
*/
|
|
99
|
+
export declare function joinInheritedSpringPath(controllerPrefix: string, inheritedPath: string, inheritedOwnerPrefix?: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Language-agnostic view of a Spring class/interface that each plugin's
|
|
102
|
+
* grammar-specific collector produces. The interface-based-controller
|
|
103
|
+
* inheritance algorithm (`scanSpringInheritanceProject`) operates only on this
|
|
104
|
+
* shape, so the Java and Kotlin plugins share one algorithm and cannot drift.
|
|
105
|
+
*
|
|
106
|
+
* `methods[].routes` carry only `{ method, path }` — the interface's own class
|
|
107
|
+
* prefix is applied *inside* `scanSpringInheritanceProject` (it is not part of
|
|
108
|
+
* the collector's output).
|
|
109
|
+
*/
|
|
110
|
+
export interface SharedSpringType {
|
|
111
|
+
filePath: string;
|
|
112
|
+
kind: 'class' | 'interface';
|
|
113
|
+
name: string;
|
|
114
|
+
/** Class-level `@RequestMapping` prefixes — one per array element. */
|
|
115
|
+
classPrefixes: string[];
|
|
116
|
+
implementedInterfaces: string[];
|
|
117
|
+
isController: boolean;
|
|
118
|
+
methods: Array<{
|
|
119
|
+
name: string;
|
|
120
|
+
routes: Array<{
|
|
121
|
+
method: string;
|
|
122
|
+
path: string;
|
|
123
|
+
}>;
|
|
124
|
+
}>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve interface-based-controller provider routes (#1743): a concrete
|
|
128
|
+
* `@RestController`/`@Controller` class inherits the `@(Get|...)Mapping` routes
|
|
129
|
+
* declared on the interface it implements. Shared by the Java and Kotlin plugins
|
|
130
|
+
* so both emit byte-identical provider contracts.
|
|
131
|
+
*
|
|
132
|
+
* An interface name that resolves to two distinct interfaces is ambiguous and
|
|
133
|
+
* its routes are dropped (the `null` marker). The controller's own class
|
|
134
|
+
* prefix(es) cross-product the inherited routes; `joinInheritedSpringPath`
|
|
135
|
+
* avoids doubling a prefix the interface already baked in (#2057).
|
|
136
|
+
*/
|
|
137
|
+
export declare function scanSpringInheritanceProject(types: SharedSpringType[]): HttpFileDetections[];
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, language-agnostic primitives for Spring / OpenFeign / Spring-HTTP-Interface
|
|
3
|
+
* HTTP *consumer* extraction, used by BOTH the Java (`java.ts`) and Kotlin
|
|
4
|
+
* (`kotlin.ts`) group-layer HTTP plugins so the two cannot drift apart.
|
|
5
|
+
*
|
|
6
|
+
* These are pure value maps + string helpers — no tree-sitter AST knowledge.
|
|
7
|
+
* Each language plugin keeps its own grammar-specific queries and walkers and
|
|
8
|
+
* funnels the extracted (verb, path, prefix) facts through these helpers, so a
|
|
9
|
+
* polyglot repo emits byte-identical contract IDs from `.java` and `.kt`.
|
|
10
|
+
*
|
|
11
|
+
* The provider-side annotation→verb map (`METHOD_ANNOTATION_TO_HTTP`),
|
|
12
|
+
* `isRouteMemberKey`, and `findEnclosingClass` live in the lower
|
|
13
|
+
* `ingestion/route-extractors/spring-shared.ts` (shared with the ingestion
|
|
14
|
+
* route extractor). This module is the consumer-side counterpart and lives in
|
|
15
|
+
* the group layer beside the plugins that use it.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* RestTemplate method-name → HTTP verb. Source-scan only: the receiver must be
|
|
19
|
+
* named exactly `restTemplate` (the per-language query enforces that).
|
|
20
|
+
*/
|
|
21
|
+
export const REST_TEMPLATE_TO_HTTP = {
|
|
22
|
+
getForObject: 'GET',
|
|
23
|
+
getForEntity: 'GET',
|
|
24
|
+
postForObject: 'POST',
|
|
25
|
+
postForEntity: 'POST',
|
|
26
|
+
put: 'PUT',
|
|
27
|
+
delete: 'DELETE',
|
|
28
|
+
patchForObject: 'PATCH',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Reactive WebClient short-form verb helper → HTTP verb
|
|
32
|
+
* (`webClient.get().uri("/x")`, `.post()`, ...). HEAD/OPTIONS/TRACE are
|
|
33
|
+
* intentionally excluded for symmetry across the plugins.
|
|
34
|
+
*/
|
|
35
|
+
export const WEB_CLIENT_SHORT_TO_HTTP = {
|
|
36
|
+
get: 'GET',
|
|
37
|
+
post: 'POST',
|
|
38
|
+
put: 'PUT',
|
|
39
|
+
delete: 'DELETE',
|
|
40
|
+
patch: 'PATCH',
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Accepted HTTP verbs for the WebClient long form
|
|
44
|
+
* `webClient.method(HttpMethod.X).uri("/y")`. The verb is captured as the
|
|
45
|
+
* literal `HttpMethod.X` field name (`GET`, `POST`, …); HEAD/OPTIONS/TRACE are
|
|
46
|
+
* intentionally excluded for symmetry with the short form
|
|
47
|
+
* (`WEB_CLIENT_SHORT_TO_HTTP`). Shared so the Java and Kotlin long-form scans
|
|
48
|
+
* gate verbs identically.
|
|
49
|
+
*/
|
|
50
|
+
export const WEB_CLIENT_LONG_VERB_RE = /^(GET|POST|PUT|DELETE|PATCH)$/;
|
|
51
|
+
/**
|
|
52
|
+
* Spring 6 declarative HTTP Interface shortcut annotation → HTTP verb.
|
|
53
|
+
*
|
|
54
|
+
* `@GetExchange`/`@PostExchange`/… on a service interface proxied by
|
|
55
|
+
* `HttpServiceProxyFactory` (over RestClient / WebClient / RestTemplate)
|
|
56
|
+
* describe an OUTBOUND call — i.e. a CONSUMER, the modern analogue of an
|
|
57
|
+
* OpenFeign `@(Get|Post|...)Mapping` interface method. The path lives in the
|
|
58
|
+
* annotation's `url` (or `value`) attribute, or positionally.
|
|
59
|
+
*
|
|
60
|
+
* The base `@HttpExchange(method = "GET", url = "...")` form carries its verb
|
|
61
|
+
* in an attribute rather than the annotation name; the shortcut annotations
|
|
62
|
+
* above are the overwhelmingly common case and the only ones mapped here.
|
|
63
|
+
*/
|
|
64
|
+
export const EXCHANGE_ANNOTATION_TO_HTTP = {
|
|
65
|
+
GetExchange: 'GET',
|
|
66
|
+
PostExchange: 'POST',
|
|
67
|
+
PutExchange: 'PUT',
|
|
68
|
+
DeleteExchange: 'DELETE',
|
|
69
|
+
PatchExchange: 'PATCH',
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Accumulate a route prefix (de-duped) under a class/interface declaration node
|
|
73
|
+
* id. A Spring route attribute is `String[]`; a multi-element array (`["/a","/b"]`,
|
|
74
|
+
* `arrayOf("/a","/b")`, `{"/a","/b"}`) yields one query match per element, so
|
|
75
|
+
* prefixes accumulate rather than overwrite. Shared by the Java and Kotlin plugins
|
|
76
|
+
* so both build their prefix maps identically.
|
|
77
|
+
*/
|
|
78
|
+
export const pushPrefix = (map, id, prefix) => {
|
|
79
|
+
const arr = map.get(id) ?? [];
|
|
80
|
+
if (!arr.includes(prefix))
|
|
81
|
+
arr.push(prefix);
|
|
82
|
+
map.set(id, arr);
|
|
83
|
+
};
|
|
84
|
+
/** Framework tags emitted on consumer detections (stable contract metadata). */
|
|
85
|
+
export const OPENFEIGN_FRAMEWORK = 'openfeign';
|
|
86
|
+
export const HTTP_INTERFACE_FRAMEWORK = 'spring-http-interface';
|
|
87
|
+
/** Consumer-detection confidences, shared so `.java` and `.kt` agree. */
|
|
88
|
+
export const FEIGN_CONFIDENCE = 0.7;
|
|
89
|
+
export const REQUEST_LINE_CONFIDENCE = 0.75;
|
|
90
|
+
export const EXCHANGE_CONFIDENCE = 0.75;
|
|
91
|
+
/**
|
|
92
|
+
* OpenFeign's native `@RequestLine("METHOD /path[?query]")` packs an HTTP
|
|
93
|
+
* method and path in a single string literal — see
|
|
94
|
+
* https://github.com/OpenFeign/feign#interface-annotations. This regex splits
|
|
95
|
+
* the verb from the path of that literal.
|
|
96
|
+
*/
|
|
97
|
+
export const REQUEST_LINE_VERB_RE = /^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\S.*?)\s*$/i;
|
|
98
|
+
/**
|
|
99
|
+
* Parse a Feign `@RequestLine` value into a method + path pair. The query
|
|
100
|
+
* portion is dropped because contract IDs are method+path only (consistent
|
|
101
|
+
* with how RestTemplate/WebClient consumers drop inline query strings).
|
|
102
|
+
*
|
|
103
|
+
* Returns null if the value is not a recognized HTTP verb followed by a path
|
|
104
|
+
* beginning with `/`.
|
|
105
|
+
*/
|
|
106
|
+
export function parseRequestLine(raw) {
|
|
107
|
+
const match = REQUEST_LINE_VERB_RE.exec(raw);
|
|
108
|
+
if (!match)
|
|
109
|
+
return null;
|
|
110
|
+
const [, verb, rest] = match;
|
|
111
|
+
if (typeof verb !== 'string' || typeof rest !== 'string')
|
|
112
|
+
return null;
|
|
113
|
+
const queryIdx = rest.indexOf('?');
|
|
114
|
+
const pathOnly = (queryIdx >= 0 ? rest.slice(0, queryIdx) : rest).trim();
|
|
115
|
+
if (!pathOnly.startsWith('/'))
|
|
116
|
+
return null;
|
|
117
|
+
return { method: verb.toUpperCase(), path: pathOnly };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Join a class/interface-level prefix and a method-level path into a single
|
|
121
|
+
* URL path: strip leading/trailing slashes on the prefix and leading slashes
|
|
122
|
+
* on the method path, then ensure exactly one slash between them.
|
|
123
|
+
*/
|
|
124
|
+
export function joinPath(prefix, methodPath) {
|
|
125
|
+
const cleanPrefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
126
|
+
const cleanSub = methodPath.replace(/^\/+/, '');
|
|
127
|
+
if (!cleanPrefix)
|
|
128
|
+
return `/${cleanSub}`;
|
|
129
|
+
return `/${cleanPrefix}/${cleanSub}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Join a controller's own class prefix with a route inherited from an interface
|
|
133
|
+
* (interface-based controllers, #1743). The inherited path already has the
|
|
134
|
+
* interface's own class prefix (`inheritedOwnerPrefix`) baked in; when the
|
|
135
|
+
* controller repeats that same prefix we must NOT prepend it twice (#2057).
|
|
136
|
+
* Shared by both plugins' `scanProject` so Java and Kotlin agree.
|
|
137
|
+
*/
|
|
138
|
+
export function joinInheritedSpringPath(controllerPrefix, inheritedPath, inheritedOwnerPrefix = '') {
|
|
139
|
+
const joined = joinPath(controllerPrefix, inheritedPath);
|
|
140
|
+
const cleanPrefix = controllerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
141
|
+
const cleanOwnerPrefix = inheritedOwnerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
142
|
+
const cleanInherited = inheritedPath.replace(/^\/+/, '');
|
|
143
|
+
if (!cleanPrefix)
|
|
144
|
+
return joined;
|
|
145
|
+
if (cleanPrefix === cleanOwnerPrefix &&
|
|
146
|
+
(cleanInherited === cleanPrefix || cleanInherited.startsWith(`${cleanPrefix}/`))) {
|
|
147
|
+
return `/${cleanInherited}`;
|
|
148
|
+
}
|
|
149
|
+
return joined;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve interface-based-controller provider routes (#1743): a concrete
|
|
153
|
+
* `@RestController`/`@Controller` class inherits the `@(Get|...)Mapping` routes
|
|
154
|
+
* declared on the interface it implements. Shared by the Java and Kotlin plugins
|
|
155
|
+
* so both emit byte-identical provider contracts.
|
|
156
|
+
*
|
|
157
|
+
* An interface name that resolves to two distinct interfaces is ambiguous and
|
|
158
|
+
* its routes are dropped (the `null` marker). The controller's own class
|
|
159
|
+
* prefix(es) cross-product the inherited routes; `joinInheritedSpringPath`
|
|
160
|
+
* avoids doubling a prefix the interface already baked in (#2057).
|
|
161
|
+
*/
|
|
162
|
+
export function scanSpringInheritanceProject(types) {
|
|
163
|
+
const interfaceRoutes = new Map();
|
|
164
|
+
for (const type of types) {
|
|
165
|
+
if (type.kind !== 'interface')
|
|
166
|
+
continue;
|
|
167
|
+
if (interfaceRoutes.has(type.name)) {
|
|
168
|
+
interfaceRoutes.set(type.name, null);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const prefixes = type.classPrefixes.length ? type.classPrefixes : [''];
|
|
172
|
+
const methodMap = new Map();
|
|
173
|
+
for (const method of type.methods) {
|
|
174
|
+
// Cross-product the interface's class prefixes with each method route, so a
|
|
175
|
+
// multi-element `@RequestMapping(["/a","/b"])` interface yields N bindings.
|
|
176
|
+
const routes = method.routes.flatMap((route) => prefixes.map((prefix) => ({
|
|
177
|
+
method: route.method,
|
|
178
|
+
path: prefix ? joinPath(prefix, route.path) : route.path,
|
|
179
|
+
ownerPrefix: prefix,
|
|
180
|
+
})));
|
|
181
|
+
if (routes.length > 0)
|
|
182
|
+
methodMap.set(method.name, routes);
|
|
183
|
+
}
|
|
184
|
+
interfaceRoutes.set(type.name, methodMap);
|
|
185
|
+
}
|
|
186
|
+
const detectionsByFile = new Map();
|
|
187
|
+
for (const type of types) {
|
|
188
|
+
if (type.kind !== 'class' || !type.isController)
|
|
189
|
+
continue;
|
|
190
|
+
// Cross-product the controller's own class prefixes with each inherited
|
|
191
|
+
// route; `['']` keeps the common no-prefix controller emitting the
|
|
192
|
+
// interface path unchanged.
|
|
193
|
+
const controllerPrefixes = type.classPrefixes.length ? type.classPrefixes : [''];
|
|
194
|
+
for (const method of type.methods) {
|
|
195
|
+
if (method.routes.length > 0)
|
|
196
|
+
continue; // own @*Mapping → already a provider via scan()
|
|
197
|
+
const inherited = type.implementedInterfaces.flatMap((iface) => {
|
|
198
|
+
const routeMap = interfaceRoutes.get(iface);
|
|
199
|
+
if (!routeMap)
|
|
200
|
+
return [];
|
|
201
|
+
const routes = routeMap.get(method.name) ?? [];
|
|
202
|
+
return routes.flatMap((route) => controllerPrefixes.map((controllerPrefix) => ({
|
|
203
|
+
method: route.method,
|
|
204
|
+
path: joinInheritedSpringPath(controllerPrefix, route.path, route.ownerPrefix),
|
|
205
|
+
})));
|
|
206
|
+
});
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
for (const route of inherited) {
|
|
209
|
+
const key = `${route.method} ${route.path}`;
|
|
210
|
+
if (seen.has(key))
|
|
211
|
+
continue;
|
|
212
|
+
seen.add(key);
|
|
213
|
+
const detections = detectionsByFile.get(type.filePath) ?? [];
|
|
214
|
+
detections.push({
|
|
215
|
+
role: 'provider',
|
|
216
|
+
framework: 'spring',
|
|
217
|
+
method: route.method,
|
|
218
|
+
path: route.path,
|
|
219
|
+
name: method.name,
|
|
220
|
+
confidence: 0.8,
|
|
221
|
+
});
|
|
222
|
+
detectionsByFile.set(type.filePath, detections);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return [...detectionsByFile.entries()].map(([filePath, detections]) => ({
|
|
227
|
+
filePath,
|
|
228
|
+
detections,
|
|
229
|
+
}));
|
|
230
|
+
}
|