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.
@@ -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
+ }