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,5 +1,7 @@
|
|
|
1
1
|
import { requireVendoredGrammar } from '../../../tree-sitter/vendored-grammars.js';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
|
+
import { METHOD_ANNOTATION_TO_HTTP, 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';
|
|
3
5
|
/**
|
|
4
6
|
* Kotlin HTTP plugin (Spring providers + consumers).
|
|
5
7
|
*
|
|
@@ -65,50 +67,34 @@ try {
|
|
|
65
67
|
catch {
|
|
66
68
|
Kotlin = null;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
// The Spring `@(Get|...)Mapping` verb map, RestTemplate / WebClient short-form
|
|
71
|
+
// verb maps, the `@(Get|...)Exchange` verb map, `joinPath`, `parseRequestLine`,
|
|
72
|
+
// and the shared confidence/framework constants are imported from
|
|
73
|
+
// `spring-consumer-shared.ts` / `spring-shared.ts` so the Kotlin and Java
|
|
74
|
+
// plugins emit identical contract IDs.
|
|
75
|
+
// The WebClient long-form verb gate (`WEB_CLIENT_LONG_VERB_RE`) is imported from
|
|
76
|
+
// `spring-consumer-shared.ts` so the Java and Kotlin long-form scans accept the
|
|
77
|
+
// same verb set (HEAD/OPTIONS/TRACE excluded, matching the short form).
|
|
78
|
+
// The de-duping prefix accumulator (`pushPrefix`) is imported from
|
|
79
|
+
// `spring-consumer-shared.ts` so the Java and Kotlin plugins build their
|
|
80
|
+
// per-declaration prefix maps identically.
|
|
75
81
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
/**
|
|
90
|
-
* WebClient short-form verb → HTTP verb. The reactive WebClient API
|
|
91
|
-
* exposes `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()` as
|
|
92
|
-
* one-liners that return a `RequestHeadersUriSpec` whose `.uri(...)`
|
|
93
|
-
* carries the path. We capture both pieces in a single query (see
|
|
94
|
-
* `WEB_CLIENT_SHORT_PATTERNS` below) and translate the verb here.
|
|
95
|
-
*/
|
|
96
|
-
const WEB_CLIENT_SHORT_TO_HTTP = {
|
|
97
|
-
get: 'GET',
|
|
98
|
-
post: 'POST',
|
|
99
|
-
put: 'PUT',
|
|
100
|
-
delete: 'DELETE',
|
|
101
|
-
patch: 'PATCH',
|
|
102
|
-
};
|
|
103
|
-
/**
|
|
104
|
-
* Allowed HTTP verbs for the WebClient long-form path
|
|
105
|
-
* `webClient.method(HttpMethod.X).uri("/y")`. Compiled once at module
|
|
106
|
-
* load (instead of inside the scan loop) per maintainer feedback on
|
|
107
|
-
* PR #1884. Mirrors the keys of `WEB_CLIENT_SHORT_TO_HTTP` above —
|
|
108
|
-
* keeping HEAD/OPTIONS/TRACE intentionally excluded for symmetry
|
|
109
|
-
* with the short form and the Java plugin.
|
|
82
|
+
* Tree-sitter sub-pattern for the Kotlin `arrayOf("/a", "/b")` annotation-array
|
|
83
|
+
* form, capturing each element string under `cap` (`@prefix` or `@path`).
|
|
84
|
+
*
|
|
85
|
+
* Kept as a DEDICATED query fragment embedded in its own pattern — NEVER as an
|
|
86
|
+
* arm of the `[(string_literal) (collection_literal …)]` alternation. The
|
|
87
|
+
* `#eq? @arrayOf "arrayOf"` predicate, sharing a single alternation bucket with
|
|
88
|
+
* the string/collection arms, would evaluate FALSE for those arms (where
|
|
89
|
+
* `@arrayOf` is absent) and silently drop them — the tree-sitter 0.21.x hazard
|
|
90
|
+
* documented in `java.ts`. tree-sitter yields one match per `arrayOf` element,
|
|
91
|
+
* so multi-element arrays accumulate through the same loops as `collection_literal`
|
|
92
|
+
* (verified by AST probe). The `arrayOf` callee constraint keeps unrelated calls
|
|
93
|
+
* (`buildPath("/x")`) from matching.
|
|
110
94
|
*/
|
|
111
|
-
const
|
|
95
|
+
const arrayOfArg = (cap) => `(call_expression
|
|
96
|
+
(simple_identifier) @arrayOf (#eq? @arrayOf "arrayOf")
|
|
97
|
+
(call_suffix (value_arguments (value_argument (string_literal) ${cap}))))`;
|
|
112
98
|
/**
|
|
113
99
|
* Build the plugin only if the Kotlin grammar is available. Compiling
|
|
114
100
|
* the queries against a null grammar would throw at module load time
|
|
@@ -148,7 +134,7 @@ function buildKotlinPlugin(language) {
|
|
|
148
134
|
(constructor_invocation
|
|
149
135
|
(user_type (type_identifier) @ann (#eq? @ann "RequestMapping"))
|
|
150
136
|
(value_arguments
|
|
151
|
-
(value_argument . (string_literal) @prefix)))))
|
|
137
|
+
(value_argument . [(string_literal) @prefix (collection_literal (string_literal) @prefix)])))))
|
|
152
138
|
(type_identifier) @cls) @class
|
|
153
139
|
`,
|
|
154
140
|
},
|
|
@@ -163,7 +149,35 @@ function buildKotlinPlugin(language) {
|
|
|
163
149
|
(value_arguments
|
|
164
150
|
(value_argument
|
|
165
151
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
166
|
-
(string_literal) @prefix)))))
|
|
152
|
+
[(string_literal) @prefix (collection_literal (string_literal) @prefix)])))))
|
|
153
|
+
(type_identifier) @cls) @class
|
|
154
|
+
`,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
meta: {},
|
|
158
|
+
query: `
|
|
159
|
+
(class_declaration
|
|
160
|
+
(modifiers
|
|
161
|
+
(annotation
|
|
162
|
+
(constructor_invocation
|
|
163
|
+
(user_type (type_identifier) @ann (#eq? @ann "RequestMapping"))
|
|
164
|
+
(value_arguments
|
|
165
|
+
(value_argument . ${arrayOfArg('@prefix')})))))
|
|
166
|
+
(type_identifier) @cls) @class
|
|
167
|
+
`,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
meta: {},
|
|
171
|
+
query: `
|
|
172
|
+
(class_declaration
|
|
173
|
+
(modifiers
|
|
174
|
+
(annotation
|
|
175
|
+
(constructor_invocation
|
|
176
|
+
(user_type (type_identifier) @ann (#eq? @ann "RequestMapping"))
|
|
177
|
+
(value_arguments
|
|
178
|
+
(value_argument
|
|
179
|
+
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
180
|
+
${arrayOfArg('@prefix')})))))
|
|
167
181
|
(type_identifier) @cls) @class
|
|
168
182
|
`,
|
|
169
183
|
},
|
|
@@ -186,7 +200,35 @@ function buildKotlinPlugin(language) {
|
|
|
186
200
|
(constructor_invocation
|
|
187
201
|
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Mapping$"))
|
|
188
202
|
(value_arguments
|
|
189
|
-
(value_argument . (string_literal) @path)))))
|
|
203
|
+
(value_argument . [(string_literal) @path (collection_literal (string_literal) @path)])))))
|
|
204
|
+
(simple_identifier) @method_name) @method
|
|
205
|
+
`,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
meta: {},
|
|
209
|
+
query: `
|
|
210
|
+
(function_declaration
|
|
211
|
+
(modifiers
|
|
212
|
+
(annotation
|
|
213
|
+
(constructor_invocation
|
|
214
|
+
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Mapping$"))
|
|
215
|
+
(value_arguments
|
|
216
|
+
(value_argument
|
|
217
|
+
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
218
|
+
[(string_literal) @path (collection_literal (string_literal) @path)])))))
|
|
219
|
+
(simple_identifier) @method_name) @method
|
|
220
|
+
`,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
meta: {},
|
|
224
|
+
query: `
|
|
225
|
+
(function_declaration
|
|
226
|
+
(modifiers
|
|
227
|
+
(annotation
|
|
228
|
+
(constructor_invocation
|
|
229
|
+
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Mapping$"))
|
|
230
|
+
(value_arguments
|
|
231
|
+
(value_argument . ${arrayOfArg('@path')})))))
|
|
190
232
|
(simple_identifier) @method_name) @method
|
|
191
233
|
`,
|
|
192
234
|
},
|
|
@@ -201,7 +243,7 @@ function buildKotlinPlugin(language) {
|
|
|
201
243
|
(value_arguments
|
|
202
244
|
(value_argument
|
|
203
245
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
204
|
-
(
|
|
246
|
+
${arrayOfArg('@path')})))))
|
|
205
247
|
(simple_identifier) @method_name) @method
|
|
206
248
|
`,
|
|
207
249
|
},
|
|
@@ -381,32 +423,355 @@ function buildKotlinPlugin(language) {
|
|
|
381
423
|
},
|
|
382
424
|
],
|
|
383
425
|
});
|
|
426
|
+
// ─── Consumer (OpenFeign): @FeignClient interface marker + path prefix ─
|
|
427
|
+
// A `@FeignClient` interface's `@(Get|...)Mapping` methods describe OUTBOUND
|
|
428
|
+
// calls (consumers), not routes the service serves. Pattern 1 marks the
|
|
429
|
+
// interface; pattern 2 captures its optional `path = "/prefix"` (the
|
|
430
|
+
// `name`/`value`/`url` attributes identify the remote service, not a path).
|
|
431
|
+
// In tree-sitter-kotlin an `interface` is a `class_declaration`, so the
|
|
432
|
+
// method-route loop reclassifies @*Mapping methods whose enclosing
|
|
433
|
+
// class_declaration is in `feignClassIds` (see scan()).
|
|
434
|
+
const SPRING_FEIGN_CLIENT_PATTERNS = compilePatterns({
|
|
435
|
+
name: 'kotlin-spring-feign-client',
|
|
436
|
+
language,
|
|
437
|
+
patterns: [
|
|
438
|
+
{
|
|
439
|
+
meta: {},
|
|
440
|
+
query: `
|
|
441
|
+
(class_declaration
|
|
442
|
+
(modifiers
|
|
443
|
+
(annotation
|
|
444
|
+
(constructor_invocation
|
|
445
|
+
(user_type (type_identifier) @ann (#eq? @ann "FeignClient")))))) @class
|
|
446
|
+
`,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
meta: {},
|
|
450
|
+
query: `
|
|
451
|
+
(class_declaration
|
|
452
|
+
(modifiers
|
|
453
|
+
(annotation
|
|
454
|
+
(constructor_invocation
|
|
455
|
+
(user_type (type_identifier) @ann (#eq? @ann "FeignClient"))
|
|
456
|
+
(value_arguments
|
|
457
|
+
(value_argument
|
|
458
|
+
(simple_identifier) @key (#eq? @key "path")
|
|
459
|
+
[(string_literal) @prefix (collection_literal (string_literal) @prefix)])))))) @class
|
|
460
|
+
`,
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
meta: {},
|
|
464
|
+
query: `
|
|
465
|
+
(class_declaration
|
|
466
|
+
(modifiers
|
|
467
|
+
(annotation
|
|
468
|
+
(constructor_invocation
|
|
469
|
+
(user_type (type_identifier) @ann (#eq? @ann "FeignClient"))
|
|
470
|
+
(value_arguments
|
|
471
|
+
(value_argument
|
|
472
|
+
(simple_identifier) @key (#eq? @key "path")
|
|
473
|
+
${arrayOfArg('@prefix')})))))) @class
|
|
474
|
+
`,
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
});
|
|
478
|
+
// ─── Consumer: Spring 6 HTTP Interface @(Get|...)Exchange ─────────────
|
|
479
|
+
// Declarative client interfaces proxied by HttpServiceProxyFactory (over
|
|
480
|
+
// RestClient / WebClient / RestTemplate). The path lives in `url`/`value`
|
|
481
|
+
// (named) or positionally. Always a consumer — no provider ambiguity.
|
|
482
|
+
const SPRING_EXCHANGE_PATTERNS = compilePatterns({
|
|
483
|
+
name: 'kotlin-spring-http-exchange',
|
|
484
|
+
language,
|
|
485
|
+
patterns: [
|
|
486
|
+
{
|
|
487
|
+
meta: {},
|
|
488
|
+
query: `
|
|
489
|
+
(function_declaration
|
|
490
|
+
(modifiers
|
|
491
|
+
(annotation
|
|
492
|
+
(constructor_invocation
|
|
493
|
+
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Exchange$"))
|
|
494
|
+
(value_arguments
|
|
495
|
+
(value_argument . [(string_literal) @path (collection_literal (string_literal) @path)])))))
|
|
496
|
+
(simple_identifier) @method_name) @method
|
|
497
|
+
`,
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
meta: {},
|
|
501
|
+
query: `
|
|
502
|
+
(function_declaration
|
|
503
|
+
(modifiers
|
|
504
|
+
(annotation
|
|
505
|
+
(constructor_invocation
|
|
506
|
+
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Exchange$"))
|
|
507
|
+
(value_arguments
|
|
508
|
+
(value_argument
|
|
509
|
+
(simple_identifier) @key (#match? @key "^(url|value)$")
|
|
510
|
+
[(string_literal) @path (collection_literal (string_literal) @path)])))))
|
|
511
|
+
(simple_identifier) @method_name) @method
|
|
512
|
+
`,
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
meta: {},
|
|
516
|
+
query: `
|
|
517
|
+
(function_declaration
|
|
518
|
+
(modifiers
|
|
519
|
+
(annotation
|
|
520
|
+
(constructor_invocation
|
|
521
|
+
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Exchange$"))
|
|
522
|
+
(value_arguments
|
|
523
|
+
(value_argument . ${arrayOfArg('@path')})))))
|
|
524
|
+
(simple_identifier) @method_name) @method
|
|
525
|
+
`,
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
meta: {},
|
|
529
|
+
query: `
|
|
530
|
+
(function_declaration
|
|
531
|
+
(modifiers
|
|
532
|
+
(annotation
|
|
533
|
+
(constructor_invocation
|
|
534
|
+
(user_type (type_identifier) @ann (#match? @ann "^(Get|Post|Put|Delete|Patch)Exchange$"))
|
|
535
|
+
(value_arguments
|
|
536
|
+
(value_argument
|
|
537
|
+
(simple_identifier) @key (#match? @key "^(url|value)$")
|
|
538
|
+
${arrayOfArg('@path')})))))
|
|
539
|
+
(simple_identifier) @method_name) @method
|
|
540
|
+
`,
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
});
|
|
544
|
+
// ─── Consumer: HTTP Interface type-level @HttpExchange(url) prefix ─────
|
|
545
|
+
const SPRING_HTTP_EXCHANGE_CLASS_PATTERNS = compilePatterns({
|
|
546
|
+
name: 'kotlin-spring-http-exchange-class',
|
|
547
|
+
language,
|
|
548
|
+
patterns: [
|
|
549
|
+
{
|
|
550
|
+
meta: {},
|
|
551
|
+
query: `
|
|
552
|
+
(class_declaration
|
|
553
|
+
(modifiers
|
|
554
|
+
(annotation
|
|
555
|
+
(constructor_invocation
|
|
556
|
+
(user_type (type_identifier) @ann (#eq? @ann "HttpExchange"))
|
|
557
|
+
(value_arguments
|
|
558
|
+
(value_argument . [(string_literal) @prefix (collection_literal (string_literal) @prefix)])))))) @class
|
|
559
|
+
`,
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
meta: {},
|
|
563
|
+
query: `
|
|
564
|
+
(class_declaration
|
|
565
|
+
(modifiers
|
|
566
|
+
(annotation
|
|
567
|
+
(constructor_invocation
|
|
568
|
+
(user_type (type_identifier) @ann (#eq? @ann "HttpExchange"))
|
|
569
|
+
(value_arguments
|
|
570
|
+
(value_argument
|
|
571
|
+
(simple_identifier) @key (#match? @key "^(url|value)$")
|
|
572
|
+
[(string_literal) @prefix (collection_literal (string_literal) @prefix)])))))) @class
|
|
573
|
+
`,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
meta: {},
|
|
577
|
+
query: `
|
|
578
|
+
(class_declaration
|
|
579
|
+
(modifiers
|
|
580
|
+
(annotation
|
|
581
|
+
(constructor_invocation
|
|
582
|
+
(user_type (type_identifier) @ann (#eq? @ann "HttpExchange"))
|
|
583
|
+
(value_arguments
|
|
584
|
+
(value_argument . ${arrayOfArg('@prefix')})))))) @class
|
|
585
|
+
`,
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
meta: {},
|
|
589
|
+
query: `
|
|
590
|
+
(class_declaration
|
|
591
|
+
(modifiers
|
|
592
|
+
(annotation
|
|
593
|
+
(constructor_invocation
|
|
594
|
+
(user_type (type_identifier) @ann (#eq? @ann "HttpExchange"))
|
|
595
|
+
(value_arguments
|
|
596
|
+
(value_argument
|
|
597
|
+
(simple_identifier) @key (#match? @key "^(url|value)$")
|
|
598
|
+
${arrayOfArg('@prefix')})))))) @class
|
|
599
|
+
`,
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
// ─── Consumer: OpenFeign native @RequestLine("VERB /path") ────────────
|
|
604
|
+
// Two patterns mirror the positional vs named split. java.ts accepts the
|
|
605
|
+
// named `value` argument (java.ts:442 drops any non-`value` key); the
|
|
606
|
+
// positional pattern's `.` anchor only matches when the string literal is the
|
|
607
|
+
// first argument, so the named form needs its own pattern. Constraining
|
|
608
|
+
// `#eq? @key "value"` keeps non-`value` keys (`name`, etc.) dropped — Java
|
|
609
|
+
// parity, just enforced in the query rather than the JS loop.
|
|
610
|
+
const SPRING_REQUEST_LINE_PATTERNS = compilePatterns({
|
|
611
|
+
name: 'kotlin-spring-request-line',
|
|
612
|
+
language,
|
|
613
|
+
patterns: [
|
|
614
|
+
{
|
|
615
|
+
meta: {},
|
|
616
|
+
query: `
|
|
617
|
+
(function_declaration
|
|
618
|
+
(modifiers
|
|
619
|
+
(annotation
|
|
620
|
+
(constructor_invocation
|
|
621
|
+
(user_type (type_identifier) @ann (#eq? @ann "RequestLine"))
|
|
622
|
+
(value_arguments
|
|
623
|
+
(value_argument . (string_literal) @value)))))
|
|
624
|
+
(simple_identifier) @method_name) @method
|
|
625
|
+
`,
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
meta: {},
|
|
629
|
+
query: `
|
|
630
|
+
(function_declaration
|
|
631
|
+
(modifiers
|
|
632
|
+
(annotation
|
|
633
|
+
(constructor_invocation
|
|
634
|
+
(user_type (type_identifier) @ann (#eq? @ann "RequestLine"))
|
|
635
|
+
(value_arguments
|
|
636
|
+
(value_argument
|
|
637
|
+
(simple_identifier) @key (#eq? @key "value")
|
|
638
|
+
(string_literal) @value)))))
|
|
639
|
+
(simple_identifier) @method_name) @method
|
|
640
|
+
`,
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
});
|
|
644
|
+
// ─── Provider via interface inheritance (Spring interface-based controller) ─
|
|
645
|
+
// Pattern: `@RestController class X(...) : XApi` where the route annotations
|
|
646
|
+
// live on the `XApi` interface and the controller's `override fun` carries
|
|
647
|
+
// none. Java resolves this in `scanProject` (scanSpringProject); this is the
|
|
648
|
+
// Kotlin port. tree-sitter-kotlin models BOTH class and interface as
|
|
649
|
+
// `class_declaration`; the `interface` keyword token distinguishes them.
|
|
650
|
+
const KOTLIN_TYPE_DECLARATION_PATTERNS = compilePatterns({
|
|
651
|
+
name: 'kotlin-type-declaration',
|
|
652
|
+
language,
|
|
653
|
+
patterns: [{ meta: {}, query: `(class_declaration (type_identifier) @name) @type` }],
|
|
654
|
+
});
|
|
655
|
+
/** A `class_declaration` is an interface when it carries the `interface` keyword token. */
|
|
656
|
+
const isKotlinInterface = (node) => node.children.some((c) => c.type === 'interface');
|
|
657
|
+
/** Resolve an `annotation` node's simple name: `@Foo` / `@Foo(...)` / `@a.b.Foo` → "Foo". */
|
|
658
|
+
const kotlinAnnotationName = (annotation) => {
|
|
659
|
+
const direct = annotation.namedChildren.find((c) => c.type === 'user_type');
|
|
660
|
+
const ctor = annotation.namedChildren.find((c) => c.type === 'constructor_invocation');
|
|
661
|
+
const userType = direct ?? ctor?.namedChildren.find((c) => c.type === 'user_type');
|
|
662
|
+
// A fully-qualified annotation (`@a.b.Foo`) parses to a `user_type` carrying
|
|
663
|
+
// one `type_identifier` per dotted segment (`a`, `b`, `Foo`); the trailing
|
|
664
|
+
// one is the simple name. Taking the FIRST would resolve `@org…RestController`
|
|
665
|
+
// to "org" and miss the controller.
|
|
666
|
+
const idents = userType?.namedChildren.filter((c) => c.type === 'type_identifier') ?? [];
|
|
667
|
+
const ident = idents.at(-1);
|
|
668
|
+
return ident ? ident.text : null;
|
|
669
|
+
};
|
|
384
670
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
671
|
+
* Whether a `class_declaration` is a Spring `@RestController` / `@Controller`.
|
|
672
|
+
* All forms attach under `modifiers` as an `annotation` (confirmed against
|
|
673
|
+
* tree-sitter-kotlin fwcd): the bare `@RestController`, the common
|
|
674
|
+
* `@RestController @RequestMapping("/x")` pair, AND the arg-form
|
|
675
|
+
* `@RestController("beanName")` — the last parses to an `annotation` whose
|
|
676
|
+
* child is a `constructor_invocation` (NOT a detached sibling), which
|
|
677
|
+
* `kotlinAnnotationName` reads. A single pass over `modifiers` covers them all.
|
|
387
678
|
*/
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
679
|
+
const CONTROLLER_ANNOTATIONS = new Set(['RestController', 'Controller']);
|
|
680
|
+
const kotlinClassIsController = (typeNode) => {
|
|
681
|
+
const modifiers = typeNode.namedChildren.find((c) => c.type === 'modifiers');
|
|
682
|
+
for (const ann of modifiers?.namedChildren ?? []) {
|
|
683
|
+
if (ann.type !== 'annotation')
|
|
684
|
+
continue;
|
|
685
|
+
const name = kotlinAnnotationName(ann);
|
|
686
|
+
if (name && CONTROLLER_ANNOTATIONS.has(name))
|
|
687
|
+
return true;
|
|
394
688
|
}
|
|
395
|
-
return
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
689
|
+
return false;
|
|
690
|
+
};
|
|
691
|
+
/** Supertype names from `: A, B` (`delegation_specifier` → `user_type` → `type_identifier`). */
|
|
692
|
+
const collectKotlinSupertypes = (node) => {
|
|
693
|
+
const out = [];
|
|
694
|
+
for (const child of node.namedChildren) {
|
|
695
|
+
if (child.type !== 'delegation_specifier')
|
|
696
|
+
continue;
|
|
697
|
+
const userType = child.namedChildren.find((c) => c.type === 'user_type');
|
|
698
|
+
// FQN supertype (`: a.b.Api`) → one `type_identifier` per segment; the
|
|
699
|
+
// trailing one is the simple name (taking the first would yield "a").
|
|
700
|
+
const idents = userType?.namedChildren.filter((c) => c.type === 'type_identifier') ?? [];
|
|
701
|
+
const ident = idents.at(-1);
|
|
702
|
+
if (ident)
|
|
703
|
+
out.push(ident.text);
|
|
704
|
+
}
|
|
705
|
+
return out;
|
|
706
|
+
};
|
|
707
|
+
/** Direct `function_declaration` members of a type (no descent into nested types). */
|
|
708
|
+
const collectKotlinDirectMethods = (typeNode) => {
|
|
709
|
+
const body = typeNode.namedChildren.find((c) => c.type === 'class_body');
|
|
710
|
+
if (!body)
|
|
711
|
+
return [];
|
|
712
|
+
return body.namedChildren.filter((c) => c.type === 'function_declaration');
|
|
713
|
+
};
|
|
714
|
+
const kotlinFunctionName = (fn) => fn.namedChildren.find((c) => c.type === 'simple_identifier')?.text ?? null;
|
|
715
|
+
const collectKotlinSpringTypes = (filePath, tree) => {
|
|
716
|
+
// Class-level @RequestMapping prefixes (reuse the provider class-prefix query).
|
|
717
|
+
const prefixByClassId = new Map();
|
|
718
|
+
for (const match of runCompiledPatterns(SPRING_CLASS_PREFIX_PATTERNS, tree)) {
|
|
719
|
+
const prefixNode = match.captures.prefix;
|
|
720
|
+
const classNode = match.captures.class;
|
|
721
|
+
if (!prefixNode || !classNode)
|
|
722
|
+
continue;
|
|
723
|
+
const prefix = unquoteLiteral(prefixNode.text);
|
|
724
|
+
if (prefix !== null)
|
|
725
|
+
pushPrefix(prefixByClassId, classNode.id, prefix);
|
|
726
|
+
}
|
|
727
|
+
// Method @(Get|...)Mapping routes keyed by the function_declaration node id.
|
|
728
|
+
const routesByMethodId = new Map();
|
|
729
|
+
for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
|
|
730
|
+
const annNode = match.captures.ann;
|
|
731
|
+
const pathNode = match.captures.path;
|
|
732
|
+
const methodNode = match.captures.method;
|
|
733
|
+
if (!annNode || !pathNode || !methodNode)
|
|
734
|
+
continue;
|
|
735
|
+
const httpMethod = METHOD_ANNOTATION_TO_HTTP[annNode.text];
|
|
736
|
+
if (!httpMethod)
|
|
737
|
+
continue;
|
|
738
|
+
const rawPath = unquoteLiteral(pathNode.text);
|
|
739
|
+
if (rawPath === null)
|
|
740
|
+
continue;
|
|
741
|
+
const arr = routesByMethodId.get(methodNode.id) ?? [];
|
|
742
|
+
arr.push({ method: httpMethod, path: rawPath });
|
|
743
|
+
routesByMethodId.set(methodNode.id, arr);
|
|
744
|
+
}
|
|
745
|
+
const out = [];
|
|
746
|
+
for (const match of runCompiledPatterns(KOTLIN_TYPE_DECLARATION_PATTERNS, tree)) {
|
|
747
|
+
const typeNode = match.captures.type;
|
|
748
|
+
const nameNode = match.captures.name;
|
|
749
|
+
if (!typeNode || !nameNode)
|
|
750
|
+
continue;
|
|
751
|
+
const kind = isKotlinInterface(typeNode) ? 'interface' : 'class';
|
|
752
|
+
const methods = collectKotlinDirectMethods(typeNode)
|
|
753
|
+
.map((fn) => ({ name: kotlinFunctionName(fn), routes: routesByMethodId.get(fn.id) ?? [] }))
|
|
754
|
+
.filter((m) => {
|
|
755
|
+
return m.name !== null;
|
|
756
|
+
});
|
|
757
|
+
out.push({
|
|
758
|
+
filePath,
|
|
759
|
+
kind,
|
|
760
|
+
name: nameNode.text,
|
|
761
|
+
isController: kind === 'class' ? kotlinClassIsController(typeNode) : false,
|
|
762
|
+
classPrefixes: prefixByClassId.get(typeNode.id) ?? [],
|
|
763
|
+
implementedInterfaces: kind === 'class' ? collectKotlinSupertypes(typeNode) : [],
|
|
764
|
+
methods,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
return out;
|
|
768
|
+
};
|
|
769
|
+
// The interface-based-controller inheritance algorithm is shared with java.ts
|
|
770
|
+
// (`scanSpringInheritanceProject`); this collects the language-specific
|
|
771
|
+
// `SharedSpringType` view and delegates. kotlinClassIsController handles every
|
|
772
|
+
// controller form (bare, paired, and the arg-form `@RestController("bean")`)
|
|
773
|
+
// via the `modifiers` `annotation`/`constructor_invocation` shape.
|
|
774
|
+
const scanKotlinProject = (files) => scanSpringInheritanceProject(files.flatMap((f) => collectKotlinSpringTypes(f.filePath, f.tree)));
|
|
410
775
|
return {
|
|
411
776
|
name: 'kotlin-http',
|
|
412
777
|
language,
|
|
@@ -421,9 +786,38 @@ function buildKotlinPlugin(language) {
|
|
|
421
786
|
continue;
|
|
422
787
|
const prefix = unquoteLiteral(prefixNode.text);
|
|
423
788
|
if (prefix !== null)
|
|
424
|
-
prefixByClassId
|
|
789
|
+
pushPrefix(prefixByClassId, classNode.id, prefix);
|
|
425
790
|
}
|
|
426
|
-
// ───
|
|
791
|
+
// ─── OpenFeign client interfaces + HTTP Interface type prefixes ──
|
|
792
|
+
// In tree-sitter-kotlin an `interface` is a `class_declaration`, so a
|
|
793
|
+
// `@FeignClient` interface's @(Get|...)Mapping methods would otherwise be
|
|
794
|
+
// mis-emitted as providers. Collect the FeignClient class ids (and their
|
|
795
|
+
// optional `path` prefix) so the method-route loop can reclassify them.
|
|
796
|
+
const feignClassIds = new Set();
|
|
797
|
+
const feignPrefixByClassId = new Map();
|
|
798
|
+
for (const match of runCompiledPatterns(SPRING_FEIGN_CLIENT_PATTERNS, tree)) {
|
|
799
|
+
const classNode = match.captures.class;
|
|
800
|
+
if (!classNode)
|
|
801
|
+
continue;
|
|
802
|
+
feignClassIds.add(classNode.id);
|
|
803
|
+
const prefixNode = match.captures.prefix;
|
|
804
|
+
if (prefixNode) {
|
|
805
|
+
const prefix = unquoteLiteral(prefixNode.text);
|
|
806
|
+
if (prefix !== null)
|
|
807
|
+
pushPrefix(feignPrefixByClassId, classNode.id, prefix);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const httpExchangePrefixByClassId = new Map();
|
|
811
|
+
for (const match of runCompiledPatterns(SPRING_HTTP_EXCHANGE_CLASS_PATTERNS, tree)) {
|
|
812
|
+
const classNode = match.captures.class;
|
|
813
|
+
const prefixNode = match.captures.prefix;
|
|
814
|
+
if (!classNode || !prefixNode)
|
|
815
|
+
continue;
|
|
816
|
+
const prefix = unquoteLiteral(prefixNode.text);
|
|
817
|
+
if (prefix !== null)
|
|
818
|
+
pushPrefix(httpExchangePrefixByClassId, classNode.id, prefix);
|
|
819
|
+
}
|
|
820
|
+
// ─── Method routes (Spring providers) + OpenFeign consumers ─────
|
|
427
821
|
for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
|
|
428
822
|
const annNode = match.captures.ann;
|
|
429
823
|
const pathNode = match.captures.path;
|
|
@@ -438,16 +832,46 @@ function buildKotlinPlugin(language) {
|
|
|
438
832
|
if (rawPath === null)
|
|
439
833
|
continue;
|
|
440
834
|
const enclosingClass = findEnclosingClass(methodNode);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
835
|
+
// A @(Get|...)Mapping inside a @FeignClient interface is an OpenFeign
|
|
836
|
+
// consumer (a remote call), not a route this service serves.
|
|
837
|
+
if (enclosingClass && feignClassIds.has(enclosingClass.id)) {
|
|
838
|
+
// @FeignClient(path) wins over @RequestMapping; a multi-element prefix
|
|
839
|
+
// yields one consumer per (prefix × this route).
|
|
840
|
+
const prefixes = feignPrefixByClassId.get(enclosingClass.id) ??
|
|
841
|
+
prefixByClassId.get(enclosingClass.id) ?? [''];
|
|
842
|
+
for (const prefix of prefixes) {
|
|
843
|
+
out.push({
|
|
844
|
+
role: 'consumer',
|
|
845
|
+
framework: OPENFEIGN_FRAMEWORK,
|
|
846
|
+
method: httpMethod,
|
|
847
|
+
path: joinPath(prefix, rawPath),
|
|
848
|
+
name: nameNode?.text ?? null,
|
|
849
|
+
confidence: FEIGN_CONFIDENCE,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
// A @(Get|...)Mapping on a (non-Feign) interface declares a route
|
|
855
|
+
// *contract*, not a route this service serves — the implementing
|
|
856
|
+
// @RestController is the provider, emitted via scanProject's interface
|
|
857
|
+
// inheritance. Java drops these implicitly (findEnclosingClass returns
|
|
858
|
+
// null for an interface_declaration); tree-sitter-kotlin models an
|
|
859
|
+
// interface as a class_declaration, so skip it explicitly here.
|
|
860
|
+
if (enclosingClass && isKotlinInterface(enclosingClass))
|
|
861
|
+
continue;
|
|
862
|
+
// A multi-element class `@RequestMapping(["/a","/b"])` registers the method
|
|
863
|
+
// under each prefix — emit one provider per (prefix × this route).
|
|
864
|
+
const prefixes = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? ['']) : [''];
|
|
865
|
+
for (const prefix of prefixes) {
|
|
866
|
+
out.push({
|
|
867
|
+
role: 'provider',
|
|
868
|
+
framework: 'spring',
|
|
869
|
+
method: httpMethod,
|
|
870
|
+
path: joinPath(prefix, rawPath),
|
|
871
|
+
name: nameNode?.text ?? null,
|
|
872
|
+
confidence: 0.8,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
451
875
|
}
|
|
452
876
|
// ─── Consumers: RestTemplate ────────────────────────────────────
|
|
453
877
|
for (const match of runCompiledPatterns(REST_TEMPLATE_PATTERNS, tree)) {
|
|
@@ -537,8 +961,78 @@ function buildKotlinPlugin(language) {
|
|
|
537
961
|
confidence: 0.7,
|
|
538
962
|
});
|
|
539
963
|
}
|
|
964
|
+
// ─── Consumers: Spring HTTP Interface @(Get|...)Exchange ────────
|
|
965
|
+
for (const match of runCompiledPatterns(SPRING_EXCHANGE_PATTERNS, tree)) {
|
|
966
|
+
const annNode = match.captures.ann;
|
|
967
|
+
const pathNode = match.captures.path;
|
|
968
|
+
const nameNode = match.captures.method_name;
|
|
969
|
+
const methodNode = match.captures.method;
|
|
970
|
+
if (!annNode || !pathNode || !methodNode)
|
|
971
|
+
continue;
|
|
972
|
+
const httpMethod = EXCHANGE_ANNOTATION_TO_HTTP[annNode.text];
|
|
973
|
+
if (!httpMethod)
|
|
974
|
+
continue;
|
|
975
|
+
const rawPath = unquoteLiteral(pathNode.text);
|
|
976
|
+
if (rawPath === null)
|
|
977
|
+
continue;
|
|
978
|
+
const enclosingClass = findEnclosingClass(methodNode);
|
|
979
|
+
const prefixes = enclosingClass
|
|
980
|
+
? (httpExchangePrefixByClassId.get(enclosingClass.id) ?? [''])
|
|
981
|
+
: [''];
|
|
982
|
+
for (const prefix of prefixes) {
|
|
983
|
+
out.push({
|
|
984
|
+
role: 'consumer',
|
|
985
|
+
framework: HTTP_INTERFACE_FRAMEWORK,
|
|
986
|
+
method: httpMethod,
|
|
987
|
+
path: joinPath(prefix, rawPath),
|
|
988
|
+
name: nameNode?.text ?? null,
|
|
989
|
+
confidence: EXCHANGE_CONFIDENCE,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// ─── Consumers: OpenFeign native @RequestLine("VERB /path") ─────
|
|
994
|
+
// Method-level only and always declared on an interface — Feign builds its
|
|
995
|
+
// proxy from the interface, so a `@RequestLine` on a concrete class is not
|
|
996
|
+
// a client call. We do NOT require an enclosing `@FeignClient` (core Feign
|
|
997
|
+
// uses `@RequestLine` with `Feign.builder()`, not Spring Cloud's
|
|
998
|
+
// `@FeignClient`); the `RequestLine` name plus the structural interface
|
|
999
|
+
// check keep false positives away. Mirrors java.ts's `findEnclosingInterface`
|
|
1000
|
+
// gate — in tree-sitter-kotlin an interface is a `class_declaration`, so we
|
|
1001
|
+
// test the `interface` keyword via isKotlinInterface.
|
|
1002
|
+
for (const match of runCompiledPatterns(SPRING_REQUEST_LINE_PATTERNS, tree)) {
|
|
1003
|
+
const valueNode = match.captures.value;
|
|
1004
|
+
const nameNode = match.captures.method_name;
|
|
1005
|
+
const methodNode = match.captures.method;
|
|
1006
|
+
if (!valueNode || !methodNode)
|
|
1007
|
+
continue;
|
|
1008
|
+
const raw = unquoteLiteral(valueNode.text);
|
|
1009
|
+
const parsed = raw !== null ? parseRequestLine(raw) : null;
|
|
1010
|
+
if (!parsed)
|
|
1011
|
+
continue;
|
|
1012
|
+
const enclosingClass = findEnclosingClass(methodNode);
|
|
1013
|
+
if (!enclosingClass || !isKotlinInterface(enclosingClass))
|
|
1014
|
+
continue;
|
|
1015
|
+
// Mirror java.ts (which pre-merges the @RequestMapping fallback into
|
|
1016
|
+
// feignPrefixByInterfaceId, "path wins"): @FeignClient(path) wins, else
|
|
1017
|
+
// the interface's class-level @RequestMapping prefix, else none. Without
|
|
1018
|
+
// the prefixByClassId fallback Kotlin dropped the class prefix that Java
|
|
1019
|
+
// applies — the same fallback chain the @GetMapping-in-Feign path uses above.
|
|
1020
|
+
const prefixes = feignPrefixByClassId.get(enclosingClass.id) ??
|
|
1021
|
+
prefixByClassId.get(enclosingClass.id) ?? [''];
|
|
1022
|
+
for (const prefix of prefixes) {
|
|
1023
|
+
out.push({
|
|
1024
|
+
role: 'consumer',
|
|
1025
|
+
framework: OPENFEIGN_FRAMEWORK,
|
|
1026
|
+
method: parsed.method,
|
|
1027
|
+
path: joinPath(prefix, parsed.path),
|
|
1028
|
+
name: nameNode?.text ?? null,
|
|
1029
|
+
confidence: REQUEST_LINE_CONFIDENCE,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
540
1033
|
return out;
|
|
541
1034
|
},
|
|
1035
|
+
scanProject: scanKotlinProject,
|
|
542
1036
|
};
|
|
543
1037
|
}
|
|
544
1038
|
/**
|