gitnexus 1.6.6-rc.74 → 1.6.6-rc.76
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/index.d.ts +6 -1
- package/dist/core/group/extractors/http-patterns/index.js +16 -1
- package/dist/core/group/extractors/http-patterns/java.js +44 -0
- package/dist/core/group/extractors/http-patterns/kotlin.d.ts +8 -0
- package/dist/core/group/extractors/http-patterns/kotlin.js +457 -0
- package/package.json +1 -1
|
@@ -4,12 +4,17 @@ export type { HttpDetection, HttpLanguagePlugin, HttpRole } from './types.js';
|
|
|
4
4
|
* Glob for files worth scanning for HTTP routes. Kept alongside the
|
|
5
5
|
* registry so adding a new language widens the glob in one edit.
|
|
6
6
|
*
|
|
7
|
+
* `.kt`/`.kts` are always present in the glob even when the optional
|
|
8
|
+
* `tree-sitter-kotlin` grammar isn't installed — `getPluginForFile`
|
|
9
|
+
* will return `undefined` for those files in that case, so the
|
|
10
|
+
* orchestrator simply skips them at scan time without erroring.
|
|
11
|
+
*
|
|
7
12
|
* `.vue` / `.svelte` files are intentionally omitted for the source-scan
|
|
8
13
|
* path — they need their own grammar-aware extraction and the existing
|
|
9
14
|
* regex fallback for them was never very accurate. The graph-assisted
|
|
10
15
|
* Strategy A still handles them via the ingestion pipeline.
|
|
11
16
|
*/
|
|
12
|
-
export declare const HTTP_SCAN_GLOB = "**/*.{ts,tsx,js,jsx,java,go,py,php}";
|
|
17
|
+
export declare const HTTP_SCAN_GLOB = "**/*.{ts,tsx,js,jsx,java,kt,kts,go,py,php}";
|
|
13
18
|
/**
|
|
14
19
|
* Return the HTTP plugin registered for the given file's extension,
|
|
15
20
|
* or `undefined` if the extension is not registered.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import { isBladeTemplateFilename } from '../../../../_shared/index.js';
|
|
3
3
|
import { JAVA_HTTP_PLUGIN } from './java.js';
|
|
4
|
+
import { KOTLIN_HTTP_PLUGIN } from './kotlin.js';
|
|
4
5
|
import { GO_HTTP_PLUGIN } from './go.js';
|
|
5
6
|
import { PYTHON_HTTP_PLUGIN } from './python.js';
|
|
6
7
|
import { PHP_HTTP_PLUGIN } from './php.js';
|
|
@@ -14,6 +15,11 @@ import { JAVASCRIPT_HTTP_PLUGIN, TYPESCRIPT_HTTP_PLUGIN, TSX_HTTP_PLUGIN } from
|
|
|
14
15
|
* new language, drop a `http-patterns/<lang>.ts` that exports a
|
|
15
16
|
* `HttpLanguagePlugin`, import it here and register the extension(s).
|
|
16
17
|
* No edits to `http-route-extractor.ts` are required.
|
|
18
|
+
*
|
|
19
|
+
* Optional grammar plugins (e.g. `kotlin.ts`, which depends on the
|
|
20
|
+
* optionalDependency `tree-sitter-kotlin`) export `null` when the
|
|
21
|
+
* native binding is unavailable; we skip registration in that case so
|
|
22
|
+
* a missing optional grammar never crashes the orchestrator.
|
|
17
23
|
*/
|
|
18
24
|
const REGISTRY = {
|
|
19
25
|
'.java': JAVA_HTTP_PLUGIN,
|
|
@@ -25,16 +31,25 @@ const REGISTRY = {
|
|
|
25
31
|
'.ts': TYPESCRIPT_HTTP_PLUGIN,
|
|
26
32
|
'.tsx': TSX_HTTP_PLUGIN,
|
|
27
33
|
};
|
|
34
|
+
if (KOTLIN_HTTP_PLUGIN) {
|
|
35
|
+
REGISTRY['.kt'] = KOTLIN_HTTP_PLUGIN;
|
|
36
|
+
REGISTRY['.kts'] = KOTLIN_HTTP_PLUGIN;
|
|
37
|
+
}
|
|
28
38
|
/**
|
|
29
39
|
* Glob for files worth scanning for HTTP routes. Kept alongside the
|
|
30
40
|
* registry so adding a new language widens the glob in one edit.
|
|
31
41
|
*
|
|
42
|
+
* `.kt`/`.kts` are always present in the glob even when the optional
|
|
43
|
+
* `tree-sitter-kotlin` grammar isn't installed — `getPluginForFile`
|
|
44
|
+
* will return `undefined` for those files in that case, so the
|
|
45
|
+
* orchestrator simply skips them at scan time without erroring.
|
|
46
|
+
*
|
|
32
47
|
* `.vue` / `.svelte` files are intentionally omitted for the source-scan
|
|
33
48
|
* path — they need their own grammar-aware extraction and the existing
|
|
34
49
|
* regex fallback for them was never very accurate. The graph-assisted
|
|
35
50
|
* Strategy A still handles them via the ingestion pipeline.
|
|
36
51
|
*/
|
|
37
|
-
export const HTTP_SCAN_GLOB = '**/*.{ts,tsx,js,jsx,java,go,py,php}';
|
|
52
|
+
export const HTTP_SCAN_GLOB = '**/*.{ts,tsx,js,jsx,java,kt,kts,go,py,php}';
|
|
38
53
|
/**
|
|
39
54
|
* Return the HTTP plugin registered for the given file's extension,
|
|
40
55
|
* or `undefined` if the extension is not registered.
|
|
@@ -20,6 +20,19 @@ const METHOD_ANNOTATION_TO_HTTP = {
|
|
|
20
20
|
PatchMapping: 'PATCH',
|
|
21
21
|
};
|
|
22
22
|
// ─── Provider: Spring class-level @RequestMapping prefix ──────────────
|
|
23
|
+
// Two patterns are needed because the AST shape differs depending on
|
|
24
|
+
// whether the annotation uses a positional argument or a named one:
|
|
25
|
+
// @RequestMapping("/api") → (annotation_argument_list (string_literal))
|
|
26
|
+
// @RequestMapping(path = "/api") → (annotation_argument_list (element_value_pair key:(identifier) value:(string_literal)))
|
|
27
|
+
// @RequestMapping(value = "/api") → same as above
|
|
28
|
+
//
|
|
29
|
+
// The named-argument pattern MUST constrain the `key` field to the route
|
|
30
|
+
// member names (`path`/`value`); without it, the query also captures
|
|
31
|
+
// non-route attributes such as `produces`, `consumes`, `headers`, `name`,
|
|
32
|
+
// `params` (their right-hand string literals would be mis-extracted as
|
|
33
|
+
// route prefixes — e.g. `produces = "application/json"` would corrupt
|
|
34
|
+
// every method route under that controller). The sibling
|
|
35
|
+
// `topic-patterns/java.ts` uses the same `key:` constraint approach.
|
|
23
36
|
const SPRING_CLASS_PREFIX_PATTERNS = compilePatterns({
|
|
24
37
|
name: 'java-spring-class-prefix',
|
|
25
38
|
language: Java,
|
|
@@ -34,9 +47,26 @@ const SPRING_CLASS_PREFIX_PATTERNS = compilePatterns({
|
|
|
34
47
|
arguments: (annotation_argument_list (string_literal) @prefix)))) @class
|
|
35
48
|
`,
|
|
36
49
|
},
|
|
50
|
+
{
|
|
51
|
+
meta: {},
|
|
52
|
+
query: `
|
|
53
|
+
(class_declaration
|
|
54
|
+
(modifiers
|
|
55
|
+
(annotation
|
|
56
|
+
name: (identifier) @ann (#eq? @ann "RequestMapping")
|
|
57
|
+
arguments: (annotation_argument_list
|
|
58
|
+
(element_value_pair
|
|
59
|
+
key: (identifier) @key (#match? @key "^(path|value)$")
|
|
60
|
+
value: (string_literal) @prefix))))) @class
|
|
61
|
+
`,
|
|
62
|
+
},
|
|
37
63
|
],
|
|
38
64
|
});
|
|
39
65
|
// ─── Provider: Spring @(Get|Post|...)Mapping method annotations ───────
|
|
66
|
+
// Same dual-pattern approach: positional vs named argument. The named
|
|
67
|
+
// pattern restricts the annotation member name to `path`/`value` to
|
|
68
|
+
// avoid capturing unrelated string-valued attributes
|
|
69
|
+
// (`produces`, `consumes`, `headers`, `name`, `params`, ...).
|
|
40
70
|
const SPRING_METHOD_ROUTE_PATTERNS = compilePatterns({
|
|
41
71
|
name: 'java-spring-method-route',
|
|
42
72
|
language: Java,
|
|
@@ -52,6 +82,20 @@ const SPRING_METHOD_ROUTE_PATTERNS = compilePatterns({
|
|
|
52
82
|
name: (identifier) @method_name) @method
|
|
53
83
|
`,
|
|
54
84
|
},
|
|
85
|
+
{
|
|
86
|
+
meta: {},
|
|
87
|
+
query: `
|
|
88
|
+
(method_declaration
|
|
89
|
+
(modifiers
|
|
90
|
+
(annotation
|
|
91
|
+
name: (identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Mapping$")
|
|
92
|
+
arguments: (annotation_argument_list
|
|
93
|
+
(element_value_pair
|
|
94
|
+
key: (identifier) @key (#match? @key "^(path|value)$")
|
|
95
|
+
value: (string_literal) @path))))
|
|
96
|
+
name: (identifier) @method_name) @method
|
|
97
|
+
`,
|
|
98
|
+
},
|
|
55
99
|
],
|
|
56
100
|
});
|
|
57
101
|
// ─── Consumer: Spring RestTemplate (object-named + method-named) ──────
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HttpLanguagePlugin } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The exported plugin is `null` when tree-sitter-kotlin's native
|
|
4
|
+
* binding is unavailable. `http-patterns/index.ts` checks for null
|
|
5
|
+
* before registering `.kt`/`.kts` so missing optional grammars never
|
|
6
|
+
* crash the orchestrator.
|
|
7
|
+
*/
|
|
8
|
+
export declare const KOTLIN_HTTP_PLUGIN: HttpLanguagePlugin | null;
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
|
+
/**
|
|
4
|
+
* Kotlin HTTP plugin (Spring providers + consumers).
|
|
5
|
+
*
|
|
6
|
+
* **Providers** (#1849) — Spring `@RequestMapping` class prefixes and
|
|
7
|
+
* `@(Get|Post|...)Mapping` method annotations on Kotlin Spring Boot
|
|
8
|
+
* controllers. Both positional shorthand (`@GetMapping("/x")`) and
|
|
9
|
+
* named annotation arguments (`@GetMapping(value = "/x")` and
|
|
10
|
+
* `@GetMapping(path = "/x")`) are supported.
|
|
11
|
+
*
|
|
12
|
+
* **Consumers** (this PR) — three call-site patterns common in Kotlin
|
|
13
|
+
* Spring projects:
|
|
14
|
+
*
|
|
15
|
+
* 1. `restTemplate.getForObject("/x", ...)` and friends
|
|
16
|
+
* 2. `webClient.get().uri("/x")` (short form, 1 verb hop + 1 uri hop)
|
|
17
|
+
* 3. `Request.Builder().url("/x")` (OkHttp)
|
|
18
|
+
*
|
|
19
|
+
* The long-form `webClient.method(HttpMethod.X).uri("/y")` chain is
|
|
20
|
+
* intentionally deferred to a follow-up: it requires walk-up logic
|
|
21
|
+
* to recover the verb from a sibling `call_expression`, and we can
|
|
22
|
+
* land 80% of real-world Kotlin Spring consumer coverage with the
|
|
23
|
+
* three simpler patterns above.
|
|
24
|
+
*
|
|
25
|
+
* tree-sitter-kotlin (fwcd) AST shapes used here:
|
|
26
|
+
* class_declaration
|
|
27
|
+
* modifiers
|
|
28
|
+
* annotation
|
|
29
|
+
* constructor_invocation
|
|
30
|
+
* user_type → type_identifier ← annotation name
|
|
31
|
+
* value_arguments
|
|
32
|
+
* value_argument
|
|
33
|
+
* (simple_identifier "=")? ← absent for positional, present for named
|
|
34
|
+
* string_literal
|
|
35
|
+
* type_identifier ← class name
|
|
36
|
+
*
|
|
37
|
+
* Consumer call shape (Kotlin chains everything via `navigation_expression`):
|
|
38
|
+
* call_expression ← outer `.uri("/x")` or `.url("/x")`
|
|
39
|
+
* navigation_expression
|
|
40
|
+
* call_expression ← inner `.get()` / `Request.Builder()` / `restTemplate.x`
|
|
41
|
+
* navigation_expression
|
|
42
|
+
* simple_identifier ← receiver: `webClient` / `Request` / `restTemplate`
|
|
43
|
+
* navigation_suffix ← `.method` / `.Builder` / `.getForObject`
|
|
44
|
+
* call_suffix (value_arguments)
|
|
45
|
+
* navigation_suffix ← `.uri` / `.url`
|
|
46
|
+
* call_suffix
|
|
47
|
+
* value_arguments
|
|
48
|
+
* value_argument
|
|
49
|
+
* string_literal ← the path
|
|
50
|
+
*
|
|
51
|
+
* tree-sitter-kotlin is an optional npm dependency — when its native
|
|
52
|
+
* binding is unavailable the plugin gracefully exports `null` and
|
|
53
|
+
* `http-patterns/index.ts` skips registration for `.kt`/`.kts` files.
|
|
54
|
+
*/
|
|
55
|
+
const _require = createRequire(import.meta.url);
|
|
56
|
+
/** Loaded lazily; null when the grammar binding isn't installed. */
|
|
57
|
+
let Kotlin = null;
|
|
58
|
+
try {
|
|
59
|
+
Kotlin = _require('tree-sitter-kotlin');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
Kotlin = null;
|
|
63
|
+
}
|
|
64
|
+
const METHOD_ANNOTATION_TO_HTTP = {
|
|
65
|
+
GetMapping: 'GET',
|
|
66
|
+
PostMapping: 'POST',
|
|
67
|
+
PutMapping: 'PUT',
|
|
68
|
+
DeleteMapping: 'DELETE',
|
|
69
|
+
PatchMapping: 'PATCH',
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* RestTemplate method-name → HTTP verb. Mirrors the Java plugin's
|
|
73
|
+
* `REST_TEMPLATE_TO_HTTP` (java.ts) so a polyglot repo emits the
|
|
74
|
+
* same contract IDs from .java and .kt sources.
|
|
75
|
+
*/
|
|
76
|
+
const REST_TEMPLATE_TO_HTTP = {
|
|
77
|
+
getForObject: 'GET',
|
|
78
|
+
getForEntity: 'GET',
|
|
79
|
+
postForObject: 'POST',
|
|
80
|
+
postForEntity: 'POST',
|
|
81
|
+
put: 'PUT',
|
|
82
|
+
delete: 'DELETE',
|
|
83
|
+
patchForObject: 'PATCH',
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* WebClient short-form verb → HTTP verb. The reactive WebClient API
|
|
87
|
+
* exposes `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()` as
|
|
88
|
+
* one-liners that return a `RequestHeadersUriSpec` whose `.uri(...)`
|
|
89
|
+
* carries the path. We capture both pieces in a single query (see
|
|
90
|
+
* `WEB_CLIENT_SHORT_PATTERNS` below) and translate the verb here.
|
|
91
|
+
*/
|
|
92
|
+
const WEB_CLIENT_SHORT_TO_HTTP = {
|
|
93
|
+
get: 'GET',
|
|
94
|
+
post: 'POST',
|
|
95
|
+
put: 'PUT',
|
|
96
|
+
delete: 'DELETE',
|
|
97
|
+
patch: 'PATCH',
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Build the plugin only if the Kotlin grammar is available. Compiling
|
|
101
|
+
* the queries against a null grammar would throw at module load time
|
|
102
|
+
* and abort the whole http-route-extractor module.
|
|
103
|
+
*/
|
|
104
|
+
function buildKotlinPlugin(language) {
|
|
105
|
+
// ─── Provider: Spring class-level @RequestMapping prefix ──────────────
|
|
106
|
+
// Two patterns mirror the Java plugin's positional vs named split:
|
|
107
|
+
// @RequestMapping("/api") → value_argument has string_literal as its first named child
|
|
108
|
+
// @RequestMapping(path = "/api") → value_argument has [simple_identifier @key, string_literal]
|
|
109
|
+
// @RequestMapping(value = "/api") → same as above, with key="value"
|
|
110
|
+
//
|
|
111
|
+
// Tree-sitter-kotlin grammar (fwcd 0.3.8) does NOT have a separate
|
|
112
|
+
// node for named arguments — both positional and named forms share
|
|
113
|
+
// `value_argument`. The positional pattern uses the immediate-child
|
|
114
|
+
// anchor `.` so it only matches when the string_literal is the FIRST
|
|
115
|
+
// named child (i.e. no preceding simple_identifier "=" prefix). The
|
|
116
|
+
// named pattern explicitly captures the simple_identifier and uses
|
|
117
|
+
// `#match?` to restrict it to `path`/`value`, matching the same
|
|
118
|
+
// safety bar that the Java plugin enforces (see java.ts and the
|
|
119
|
+
// sibling topic-patterns/java.ts for the analogous constraint).
|
|
120
|
+
//
|
|
121
|
+
// Without the `key:` constraint the named query would also capture
|
|
122
|
+
// unrelated attributes like `produces`, `consumes`, `headers`,
|
|
123
|
+
// `name`, `params` — emitting bogus route contracts (a regression
|
|
124
|
+
// identical to the one Claude flagged on PR #1834 for Java).
|
|
125
|
+
const SPRING_CLASS_PREFIX_PATTERNS = compilePatterns({
|
|
126
|
+
name: 'kotlin-spring-class-prefix',
|
|
127
|
+
language,
|
|
128
|
+
patterns: [
|
|
129
|
+
{
|
|
130
|
+
meta: {},
|
|
131
|
+
query: `
|
|
132
|
+
(class_declaration
|
|
133
|
+
(modifiers
|
|
134
|
+
(annotation
|
|
135
|
+
(constructor_invocation
|
|
136
|
+
(user_type (type_identifier) @ann (#eq? @ann "RequestMapping"))
|
|
137
|
+
(value_arguments
|
|
138
|
+
(value_argument . (string_literal) @prefix)))))
|
|
139
|
+
(type_identifier) @cls) @class
|
|
140
|
+
`,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
meta: {},
|
|
144
|
+
query: `
|
|
145
|
+
(class_declaration
|
|
146
|
+
(modifiers
|
|
147
|
+
(annotation
|
|
148
|
+
(constructor_invocation
|
|
149
|
+
(user_type (type_identifier) @ann (#eq? @ann "RequestMapping"))
|
|
150
|
+
(value_arguments
|
|
151
|
+
(value_argument
|
|
152
|
+
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
153
|
+
(string_literal) @prefix)))))
|
|
154
|
+
(type_identifier) @cls) @class
|
|
155
|
+
`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
// ─── Provider: Spring @(Get|Post|...)Mapping method annotations ───────
|
|
160
|
+
// Same dual-pattern positional/named approach. The Kotlin AST puts the
|
|
161
|
+
// function name (`simple_identifier`) outside the `modifiers` subtree,
|
|
162
|
+
// so we capture it from `function_declaration` directly.
|
|
163
|
+
const SPRING_METHOD_ROUTE_PATTERNS = compilePatterns({
|
|
164
|
+
name: 'kotlin-spring-method-route',
|
|
165
|
+
language,
|
|
166
|
+
patterns: [
|
|
167
|
+
{
|
|
168
|
+
meta: {},
|
|
169
|
+
query: `
|
|
170
|
+
(function_declaration
|
|
171
|
+
(modifiers
|
|
172
|
+
(annotation
|
|
173
|
+
(constructor_invocation
|
|
174
|
+
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Mapping$"))
|
|
175
|
+
(value_arguments
|
|
176
|
+
(value_argument . (string_literal) @path)))))
|
|
177
|
+
(simple_identifier) @method_name) @method
|
|
178
|
+
`,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
meta: {},
|
|
182
|
+
query: `
|
|
183
|
+
(function_declaration
|
|
184
|
+
(modifiers
|
|
185
|
+
(annotation
|
|
186
|
+
(constructor_invocation
|
|
187
|
+
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Mapping$"))
|
|
188
|
+
(value_arguments
|
|
189
|
+
(value_argument
|
|
190
|
+
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
191
|
+
(string_literal) @path)))))
|
|
192
|
+
(simple_identifier) @method_name) @method
|
|
193
|
+
`,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
// ─── Consumer: Spring RestTemplate ────────────────────────────────────
|
|
198
|
+
// Kotlin call-site shape mirrors the Java plugin's
|
|
199
|
+
// `REST_TEMPLATE_PATTERNS`, but goes through tree-sitter-kotlin's
|
|
200
|
+
// `navigation_expression` instead of Java's `method_invocation`:
|
|
201
|
+
//
|
|
202
|
+
// restTemplate.getForObject("/x", User::class.java)
|
|
203
|
+
//
|
|
204
|
+
// becomes
|
|
205
|
+
//
|
|
206
|
+
// call_expression
|
|
207
|
+
// navigation_expression
|
|
208
|
+
// simple_identifier "restTemplate"
|
|
209
|
+
// navigation_suffix → simple_identifier "getForObject"
|
|
210
|
+
// call_suffix
|
|
211
|
+
// value_arguments
|
|
212
|
+
// value_argument . string_literal "/x" ← captured
|
|
213
|
+
// value_argument User::class.java
|
|
214
|
+
//
|
|
215
|
+
// The receiver name is constrained to `restTemplate` (#eq? @obj),
|
|
216
|
+
// matching the Java plugin's heuristic. This means a non-conventional
|
|
217
|
+
// field name (e.g. `userServiceTemplate`) will not be picked up;
|
|
218
|
+
// that's the same trade-off already accepted on the Java side.
|
|
219
|
+
const REST_TEMPLATE_PATTERNS = compilePatterns({
|
|
220
|
+
name: 'kotlin-rest-template',
|
|
221
|
+
language,
|
|
222
|
+
patterns: [
|
|
223
|
+
{
|
|
224
|
+
meta: {},
|
|
225
|
+
query: `
|
|
226
|
+
(call_expression
|
|
227
|
+
(navigation_expression
|
|
228
|
+
(simple_identifier) @obj (#eq? @obj "restTemplate")
|
|
229
|
+
(navigation_suffix (simple_identifier) @method))
|
|
230
|
+
(call_suffix
|
|
231
|
+
(value_arguments . (value_argument . (string_literal) @path))))
|
|
232
|
+
`,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
// ─── Consumer: Spring WebClient (short form) ──────────────────────────
|
|
237
|
+
// Reactive WebClient exposes one-liner verb helpers:
|
|
238
|
+
//
|
|
239
|
+
// webClient.get().uri("/x").retrieve().awaitBody<T>()
|
|
240
|
+
// webClient.post().uri("/x")...
|
|
241
|
+
//
|
|
242
|
+
// The chain `webClient.get().uri("/x")` parses as two nested
|
|
243
|
+
// `call_expression` nodes — the OUTER call is `.uri("/x")` and the
|
|
244
|
+
// INNER call is `webClient.get()`. We anchor on the outer call and
|
|
245
|
+
// require:
|
|
246
|
+
// - inner receiver is `webClient`
|
|
247
|
+
// - inner suffix is one of the HTTP verbs (#match?)
|
|
248
|
+
// - outer suffix is exactly `uri`
|
|
249
|
+
// - outer call's first value_argument is a string literal
|
|
250
|
+
//
|
|
251
|
+
// The long-form `webClient.method(HttpMethod.GET).uri("/x")` chain
|
|
252
|
+
// uses an extra navigation hop and an enum field access — it's
|
|
253
|
+
// intentionally out of scope here (see file header).
|
|
254
|
+
const WEB_CLIENT_SHORT_PATTERNS = compilePatterns({
|
|
255
|
+
name: 'kotlin-web-client-short',
|
|
256
|
+
language,
|
|
257
|
+
patterns: [
|
|
258
|
+
{
|
|
259
|
+
meta: {},
|
|
260
|
+
query: `
|
|
261
|
+
(call_expression
|
|
262
|
+
(navigation_expression
|
|
263
|
+
(call_expression
|
|
264
|
+
(navigation_expression
|
|
265
|
+
(simple_identifier) @obj (#eq? @obj "webClient")
|
|
266
|
+
(navigation_suffix
|
|
267
|
+
(simple_identifier) @verb (#match? @verb "^(get|post|put|delete|patch)$")))
|
|
268
|
+
(call_suffix (value_arguments)))
|
|
269
|
+
(navigation_suffix (simple_identifier) @uri (#eq? @uri "uri")))
|
|
270
|
+
(call_suffix
|
|
271
|
+
(value_arguments . (value_argument . (string_literal) @path))))
|
|
272
|
+
`,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
});
|
|
276
|
+
// ─── Consumer: OkHttp Request.Builder().url("/x") ─────────────────────
|
|
277
|
+
// Kotlin parses `Request.Builder()` as a `call_expression` whose
|
|
278
|
+
// callee is a `navigation_expression` (Request → .Builder), NOT as
|
|
279
|
+
// Java's `object_creation_expression`. The chain `.url("/x")` then
|
|
280
|
+
// wraps that in another `call_expression`. The query mirrors Java's
|
|
281
|
+
// `OK_HTTP_PATTERNS` (java.ts) but adapts the node types.
|
|
282
|
+
//
|
|
283
|
+
// Receiver `Request` is constrained by name (#eq? @cls); a project
|
|
284
|
+
// that imports OkHttp's `Request` under an alias (`import okhttp3.Request as OkRequest`)
|
|
285
|
+
// would not be picked up — this matches the Java plugin's heuristic.
|
|
286
|
+
//
|
|
287
|
+
// **Known limitation — verb defaults to GET.** OkHttp encodes the
|
|
288
|
+
// verb on a *sibling* call further down the builder chain (e.g.
|
|
289
|
+
// `.post(body)` / `.get()` / `.delete()`), not on `.url(...)` itself.
|
|
290
|
+
// This query intentionally does not walk the chain to recover the
|
|
291
|
+
// verb — it emits `method: 'GET'` for every match, mirroring
|
|
292
|
+
// `java.ts:OK_HTTP_PATTERNS`. So a `Request.Builder().url("/x").post(body).build()`
|
|
293
|
+
// call becomes `http::GET::/x`, not `http::POST::/x`. This is the
|
|
294
|
+
// same trade-off Java has accepted; pinned by an anti-overreach
|
|
295
|
+
// test in `http-route-extractor.test.ts` so a future verb-walk
|
|
296
|
+
// implementation has to update this comment in lockstep.
|
|
297
|
+
const OK_HTTP_PATTERNS = compilePatterns({
|
|
298
|
+
name: 'kotlin-okhttp',
|
|
299
|
+
language,
|
|
300
|
+
patterns: [
|
|
301
|
+
{
|
|
302
|
+
meta: {},
|
|
303
|
+
query: `
|
|
304
|
+
(call_expression
|
|
305
|
+
(navigation_expression
|
|
306
|
+
(call_expression
|
|
307
|
+
(navigation_expression
|
|
308
|
+
(simple_identifier) @cls (#eq? @cls "Request")
|
|
309
|
+
(navigation_suffix (simple_identifier) @builder (#eq? @builder "Builder")))
|
|
310
|
+
(call_suffix (value_arguments)))
|
|
311
|
+
(navigation_suffix (simple_identifier) @method (#eq? @method "url")))
|
|
312
|
+
(call_suffix
|
|
313
|
+
(value_arguments . (value_argument . (string_literal) @path))))
|
|
314
|
+
`,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
/**
|
|
319
|
+
* Find the nearest enclosing class_declaration ancestor for a node, or
|
|
320
|
+
* null if the node is top-level. Mirrors the Java plugin's helper.
|
|
321
|
+
*/
|
|
322
|
+
function findEnclosingClass(node) {
|
|
323
|
+
let cur = node.parent;
|
|
324
|
+
while (cur) {
|
|
325
|
+
if (cur.type === 'class_declaration')
|
|
326
|
+
return cur;
|
|
327
|
+
cur = cur.parent;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Join a class-level prefix and a method-level path. Identical
|
|
333
|
+
* semantics to the Java plugin: strip leading/trailing slashes on
|
|
334
|
+
* the prefix, strip leading slashes on the method path, ensure a
|
|
335
|
+
* single slash between them.
|
|
336
|
+
*/
|
|
337
|
+
function joinPath(prefix, methodPath) {
|
|
338
|
+
const cleanPrefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
339
|
+
const cleanSub = methodPath.replace(/^\/+/, '');
|
|
340
|
+
if (!cleanPrefix)
|
|
341
|
+
return `/${cleanSub}`;
|
|
342
|
+
return `/${cleanPrefix}/${cleanSub}`;
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
name: 'kotlin-http',
|
|
346
|
+
language,
|
|
347
|
+
scan(tree) {
|
|
348
|
+
const out = [];
|
|
349
|
+
// ─── Class prefixes ─────────────────────────────────────────────
|
|
350
|
+
const prefixByClassId = new Map();
|
|
351
|
+
for (const match of runCompiledPatterns(SPRING_CLASS_PREFIX_PATTERNS, tree)) {
|
|
352
|
+
const prefixNode = match.captures.prefix;
|
|
353
|
+
const classNode = match.captures.class;
|
|
354
|
+
if (!prefixNode || !classNode)
|
|
355
|
+
continue;
|
|
356
|
+
const prefix = unquoteLiteral(prefixNode.text);
|
|
357
|
+
if (prefix !== null)
|
|
358
|
+
prefixByClassId.set(classNode.id, prefix);
|
|
359
|
+
}
|
|
360
|
+
// ─── Method routes ──────────────────────────────────────────────
|
|
361
|
+
for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
|
|
362
|
+
const annNode = match.captures.ann;
|
|
363
|
+
const pathNode = match.captures.path;
|
|
364
|
+
const nameNode = match.captures.method_name;
|
|
365
|
+
const methodNode = match.captures.method;
|
|
366
|
+
if (!annNode || !pathNode || !methodNode)
|
|
367
|
+
continue;
|
|
368
|
+
const httpMethod = METHOD_ANNOTATION_TO_HTTP[annNode.text];
|
|
369
|
+
if (!httpMethod)
|
|
370
|
+
continue;
|
|
371
|
+
const rawPath = unquoteLiteral(pathNode.text);
|
|
372
|
+
if (rawPath === null)
|
|
373
|
+
continue;
|
|
374
|
+
const enclosingClass = findEnclosingClass(methodNode);
|
|
375
|
+
const prefix = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? '') : '';
|
|
376
|
+
const fullPath = joinPath(prefix, rawPath);
|
|
377
|
+
out.push({
|
|
378
|
+
role: 'provider',
|
|
379
|
+
framework: 'spring',
|
|
380
|
+
method: httpMethod,
|
|
381
|
+
path: fullPath,
|
|
382
|
+
name: nameNode?.text ?? null,
|
|
383
|
+
confidence: 0.8,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
// ─── Consumers: RestTemplate ────────────────────────────────────
|
|
387
|
+
for (const match of runCompiledPatterns(REST_TEMPLATE_PATTERNS, tree)) {
|
|
388
|
+
const methodNode = match.captures.method;
|
|
389
|
+
const pathNode = match.captures.path;
|
|
390
|
+
if (!methodNode || !pathNode)
|
|
391
|
+
continue;
|
|
392
|
+
const httpMethod = REST_TEMPLATE_TO_HTTP[methodNode.text];
|
|
393
|
+
if (!httpMethod)
|
|
394
|
+
continue;
|
|
395
|
+
const path = unquoteLiteral(pathNode.text);
|
|
396
|
+
if (path === null)
|
|
397
|
+
continue;
|
|
398
|
+
out.push({
|
|
399
|
+
role: 'consumer',
|
|
400
|
+
framework: 'spring-rest-template',
|
|
401
|
+
method: httpMethod,
|
|
402
|
+
path,
|
|
403
|
+
name: null,
|
|
404
|
+
confidence: 0.7,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
// ─── Consumers: WebClient short form (.get()/.post()/etc → .uri) ─
|
|
408
|
+
for (const match of runCompiledPatterns(WEB_CLIENT_SHORT_PATTERNS, tree)) {
|
|
409
|
+
const verbNode = match.captures.verb;
|
|
410
|
+
const pathNode = match.captures.path;
|
|
411
|
+
if (!verbNode || !pathNode)
|
|
412
|
+
continue;
|
|
413
|
+
const httpMethod = WEB_CLIENT_SHORT_TO_HTTP[verbNode.text];
|
|
414
|
+
if (!httpMethod)
|
|
415
|
+
continue;
|
|
416
|
+
const path = unquoteLiteral(pathNode.text);
|
|
417
|
+
if (path === null)
|
|
418
|
+
continue;
|
|
419
|
+
out.push({
|
|
420
|
+
role: 'consumer',
|
|
421
|
+
framework: 'spring-web-client',
|
|
422
|
+
method: httpMethod,
|
|
423
|
+
path,
|
|
424
|
+
name: null,
|
|
425
|
+
confidence: 0.7,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
// ─── Consumers: OkHttp Request.Builder().url("path") ────────────
|
|
429
|
+
for (const match of runCompiledPatterns(OK_HTTP_PATTERNS, tree)) {
|
|
430
|
+
const pathNode = match.captures.path;
|
|
431
|
+
if (!pathNode)
|
|
432
|
+
continue;
|
|
433
|
+
const path = unquoteLiteral(pathNode.text);
|
|
434
|
+
if (path === null)
|
|
435
|
+
continue;
|
|
436
|
+
out.push({
|
|
437
|
+
role: 'consumer',
|
|
438
|
+
framework: 'okhttp',
|
|
439
|
+
method: 'GET',
|
|
440
|
+
path,
|
|
441
|
+
name: null,
|
|
442
|
+
confidence: 0.7,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The exported plugin is `null` when tree-sitter-kotlin's native
|
|
451
|
+
* binding is unavailable. `http-patterns/index.ts` checks for null
|
|
452
|
+
* before registering `.kt`/`.kts` so missing optional grammars never
|
|
453
|
+
* crash the orchestrator.
|
|
454
|
+
*/
|
|
455
|
+
export const KOTLIN_HTTP_PLUGIN = Kotlin
|
|
456
|
+
? buildKotlinPlugin(Kotlin)
|
|
457
|
+
: null;
|
package/package.json
CHANGED