gitnexus 1.6.9-rc.20 → 1.6.9-rc.22
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 +8 -7
- package/dist/core/group/extractors/http-patterns/kotlin.js +2 -2
- package/dist/core/group/extractors/http-patterns/spring-consumer-shared.d.ts +8 -48
- package/dist/core/group/extractors/http-patterns/spring-consumer-shared.js +19 -101
- package/dist/core/ingestion/language-provider.d.ts +14 -0
- package/dist/core/ingestion/languages/java.js +2 -1
- package/dist/core/ingestion/parsing-processor.d.ts +3 -0
- package/dist/core/ingestion/parsing-processor.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +25 -0
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +78 -0
- package/dist/core/ingestion/route-extractors/spring-shared.js +129 -0
- package/dist/core/ingestion/route-extractors/spring.d.ts +13 -0
- package/dist/core/ingestion/route-extractors/spring.js +170 -2
- package/dist/core/ingestion/workers/parse-worker.d.ts +10 -0
- package/dist/core/ingestion/workers/parse-worker.js +8 -0
- package/dist/core/ingestion/workers/result-merge.js +4 -0
- package/dist/core/run-analyze.js +14 -0
- package/dist/core/search/fts-indexes.js +35 -19
- package/dist/core/search/fts-schema.js +37 -4
- package/dist/storage/parse-cache.js +1 -1
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Java from 'tree-sitter-java';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
|
-
import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
|
-
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix,
|
|
3
|
+
import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, joinPath, } 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, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
5
5
|
import { extractStaticPathExpression, inferOkHttpMethod, inferHttpClientMethod, okHttpUrlRootsAtBuilder, httpClientUriRootsAtNewBuilder, httpClientChainHasUriCall, } from './java-static-path.js';
|
|
6
6
|
/**
|
|
7
7
|
* Java HTTP plugin. Handles:
|
|
@@ -579,12 +579,13 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
579
579
|
// ingestion does NOT emit a Route node for (1) a method-level array route
|
|
580
580
|
// nested under a class-level array-form `@RequestMapping` (ingestion suppresses
|
|
581
581
|
// it rather than drop the prefix; bare/scalar-prefixed array methods ARE now
|
|
582
|
-
// emitted — see #2280), (2)
|
|
583
|
-
//
|
|
584
|
-
//
|
|
582
|
+
// emitted — see #2280), or (2) the 2nd verb of a same-URL GET+POST pair (Route
|
|
583
|
+
// nodes are URL-keyed). Interface-inherited Spring routes ARE now emitted by
|
|
584
|
+
// ingestion (#2288), so they are no longer a coverage gap. Declaring 'complete'
|
|
585
|
+
// here would let the parse-skip drop those remaining group-only providers.
|
|
585
586
|
// Java flips to 'complete' only once ingestion provider extraction matches this
|
|
586
|
-
// scan (a follow-up: class-level array-form prefix support +
|
|
587
|
-
//
|
|
587
|
+
// scan (a follow-up: class-level array-form prefix support + per-verb Route
|
|
588
|
+
// identity — tracked in #2280).
|
|
588
589
|
// `hasConsumerSignals` below is kept ready for that flip.
|
|
589
590
|
// Consumer signals this plugin's scan() can detect: RestTemplate / WebClient /
|
|
590
591
|
// OkHttp / Java-HttpClient / Apache-HttpClient call sites, OpenFeign
|
|
@@ -1,7 +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,
|
|
3
|
+
import { METHOD_ANNOTATION_TO_HTTP, findEnclosingClass, joinPath, } 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, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
5
5
|
/**
|
|
6
6
|
* Kotlin HTTP plugin (Spring providers + consumers).
|
|
7
7
|
*
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* the group layer beside the plugins that use it.
|
|
16
16
|
*/
|
|
17
17
|
import type { HttpFileDetections } from './types.js';
|
|
18
|
+
import { type SharedSpringType } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
18
19
|
/**
|
|
19
20
|
* RestTemplate method-name → HTTP verb. Source-scan only: the receiver must be
|
|
20
21
|
* named exactly `restTemplate` (the per-language query enforces that).
|
|
@@ -84,54 +85,13 @@ export declare function parseRequestLine(raw: string): {
|
|
|
84
85
|
path: string;
|
|
85
86
|
} | null;
|
|
86
87
|
/**
|
|
87
|
-
*
|
|
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
|
|
88
|
+
* Resolve interface-based-controller provider *detections* (#1743): a concrete
|
|
128
89
|
* `@RestController`/`@Controller` class inherits the `@(Get|...)Mapping` routes
|
|
129
|
-
* declared on the interface it implements.
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* avoids doubling a prefix the interface already baked in (#2057).
|
|
90
|
+
* declared on the interface it implements. Thin group-layer adapter over the
|
|
91
|
+
* shared, language-agnostic `resolveInheritedSpringRoutes` (in
|
|
92
|
+
* `ingestion/route-extractors/spring-shared.ts`) — it maps each inherited route
|
|
93
|
+
* to a provider `HttpDetection`. Shared by the Java and Kotlin plugins so both
|
|
94
|
+
* emit byte-identical provider contracts; the ingestion route extractor calls
|
|
95
|
+
* the same underlying algorithm so all three stay in parity.
|
|
136
96
|
*/
|
|
137
97
|
export declare function scanSpringInheritanceProject(types: SharedSpringType[]): HttpFileDetections[];
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* route extractor). This module is the consumer-side counterpart and lives in
|
|
15
15
|
* the group layer beside the plugins that use it.
|
|
16
16
|
*/
|
|
17
|
+
import { resolveInheritedSpringRoutes, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
17
18
|
/**
|
|
18
19
|
* RestTemplate method-name → HTTP verb. Source-scan only: the receiver must be
|
|
19
20
|
* named exactly `restTemplate` (the per-language query enforces that).
|
|
@@ -117,111 +118,28 @@ export function parseRequestLine(raw) {
|
|
|
117
118
|
return { method: verb.toUpperCase(), path: pathOnly };
|
|
118
119
|
}
|
|
119
120
|
/**
|
|
120
|
-
*
|
|
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
|
|
121
|
+
* Resolve interface-based-controller provider *detections* (#1743): a concrete
|
|
153
122
|
* `@RestController`/`@Controller` class inherits the `@(Get|...)Mapping` routes
|
|
154
|
-
* declared on the interface it implements.
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* avoids doubling a prefix the interface already baked in (#2057).
|
|
123
|
+
* declared on the interface it implements. Thin group-layer adapter over the
|
|
124
|
+
* shared, language-agnostic `resolveInheritedSpringRoutes` (in
|
|
125
|
+
* `ingestion/route-extractors/spring-shared.ts`) — it maps each inherited route
|
|
126
|
+
* to a provider `HttpDetection`. Shared by the Java and Kotlin plugins so both
|
|
127
|
+
* emit byte-identical provider contracts; the ingestion route extractor calls
|
|
128
|
+
* the same underlying algorithm so all three stay in parity.
|
|
161
129
|
*/
|
|
162
130
|
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
131
|
const detectionsByFile = new Map();
|
|
187
|
-
for (const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
}
|
|
132
|
+
for (const route of resolveInheritedSpringRoutes(types)) {
|
|
133
|
+
const detections = detectionsByFile.get(route.filePath) ?? [];
|
|
134
|
+
detections.push({
|
|
135
|
+
role: 'provider',
|
|
136
|
+
framework: 'spring',
|
|
137
|
+
method: route.method,
|
|
138
|
+
path: route.path,
|
|
139
|
+
name: route.methodName,
|
|
140
|
+
confidence: 0.8,
|
|
141
|
+
});
|
|
142
|
+
detectionsByFile.set(route.filePath, detections);
|
|
225
143
|
}
|
|
226
144
|
return [...detectionsByFile.entries()].map(([filePath, detections]) => ({
|
|
227
145
|
filePath,
|
|
@@ -22,6 +22,7 @@ import type { SyntaxNode } from './utils/ast-helpers.js';
|
|
|
22
22
|
import type { CfgVisitor } from './cfg/types.js';
|
|
23
23
|
import type { NodeLabel } from '../../_shared/index.js';
|
|
24
24
|
import type { ExtractedRoute } from './route-extractors/laravel.js';
|
|
25
|
+
import type { SharedSpringType } from './route-extractors/spring-shared.js';
|
|
25
26
|
import type Parser from 'tree-sitter';
|
|
26
27
|
import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
|
|
27
28
|
/** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
|
|
@@ -226,6 +227,19 @@ interface LanguageProviderConfig {
|
|
|
226
227
|
* Default: undefined (no language-specific decorator route extraction).
|
|
227
228
|
*/
|
|
228
229
|
readonly extractDecoratorRoutes?: (tree: Parser.Tree, filePath: string, lineOffset: number) => ExtractedDecoratorRoute[];
|
|
230
|
+
/**
|
|
231
|
+
* Collect a project-wide, language-agnostic view of route-defining
|
|
232
|
+
* class/interface declarations (`SharedSpringType`) from a parsed file.
|
|
233
|
+
*
|
|
234
|
+
* When defined, the parse worker calls this per file and the parse phase
|
|
235
|
+
* aggregates the results, then runs a cross-file pass that resolves
|
|
236
|
+
* interface-inherited routes (a concrete controller inherits the `@*Mapping`s
|
|
237
|
+
* its interfaces declare) and appends them to `decoratorRoutes`. Separate from
|
|
238
|
+
* `extractDecoratorRoutes` because inheritance needs all files, not one.
|
|
239
|
+
*
|
|
240
|
+
* Default: undefined (no interface-inheritance route resolution).
|
|
241
|
+
*/
|
|
242
|
+
readonly extractRouteInheritanceTypes?: (tree: Parser.Tree, filePath: string) => SharedSpringType[];
|
|
229
243
|
/** Built-in/stdlib names that should be filtered from the call graph for this language.
|
|
230
244
|
* Default: undefined (no language-specific filtering). */
|
|
231
245
|
readonly builtInNames?: ReadonlySet<string>;
|
|
@@ -12,7 +12,7 @@ import { javaClassConfig } from '../class-extractors/configs/jvm.js';
|
|
|
12
12
|
import { defineLanguage } from '../language-provider.js';
|
|
13
13
|
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
14
14
|
import { javaTypeConfig } from '../type-extractors/jvm.js';
|
|
15
|
-
import { extractSpringRoutes } from '../route-extractors/spring.js';
|
|
15
|
+
import { extractSpringRoutes, extractSpringTypes } from '../route-extractors/spring.js';
|
|
16
16
|
import { javaExportChecker } from '../export-detection.js';
|
|
17
17
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
18
18
|
import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
|
|
@@ -112,4 +112,5 @@ export const javaProvider = defineLanguage({
|
|
|
112
112
|
orderSameNameTypeCandidates: orderJavaSameNameTypeCandidates,
|
|
113
113
|
// ── Route extraction ──
|
|
114
114
|
extractDecoratorRoutes: extractSpringRoutes,
|
|
115
|
+
extractRouteInheritanceTypes: extractSpringTypes,
|
|
115
116
|
});
|
|
@@ -5,6 +5,7 @@ import type { ParsedFile } from '../../_shared/index.js';
|
|
|
5
5
|
import { WorkerPool } from './workers/worker-pool.js';
|
|
6
6
|
import type { ParseWorkerResult, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileScopeBindings, ExtractedORMQuery, FetchWrapperDef } from './workers/parse-worker.js';
|
|
7
7
|
import type { ExtractedRouterImport, ExtractedRouterInclude, ExtractedRouterModuleAlias } from './route-extractors/fastapi-router-bindings.js';
|
|
8
|
+
import type { SharedSpringType } from './route-extractors/spring-shared.js';
|
|
8
9
|
export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
|
|
9
10
|
export interface WorkerExtractedData {
|
|
10
11
|
routes: ExtractedRoute[];
|
|
@@ -16,6 +17,8 @@ export interface WorkerExtractedData {
|
|
|
16
17
|
routerModuleAliases: ExtractedRouterModuleAlias[];
|
|
17
18
|
toolDefs: ExtractedToolDef[];
|
|
18
19
|
ormQueries: ExtractedORMQuery[];
|
|
20
|
+
/** Project-wide Spring class/interface views for the #2288 inheritance pass. */
|
|
21
|
+
springTypes: SharedSpringType[];
|
|
19
22
|
fileScopeBindings: FileScopeBindings[];
|
|
20
23
|
/**
|
|
21
24
|
* Per-file `ParsedFile` artifacts from the new scope-based resolution
|
|
@@ -23,6 +23,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
23
23
|
const allRouterIncludes = [];
|
|
24
24
|
const allRouterImports = [];
|
|
25
25
|
const allRouterModuleAliases = [];
|
|
26
|
+
const allSpringTypes = [];
|
|
26
27
|
const allToolDefs = [];
|
|
27
28
|
const allORMQueries = [];
|
|
28
29
|
const fileScopeBindingsByFile = [];
|
|
@@ -71,6 +72,8 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
71
72
|
allRouterImports.push(item);
|
|
72
73
|
for (const item of result.routerModuleAliases ?? [])
|
|
73
74
|
allRouterModuleAliases.push(item);
|
|
75
|
+
for (const item of result.springTypes ?? [])
|
|
76
|
+
allSpringTypes.push(item);
|
|
74
77
|
for (const item of result.toolDefs)
|
|
75
78
|
allToolDefs.push(item);
|
|
76
79
|
if (result.ormQueries)
|
|
@@ -93,6 +96,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
93
96
|
routerModuleAliases: allRouterModuleAliases,
|
|
94
97
|
toolDefs: allToolDefs,
|
|
95
98
|
ormQueries: allORMQueries,
|
|
99
|
+
springTypes: allSpringTypes,
|
|
96
100
|
fileScopeBindings: fileScopeBindingsByFile,
|
|
97
101
|
parsedFiles: allParsedFiles,
|
|
98
102
|
};
|
|
@@ -27,6 +27,7 @@ import { isLanguageAvailable, isGrammarRuntimeSkipped, createParserForLanguage,
|
|
|
27
27
|
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
28
28
|
import { getProvider, providers } from '../languages/index.js';
|
|
29
29
|
import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
|
|
30
|
+
import { resolveInheritedSpringRoutes, } from '../route-extractors/spring-shared.js';
|
|
30
31
|
import fs from 'node:fs';
|
|
31
32
|
import path from 'node:path';
|
|
32
33
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -461,6 +462,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
461
462
|
const allRouterIncludes = [];
|
|
462
463
|
const allRouterImports = [];
|
|
463
464
|
const allRouterModuleAliases = [];
|
|
465
|
+
const allSpringTypes = [];
|
|
464
466
|
const allToolDefs = [];
|
|
465
467
|
const allORMQueries = [];
|
|
466
468
|
// Aggregated per-file ParsedFile artifacts produced by workers' calls
|
|
@@ -601,6 +603,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
601
603
|
for (const item of chunkWorkerData.routerModuleAliases)
|
|
602
604
|
allRouterModuleAliases.push(item);
|
|
603
605
|
}
|
|
606
|
+
if (chunkWorkerData.springTypes?.length) {
|
|
607
|
+
for (const item of chunkWorkerData.springTypes)
|
|
608
|
+
allSpringTypes.push(item);
|
|
609
|
+
}
|
|
604
610
|
if (chunkWorkerData.toolDefs?.length) {
|
|
605
611
|
for (const item of chunkWorkerData.toolDefs)
|
|
606
612
|
allToolDefs.push(item);
|
|
@@ -1047,6 +1053,25 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1047
1053
|
allDecoratorRoutes.push(dr);
|
|
1048
1054
|
}
|
|
1049
1055
|
}
|
|
1056
|
+
// Cross-file Spring interface-inheritance pass (#2288): a concrete
|
|
1057
|
+
// `@RestController` inherits the `@*Mapping`s declared on the interfaces it
|
|
1058
|
+
// implements. The per-file `SharedSpringType` views collected by the Java
|
|
1059
|
+
// provider's `extractRouteInheritanceTypes` hook are resolved here, project-
|
|
1060
|
+
// wide, into decorator routes attributed to the implementing controller (the
|
|
1061
|
+
// interface's own per-file routes were suppressed at extraction). Mirrors the
|
|
1062
|
+
// group layer via the shared `resolveInheritedSpringRoutes` so both agree.
|
|
1063
|
+
if (allSpringTypes.length > 0) {
|
|
1064
|
+
for (const inherited of resolveInheritedSpringRoutes(allSpringTypes)) {
|
|
1065
|
+
allDecoratorRoutes.push({
|
|
1066
|
+
filePath: inherited.filePath,
|
|
1067
|
+
routePath: inherited.path,
|
|
1068
|
+
httpMethod: inherited.method,
|
|
1069
|
+
decoratorName: 'inherited-mapping',
|
|
1070
|
+
lineNumber: 0,
|
|
1071
|
+
handlerName: inherited.methodName,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1050
1075
|
logHeapProbe('parse-impl-return', `exportedTypeMap=${exportedTypeMap.size} parsedFiles=${allParsedFiles.length} nodes=${graph.nodeCount}`);
|
|
1051
1076
|
// Part 2 (#2138): resolve each route's handler to a real symbol UID now that
|
|
1052
1077
|
// the model is fully populated and decorator-route prefixes are finalized.
|
|
@@ -38,6 +38,16 @@ export declare function isRouteMemberKey(keyNode: Parser.SyntaxNode | undefined)
|
|
|
38
38
|
* at a time.
|
|
39
39
|
*/
|
|
40
40
|
export declare function findEnclosingClass(node: Parser.SyntaxNode): Parser.SyntaxNode | null;
|
|
41
|
+
/**
|
|
42
|
+
* Find the nearest enclosing Java type declaration (class OR interface) for a
|
|
43
|
+
* node, reporting its kind. Used by the ingestion route extractor to tell an
|
|
44
|
+
* interface-declared `@*Mapping` (handled by the cross-file inheritance pass,
|
|
45
|
+
* #2288) apart from a concrete class route, and by the type collector.
|
|
46
|
+
*/
|
|
47
|
+
export declare function findEnclosingType(node: Parser.SyntaxNode): {
|
|
48
|
+
node: Parser.SyntaxNode;
|
|
49
|
+
kind: 'class' | 'interface';
|
|
50
|
+
} | null;
|
|
41
51
|
/**
|
|
42
52
|
* Strip enclosing quotes from a tree-sitter string-literal node's text.
|
|
43
53
|
* Handles single / double / template (backtick) quotes and triple-quoted
|
|
@@ -48,3 +58,71 @@ export declare function findEnclosingClass(node: Parser.SyntaxNode): Parser.Synt
|
|
|
48
58
|
* content without quotes already).
|
|
49
59
|
*/
|
|
50
60
|
export declare function unquoteSpringLiteral(raw: string): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Join a class/interface-level prefix and a method-level path into a single
|
|
63
|
+
* URL path: strip leading/trailing slashes on the prefix and leading slashes
|
|
64
|
+
* on the method path, then ensure exactly one slash between them.
|
|
65
|
+
*
|
|
66
|
+
* Lives here (the lower shared layer) so the ingestion route extractor and the
|
|
67
|
+
* group-layer Spring/Kotlin plugins join prefixes identically; the group
|
|
68
|
+
* `spring-consumer-shared.ts` re-exports it for its existing importers.
|
|
69
|
+
*/
|
|
70
|
+
export declare function joinPath(prefix: string, methodPath: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Join a controller's own class prefix with a route inherited from an interface
|
|
73
|
+
* (interface-based controllers, #1743). The inherited path already has the
|
|
74
|
+
* interface's own class prefix (`inheritedOwnerPrefix`) baked in; when the
|
|
75
|
+
* controller repeats that same prefix we must NOT prepend it twice (#2057).
|
|
76
|
+
*/
|
|
77
|
+
export declare function joinInheritedSpringPath(controllerPrefix: string, inheritedPath: string, inheritedOwnerPrefix?: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Language-agnostic view of a Spring class/interface that each extractor's
|
|
80
|
+
* grammar-specific collector produces. The interface-based-controller
|
|
81
|
+
* inheritance algorithm (`resolveInheritedSpringRoutes`) operates only on this
|
|
82
|
+
* shape, so the ingestion route extractor and the group Java/Kotlin plugins
|
|
83
|
+
* share one algorithm and cannot drift.
|
|
84
|
+
*
|
|
85
|
+
* `methods[].routes` carry only `{ method, path }` — the interface's own class
|
|
86
|
+
* prefix is applied *inside* `resolveInheritedSpringRoutes` (it is not part of
|
|
87
|
+
* the collector's output).
|
|
88
|
+
*/
|
|
89
|
+
export interface SharedSpringType {
|
|
90
|
+
filePath: string;
|
|
91
|
+
kind: 'class' | 'interface';
|
|
92
|
+
name: string;
|
|
93
|
+
/** Class-level `@RequestMapping` prefixes — one per array element. */
|
|
94
|
+
classPrefixes: string[];
|
|
95
|
+
implementedInterfaces: string[];
|
|
96
|
+
isController: boolean;
|
|
97
|
+
methods: Array<{
|
|
98
|
+
name: string;
|
|
99
|
+
routes: Array<{
|
|
100
|
+
method: string;
|
|
101
|
+
path: string;
|
|
102
|
+
}>;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
/** One provider route a concrete controller inherits from an interface. */
|
|
106
|
+
interface InheritedSpringRoute {
|
|
107
|
+
/** File of the implementing controller (where the route should be attributed). */
|
|
108
|
+
filePath: string;
|
|
109
|
+
/** Name of the controller method that inherits the route. */
|
|
110
|
+
methodName: string;
|
|
111
|
+
method: string;
|
|
112
|
+
path: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve interface-based-controller provider routes (#1743): a concrete
|
|
116
|
+
* `@RestController`/`@Controller` class inherits the `@(Get|...)Mapping` routes
|
|
117
|
+
* declared on the interface it implements. Pure and language-agnostic — shared
|
|
118
|
+
* by the ingestion route extractor and the group Java/Kotlin plugins so all
|
|
119
|
+
* three emit the same inherited routes.
|
|
120
|
+
*
|
|
121
|
+
* An interface name that resolves to two distinct interfaces is ambiguous and
|
|
122
|
+
* its routes are dropped (the `null` marker). The controller's own class
|
|
123
|
+
* prefix(es) cross-product the inherited routes; `joinInheritedSpringPath`
|
|
124
|
+
* avoids doubling a prefix the interface already baked in (#2057). Duplicate
|
|
125
|
+
* `(method, path)` results per controller method are de-duped.
|
|
126
|
+
*/
|
|
127
|
+
export declare function resolveInheritedSpringRoutes(types: SharedSpringType[]): InheritedSpringRoute[];
|
|
128
|
+
export {};
|
|
@@ -55,6 +55,23 @@ export function findEnclosingClass(node) {
|
|
|
55
55
|
}
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Find the nearest enclosing Java type declaration (class OR interface) for a
|
|
60
|
+
* node, reporting its kind. Used by the ingestion route extractor to tell an
|
|
61
|
+
* interface-declared `@*Mapping` (handled by the cross-file inheritance pass,
|
|
62
|
+
* #2288) apart from a concrete class route, and by the type collector.
|
|
63
|
+
*/
|
|
64
|
+
export function findEnclosingType(node) {
|
|
65
|
+
let cur = node.parent;
|
|
66
|
+
while (cur) {
|
|
67
|
+
if (cur.type === 'class_declaration')
|
|
68
|
+
return { node: cur, kind: 'class' };
|
|
69
|
+
if (cur.type === 'interface_declaration')
|
|
70
|
+
return { node: cur, kind: 'interface' };
|
|
71
|
+
cur = cur.parent;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
58
75
|
/**
|
|
59
76
|
* Strip enclosing quotes from a tree-sitter string-literal node's text.
|
|
60
77
|
* Handles single / double / template (backtick) quotes and triple-quoted
|
|
@@ -78,3 +95,115 @@ export function unquoteSpringLiteral(raw) {
|
|
|
78
95
|
}
|
|
79
96
|
return raw;
|
|
80
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Join a class/interface-level prefix and a method-level path into a single
|
|
100
|
+
* URL path: strip leading/trailing slashes on the prefix and leading slashes
|
|
101
|
+
* on the method path, then ensure exactly one slash between them.
|
|
102
|
+
*
|
|
103
|
+
* Lives here (the lower shared layer) so the ingestion route extractor and the
|
|
104
|
+
* group-layer Spring/Kotlin plugins join prefixes identically; the group
|
|
105
|
+
* `spring-consumer-shared.ts` re-exports it for its existing importers.
|
|
106
|
+
*/
|
|
107
|
+
export function joinPath(prefix, methodPath) {
|
|
108
|
+
const cleanPrefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
109
|
+
const cleanSub = methodPath.replace(/^\/+/, '');
|
|
110
|
+
if (!cleanPrefix)
|
|
111
|
+
return `/${cleanSub}`;
|
|
112
|
+
return `/${cleanPrefix}/${cleanSub}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Join a controller's own class prefix with a route inherited from an interface
|
|
116
|
+
* (interface-based controllers, #1743). The inherited path already has the
|
|
117
|
+
* interface's own class prefix (`inheritedOwnerPrefix`) baked in; when the
|
|
118
|
+
* controller repeats that same prefix we must NOT prepend it twice (#2057).
|
|
119
|
+
*/
|
|
120
|
+
export function joinInheritedSpringPath(controllerPrefix, inheritedPath, inheritedOwnerPrefix = '') {
|
|
121
|
+
const joined = joinPath(controllerPrefix, inheritedPath);
|
|
122
|
+
const cleanPrefix = controllerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
123
|
+
const cleanOwnerPrefix = inheritedOwnerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
124
|
+
const cleanInherited = inheritedPath.replace(/^\/+/, '');
|
|
125
|
+
if (!cleanPrefix)
|
|
126
|
+
return joined;
|
|
127
|
+
if (cleanPrefix === cleanOwnerPrefix &&
|
|
128
|
+
(cleanInherited === cleanPrefix || cleanInherited.startsWith(`${cleanPrefix}/`))) {
|
|
129
|
+
return `/${cleanInherited}`;
|
|
130
|
+
}
|
|
131
|
+
return joined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve interface-based-controller provider routes (#1743): a concrete
|
|
135
|
+
* `@RestController`/`@Controller` class inherits the `@(Get|...)Mapping` routes
|
|
136
|
+
* declared on the interface it implements. Pure and language-agnostic — shared
|
|
137
|
+
* by the ingestion route extractor and the group Java/Kotlin plugins so all
|
|
138
|
+
* three emit the same inherited routes.
|
|
139
|
+
*
|
|
140
|
+
* An interface name that resolves to two distinct interfaces is ambiguous and
|
|
141
|
+
* its routes are dropped (the `null` marker). The controller's own class
|
|
142
|
+
* prefix(es) cross-product the inherited routes; `joinInheritedSpringPath`
|
|
143
|
+
* avoids doubling a prefix the interface already baked in (#2057). Duplicate
|
|
144
|
+
* `(method, path)` results per controller method are de-duped.
|
|
145
|
+
*/
|
|
146
|
+
export function resolveInheritedSpringRoutes(types) {
|
|
147
|
+
// interface name → (method name → routes). `null` marks an ambiguous
|
|
148
|
+
// (duplicated) interface name. `IntermediateRoute` is declared at module scope.
|
|
149
|
+
const interfaceRoutes = new Map();
|
|
150
|
+
for (const type of types) {
|
|
151
|
+
if (type.kind !== 'interface')
|
|
152
|
+
continue;
|
|
153
|
+
if (interfaceRoutes.has(type.name)) {
|
|
154
|
+
interfaceRoutes.set(type.name, null);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const prefixes = type.classPrefixes.length ? type.classPrefixes : [''];
|
|
158
|
+
const methodMap = new Map();
|
|
159
|
+
for (const method of type.methods) {
|
|
160
|
+
// Cross-product the interface's class prefixes with each method route, so a
|
|
161
|
+
// multi-element `@RequestMapping(["/a","/b"])` interface yields N bindings.
|
|
162
|
+
const routes = method.routes.flatMap((route) => prefixes.map((prefix) => ({
|
|
163
|
+
method: route.method,
|
|
164
|
+
path: prefix ? joinPath(prefix, route.path) : route.path,
|
|
165
|
+
ownerPrefix: prefix,
|
|
166
|
+
})));
|
|
167
|
+
if (routes.length > 0)
|
|
168
|
+
methodMap.set(method.name, routes);
|
|
169
|
+
}
|
|
170
|
+
interfaceRoutes.set(type.name, methodMap);
|
|
171
|
+
}
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const type of types) {
|
|
174
|
+
if (type.kind !== 'class' || !type.isController)
|
|
175
|
+
continue;
|
|
176
|
+
// Cross-product the controller's own class prefixes with each inherited
|
|
177
|
+
// route; `['']` keeps the common no-prefix controller emitting the
|
|
178
|
+
// interface path unchanged.
|
|
179
|
+
const controllerPrefixes = type.classPrefixes.length ? type.classPrefixes : [''];
|
|
180
|
+
for (const method of type.methods) {
|
|
181
|
+
if (method.routes.length > 0)
|
|
182
|
+
continue; // own @*Mapping → already a provider
|
|
183
|
+
const inherited = type.implementedInterfaces.flatMap((iface) => {
|
|
184
|
+
const routeMap = interfaceRoutes.get(iface);
|
|
185
|
+
if (!routeMap)
|
|
186
|
+
return [];
|
|
187
|
+
const routes = routeMap.get(method.name) ?? [];
|
|
188
|
+
return routes.flatMap((route) => controllerPrefixes.map((controllerPrefix) => ({
|
|
189
|
+
method: route.method,
|
|
190
|
+
path: joinInheritedSpringPath(controllerPrefix, route.path, route.ownerPrefix),
|
|
191
|
+
})));
|
|
192
|
+
});
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
for (const route of inherited) {
|
|
195
|
+
const key = `${route.method} ${route.path}`;
|
|
196
|
+
if (seen.has(key))
|
|
197
|
+
continue;
|
|
198
|
+
seen.add(key);
|
|
199
|
+
out.push({
|
|
200
|
+
filePath: type.filePath,
|
|
201
|
+
methodName: method.name,
|
|
202
|
+
method: route.method,
|
|
203
|
+
path: route.path,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import Parser from 'tree-sitter';
|
|
22
22
|
import type { ExtractedDecoratorRoute } from '../workers/parse-worker.js';
|
|
23
|
+
import { type SharedSpringType } from './spring-shared.js';
|
|
23
24
|
/**
|
|
24
25
|
* Extract Spring route annotations from a parsed Java file.
|
|
25
26
|
*
|
|
@@ -33,3 +34,15 @@ import type { ExtractedDecoratorRoute } from '../workers/parse-worker.js';
|
|
|
33
34
|
* @returns Decorator routes with prefix already set per-class
|
|
34
35
|
*/
|
|
35
36
|
export declare function extractSpringRoutes(tree: Parser.Tree, filePath: string, lineOffset?: number): ExtractedDecoratorRoute[];
|
|
37
|
+
/**
|
|
38
|
+
* Build the project-wide `SharedSpringType` view for one Java file: every class
|
|
39
|
+
* and interface with its class prefixes, implemented interfaces, controller
|
|
40
|
+
* flag, and per-method route annotations. The cross-file inheritance pass
|
|
41
|
+
* (#2288) feeds these into the shared `resolveInheritedSpringRoutes` so a
|
|
42
|
+
* concrete controller inherits the `@*Mapping`s declared on its interfaces.
|
|
43
|
+
*
|
|
44
|
+
* This is the ingestion counterpart of the group layer's `collectSpringTypes`
|
|
45
|
+
* (`group/extractors/http-patterns/java.ts`); both produce the same neutral
|
|
46
|
+
* shape so the two layers resolve inheritance identically (#2078 parity).
|
|
47
|
+
*/
|
|
48
|
+
export declare function extractSpringTypes(tree: Parser.Tree, filePath: string): SharedSpringType[];
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import Parser from 'tree-sitter';
|
|
22
22
|
import Java from 'tree-sitter-java';
|
|
23
|
-
import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey,
|
|
23
|
+
import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingType, unquoteSpringLiteral, } from './spring-shared.js';
|
|
24
24
|
/**
|
|
25
25
|
* Single predicate-free tree-sitter query that captures all route annotations
|
|
26
26
|
* on classes and methods. Discrimination by annotation name and node type
|
|
@@ -150,7 +150,15 @@ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
|
|
|
150
150
|
const routePath = unquoteSpringLiteral(valueNode.text);
|
|
151
151
|
if (routePath === null)
|
|
152
152
|
continue;
|
|
153
|
-
const
|
|
153
|
+
const enclosingType = findEnclosingType(node);
|
|
154
|
+
// Interface-declared `@*Mapping`s are not concrete routes on their own — the
|
|
155
|
+
// implementing controller inherits them. Skip here; the cross-file
|
|
156
|
+
// inheritance pass (#2288) re-emits them attributed to the controller, with
|
|
157
|
+
// both the interface's and the controller's class prefixes resolved. Emitting
|
|
158
|
+
// the interface route directly would be wrong (unprefixed, wrong owner).
|
|
159
|
+
if (enclosingType?.kind === 'interface')
|
|
160
|
+
continue;
|
|
161
|
+
const enclosingClass = enclosingType?.kind === 'class' ? enclosingType.node : null;
|
|
154
162
|
// Suppress a method-level *array-form* route nested under a class-level
|
|
155
163
|
// array-form @RequestMapping. The class prefix is one of several values that
|
|
156
164
|
// cannot be resolved to a single string here, so emitting the route would
|
|
@@ -179,3 +187,163 @@ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
|
|
|
179
187
|
}
|
|
180
188
|
return routes;
|
|
181
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Tree-sitter query capturing every Java type declaration (class + interface),
|
|
192
|
+
* used by `extractSpringTypes` to build the project-wide `SharedSpringType`
|
|
193
|
+
* view the cross-file interface-inheritance pass consumes (#2288).
|
|
194
|
+
*/
|
|
195
|
+
const TYPE_DECLARATION_QUERY = new Parser.Query(Java, `[(class_declaration) @type (interface_declaration) @type]`);
|
|
196
|
+
/** Direct annotations on a type/method declaration (reads its `modifiers` child). */
|
|
197
|
+
function declarationAnnotations(node) {
|
|
198
|
+
// `modifiers` is a NAMED CHILD of the declaration (not a named field) in
|
|
199
|
+
// tree-sitter-java — matching the group layer's `hasAnnotation`.
|
|
200
|
+
const modifiers = node.namedChildren.find((c) => c.type === 'modifiers');
|
|
201
|
+
if (!modifiers)
|
|
202
|
+
return [];
|
|
203
|
+
return modifiers.namedChildren.filter((c) => c.type === 'annotation' || c.type === 'marker_annotation');
|
|
204
|
+
}
|
|
205
|
+
const annotationName = (ann) => {
|
|
206
|
+
// Trailing segment of a possibly fully-qualified annotation name
|
|
207
|
+
// (`org.springframework.web.bind.annotation.GetMapping` → `GetMapping`), so a
|
|
208
|
+
// FQN annotation is classified the same as its simple form — matching the
|
|
209
|
+
// group layer's `simpleName` normalization. A simple name maps to itself.
|
|
210
|
+
const text = ann.childForFieldName('name')?.text;
|
|
211
|
+
return text?.split('.').pop() ?? text;
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Collect the route path(s) carried by an annotation's argument list, honoring
|
|
215
|
+
* both positional and `path =`/`value =` named arguments and both the bare
|
|
216
|
+
* string and array forms. Non-route named args (`consumes`, `produces`, …) are
|
|
217
|
+
* dropped via `isRouteMemberKey`. Returns one entry per string element.
|
|
218
|
+
*/
|
|
219
|
+
function annotationRoutePaths(ann) {
|
|
220
|
+
const args = ann.childForFieldName('arguments');
|
|
221
|
+
if (!args)
|
|
222
|
+
return [];
|
|
223
|
+
const out = [];
|
|
224
|
+
const pushLiteral = (lit) => {
|
|
225
|
+
const v = unquoteSpringLiteral(lit.text);
|
|
226
|
+
if (v !== null)
|
|
227
|
+
out.push(v);
|
|
228
|
+
};
|
|
229
|
+
const pushFromValue = (valueNode) => {
|
|
230
|
+
if (valueNode.type === 'string_literal')
|
|
231
|
+
pushLiteral(valueNode);
|
|
232
|
+
else if (valueNode.type === 'element_value_array_initializer') {
|
|
233
|
+
for (const el of valueNode.namedChildren)
|
|
234
|
+
if (el.type === 'string_literal')
|
|
235
|
+
pushLiteral(el);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
for (const child of args.namedChildren) {
|
|
239
|
+
if (child.type === 'string_literal' || child.type === 'element_value_array_initializer') {
|
|
240
|
+
pushFromValue(child); // positional
|
|
241
|
+
}
|
|
242
|
+
else if (child.type === 'element_value_pair') {
|
|
243
|
+
const key = child.childForFieldName('key');
|
|
244
|
+
if (!isRouteMemberKey(key ?? undefined))
|
|
245
|
+
continue;
|
|
246
|
+
const value = child.childForFieldName('value');
|
|
247
|
+
if (value)
|
|
248
|
+
pushFromValue(value);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
/** Class-level `@RequestMapping` prefixes for a type (array-aware; may be []). */
|
|
254
|
+
function typeClassPrefixes(typeNode) {
|
|
255
|
+
const prefixes = [];
|
|
256
|
+
for (const ann of declarationAnnotations(typeNode)) {
|
|
257
|
+
if (annotationName(ann) === 'RequestMapping')
|
|
258
|
+
prefixes.push(...annotationRoutePaths(ann));
|
|
259
|
+
}
|
|
260
|
+
return prefixes;
|
|
261
|
+
}
|
|
262
|
+
/** The simple names of the interfaces a class declares via `implements`. */
|
|
263
|
+
function implementedInterfaceNames(typeNode) {
|
|
264
|
+
const interfacesNode = typeNode.childForFieldName('interfaces');
|
|
265
|
+
if (!interfacesNode)
|
|
266
|
+
return [];
|
|
267
|
+
const out = [];
|
|
268
|
+
const visit = (node) => {
|
|
269
|
+
if (node.type === 'type_identifier' || node.type === 'scoped_type_identifier') {
|
|
270
|
+
out.push(node.text.split('.').pop() ?? node.text);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
for (const child of node.namedChildren)
|
|
274
|
+
visit(child);
|
|
275
|
+
};
|
|
276
|
+
visit(interfacesNode);
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
/** Direct `method_declaration` children of a type (not methods of nested types). */
|
|
280
|
+
function directMethods(typeNode) {
|
|
281
|
+
const out = [];
|
|
282
|
+
const visit = (node) => {
|
|
283
|
+
for (const child of node.namedChildren) {
|
|
284
|
+
if (child.type === 'method_declaration') {
|
|
285
|
+
out.push(child);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
// Don't descend into a nested type — its methods aren't this type's.
|
|
289
|
+
if (child !== typeNode &&
|
|
290
|
+
(child.type === 'class_declaration' || child.type === 'interface_declaration')) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
visit(child);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
visit(typeNode);
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Build the project-wide `SharedSpringType` view for one Java file: every class
|
|
301
|
+
* and interface with its class prefixes, implemented interfaces, controller
|
|
302
|
+
* flag, and per-method route annotations. The cross-file inheritance pass
|
|
303
|
+
* (#2288) feeds these into the shared `resolveInheritedSpringRoutes` so a
|
|
304
|
+
* concrete controller inherits the `@*Mapping`s declared on its interfaces.
|
|
305
|
+
*
|
|
306
|
+
* This is the ingestion counterpart of the group layer's `collectSpringTypes`
|
|
307
|
+
* (`group/extractors/http-patterns/java.ts`); both produce the same neutral
|
|
308
|
+
* shape so the two layers resolve inheritance identically (#2078 parity).
|
|
309
|
+
*/
|
|
310
|
+
export function extractSpringTypes(tree, filePath) {
|
|
311
|
+
const out = [];
|
|
312
|
+
for (const match of TYPE_DECLARATION_QUERY.matches(tree.rootNode)) {
|
|
313
|
+
const typeNode = match.captures.find((c) => c.name === 'type')?.node;
|
|
314
|
+
if (!typeNode)
|
|
315
|
+
continue;
|
|
316
|
+
const name = typeNode.childForFieldName('name')?.text;
|
|
317
|
+
if (!name)
|
|
318
|
+
continue;
|
|
319
|
+
const kind = typeNode.type === 'interface_declaration' ? 'interface' : 'class';
|
|
320
|
+
const annNames = declarationAnnotations(typeNode).map(annotationName);
|
|
321
|
+
const isController = kind === 'class' && (annNames.includes('RestController') || annNames.includes('Controller'));
|
|
322
|
+
const methods = directMethods(typeNode)
|
|
323
|
+
.map((methodNode) => {
|
|
324
|
+
const methodName = methodNode.childForFieldName('name')?.text;
|
|
325
|
+
if (!methodName)
|
|
326
|
+
return null;
|
|
327
|
+
const routes = [];
|
|
328
|
+
for (const ann of declarationAnnotations(methodNode)) {
|
|
329
|
+
const verb = METHOD_ANNOTATION_TO_HTTP[annotationName(ann) ?? ''];
|
|
330
|
+
if (!verb)
|
|
331
|
+
continue;
|
|
332
|
+
for (const path of annotationRoutePaths(ann))
|
|
333
|
+
routes.push({ method: verb, path });
|
|
334
|
+
}
|
|
335
|
+
return { name: methodName, routes };
|
|
336
|
+
})
|
|
337
|
+
.filter((m) => m !== null);
|
|
338
|
+
out.push({
|
|
339
|
+
filePath,
|
|
340
|
+
kind,
|
|
341
|
+
name,
|
|
342
|
+
classPrefixes: typeClassPrefixes(typeNode),
|
|
343
|
+
implementedInterfaces: kind === 'class' ? implementedInterfaceNames(typeNode) : [],
|
|
344
|
+
isController,
|
|
345
|
+
methods,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { ConstructorBinding } from '../type-env.js';
|
|
|
6
6
|
import type { NodeLabel, ParameterTypeClass } from '../../../_shared/index.js';
|
|
7
7
|
import type { ParsedFile } from '../../../_shared/index.js';
|
|
8
8
|
import { type ExtractedRoute } from '../route-extractors/laravel.js';
|
|
9
|
+
import type { SharedSpringType } from '../route-extractors/spring-shared.js';
|
|
9
10
|
import { type CfgSkipCounts } from '../cfg/collect.js';
|
|
10
11
|
export type { ExtractedRoute } from '../route-extractors/laravel.js';
|
|
11
12
|
interface ParsedNode {
|
|
@@ -190,6 +191,15 @@ export interface ParseWorkerResult {
|
|
|
190
191
|
decoratorRoutes: ExtractedDecoratorRoute[];
|
|
191
192
|
routerIncludes: ExtractedRouterInclude[];
|
|
192
193
|
routerImports: ExtractedRouterImport[];
|
|
194
|
+
/**
|
|
195
|
+
* Optional. Project-wide `SharedSpringType` view of route-defining
|
|
196
|
+
* class/interface declarations, produced by the provider's
|
|
197
|
+
* `extractRouteInheritanceTypes` hook (Java/Spring). parse-impl aggregates
|
|
198
|
+
* these and runs a cross-file pass that resolves interface-inherited routes
|
|
199
|
+
* into additional `decoratorRoutes` (#2288). Optional for cache backward
|
|
200
|
+
* compatibility; consumers must guard with `?? []`.
|
|
201
|
+
*/
|
|
202
|
+
springTypes?: SharedSpringType[];
|
|
193
203
|
/**
|
|
194
204
|
* Optional. `from <pkg> import <module>` records from Python files
|
|
195
205
|
* where `<module>` is later used as a Shape-A include receiver
|
|
@@ -1832,6 +1832,14 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1832
1832
|
for (const r of frameworkRoutes)
|
|
1833
1833
|
result.decoratorRoutes.push(r);
|
|
1834
1834
|
}
|
|
1835
|
+
// Project-wide route-inheritance type collection via provider hook (#2288).
|
|
1836
|
+
// The per-file SharedSpringType views are aggregated by the parse phase,
|
|
1837
|
+
// which then resolves interface-inherited routes cross-file.
|
|
1838
|
+
if (provider.extractRouteInheritanceTypes) {
|
|
1839
|
+
const springTypes = provider.extractRouteInheritanceTypes(tree, file.path);
|
|
1840
|
+
if (springTypes.length > 0)
|
|
1841
|
+
(result.springTypes ??= []).push(...springTypes);
|
|
1842
|
+
}
|
|
1835
1843
|
// Vue: emit CALLS edges for components used in <template>
|
|
1836
1844
|
if (language === SupportedLanguages.Vue) {
|
|
1837
1845
|
const templateComponents = extractTemplateComponents(file.content);
|
|
@@ -28,6 +28,10 @@ export const mergeResult = (target, src) => {
|
|
|
28
28
|
target.routerModuleAliases ??= [];
|
|
29
29
|
appendAll(target.routerModuleAliases, src.routerModuleAliases);
|
|
30
30
|
}
|
|
31
|
+
if (src.springTypes) {
|
|
32
|
+
target.springTypes ??= [];
|
|
33
|
+
appendAll(target.springTypes, src.springTypes);
|
|
34
|
+
}
|
|
31
35
|
appendAll(target.toolDefs, src.toolDefs);
|
|
32
36
|
appendAll(target.ormQueries, src.ormQueries);
|
|
33
37
|
appendAll(target.constructorBindings, src.constructorBindings);
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -353,6 +353,20 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
353
353
|
}
|
|
354
354
|
try {
|
|
355
355
|
await initLbug(lbugPath);
|
|
356
|
+
// Gate on FTS availability BEFORE touching any index. createSearchFTSIndexes
|
|
357
|
+
// now DROPs each index before recreating it (so schema changes reach existing
|
|
358
|
+
// DBs); if the extension were unavailable, the drops would run and leave the
|
|
359
|
+
// DB index-less, only failing at the create step. Fail loudly first — mirrors
|
|
360
|
+
// the analyze path's `if (ftsAvailable)` gate below — so an unavailable
|
|
361
|
+
// extension never destroys the existing indexes.
|
|
362
|
+
const repairFtsAvailable = await loadFTSExtension(undefined, {
|
|
363
|
+
policy: resolveAnalyzeInstallPolicy(),
|
|
364
|
+
});
|
|
365
|
+
if (!repairFtsAvailable) {
|
|
366
|
+
throw new Error('Cannot repair FTS indexes: the LadybugDB FTS extension is unavailable ' +
|
|
367
|
+
'(not pre-installed and could not be installed on this machine). ' +
|
|
368
|
+
'Run `gitnexus doctor` to install it, then retry `--repair-fts`.');
|
|
369
|
+
}
|
|
356
370
|
progress('fts', 85, 'Repairing search indexes...');
|
|
357
371
|
await createSearchFTSIndexes({
|
|
358
372
|
onIndexStart: options.verbose
|
|
@@ -1,32 +1,48 @@
|
|
|
1
|
-
import { createFTSIndex } from '../lbug/lbug-adapter.js';
|
|
1
|
+
import { createFTSIndex, dropFTSIndex } from '../lbug/lbug-adapter.js';
|
|
2
2
|
import { FTS_INDEXES } from './fts-schema.js';
|
|
3
3
|
export async function createSearchFTSIndexes(options) {
|
|
4
4
|
for (const { table, indexName, properties } of FTS_INDEXES) {
|
|
5
5
|
options?.onIndexStart?.(table, indexName);
|
|
6
|
+
// Drop first so the live `properties` always win. `createFTSIndex` is
|
|
7
|
+
// idempotent-by-name (skips when the index already exists), so without the
|
|
8
|
+
// drop a schema change — e.g. adding `description` (#2299) — would never
|
|
9
|
+
// reach an existing `.lbug` DB on an incremental re-analyze or `--repair-fts`;
|
|
10
|
+
// the old name+content index would silently persist. `dropFTSIndex` no-ops
|
|
11
|
+
// when the index is absent (first-ever analyze) and clears the per-connection
|
|
12
|
+
// memo so the create below actually runs.
|
|
13
|
+
// ponytail: this rebuilds every FTS index on every analyze instead of
|
|
14
|
+
// skipping when present; FTS build is proportional to symbol-table size and
|
|
15
|
+
// runs inside the existing FTS phase. Gate on a stored schema fingerprint if
|
|
16
|
+
// this rebuild cost ever shows up in analyze profiles.
|
|
17
|
+
await dropFTSIndex(table, indexName);
|
|
6
18
|
await createFTSIndex(table, indexName, [...properties]);
|
|
7
19
|
options?.onIndexReady?.(table, indexName);
|
|
8
20
|
}
|
|
9
21
|
}
|
|
10
22
|
export async function verifySearchFTSIndexes(executeQuery) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
// Read the catalog once and check each configured index both EXISTS and
|
|
24
|
+
// covers its expected columns. A queryability-only probe (CALL QUERY_FTS_INDEX
|
|
25
|
+
// ... catch) is not enough: a stale `name+content`-only index left on a
|
|
26
|
+
// pre-#2299 DB stays queryable yet silently misses `description`, so the probe
|
|
27
|
+
// would pass while doc-comment search is still broken (#2299). SHOW_INDEXES
|
|
28
|
+
// exposes `property_names` (STRING[]) per index, so we assert coverage directly.
|
|
29
|
+
const rows = await executeQuery('CALL SHOW_INDEXES() RETURN *');
|
|
30
|
+
const propsByIndex = new Map();
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
if (typeof row !== 'object' || row === null)
|
|
33
|
+
continue;
|
|
34
|
+
const record = row;
|
|
35
|
+
const indexName = record.index_name;
|
|
36
|
+
const propertyNames = record.property_names;
|
|
37
|
+
if (typeof indexName !== 'string' || !Array.isArray(propertyNames))
|
|
38
|
+
continue;
|
|
39
|
+
propsByIndex.set(indexName, propertyNames.filter((p) => typeof p === 'string'));
|
|
40
|
+
}
|
|
17
41
|
const missing = [];
|
|
18
|
-
for (const { table, indexName } of FTS_INDEXES) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
CALL QUERY_FTS_INDEX('${safeTable}', '${safeIndex}', '__gitnexus_fts_probe__', conjunctive := false)
|
|
23
|
-
RETURN score
|
|
24
|
-
LIMIT 1
|
|
25
|
-
`;
|
|
26
|
-
try {
|
|
27
|
-
await executeQuery(probe);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
42
|
+
for (const { table, indexName, properties } of FTS_INDEXES) {
|
|
43
|
+
const actual = propsByIndex.get(indexName);
|
|
44
|
+
// Absent from the catalog, or present but not covering every expected column.
|
|
45
|
+
if (!actual || !properties.every((p) => actual.includes(p))) {
|
|
30
46
|
missing.push(`${table}.${indexName}`);
|
|
31
47
|
}
|
|
32
48
|
}
|
|
@@ -1,7 +1,40 @@
|
|
|
1
|
+
// Shared by both index creation (`createSearchFTSIndexes`) and querying
|
|
2
|
+
// (`searchFTSFromLbug` / `verifySearchFTSIndexes`) — the single source of truth
|
|
3
|
+
// for which tables/columns are full-text searchable. Adding `description` here
|
|
4
|
+
// makes doc comments (Javadoc/KDoc/JSDoc/Doxygen/godoc/RDoc) keyword-searchable
|
|
5
|
+
// once they are populated by `descriptionExtractor` (#2270/#2286, issue #2299).
|
|
6
|
+
//
|
|
7
|
+
// IMPORTANT: every property must be a real column on its table (see
|
|
8
|
+
// `core/lbug/schema.ts`). `File` has no `description` column, so it stays
|
|
9
|
+
// name+content. All other entries below carry a `description` column.
|
|
10
|
+
//
|
|
11
|
+
// Tables beyond the original 5 mirror `EMBEDDABLE_LABELS` (embeddings/types.ts):
|
|
12
|
+
// indexing the same set keeps a symbol's doc comment both keyword- and
|
|
13
|
+
// semantically-searchable.
|
|
14
|
+
const FTS_PROPERTIES = ['name', 'content', 'description'];
|
|
1
15
|
export const FTS_INDEXES = [
|
|
16
|
+
// File has no `description` column — keep it name+content only.
|
|
2
17
|
{ table: 'File', indexName: 'file_fts', properties: ['name', 'content'] },
|
|
3
|
-
|
|
4
|
-
{ table: '
|
|
5
|
-
{ table: '
|
|
6
|
-
{ table: '
|
|
18
|
+
// Original 5 (minus File) gain `description`.
|
|
19
|
+
{ table: 'Function', indexName: 'function_fts', properties: FTS_PROPERTIES },
|
|
20
|
+
{ table: 'Class', indexName: 'class_fts', properties: FTS_PROPERTIES },
|
|
21
|
+
{ table: 'Method', indexName: 'method_fts', properties: FTS_PROPERTIES },
|
|
22
|
+
{ table: 'Interface', indexName: 'interface_fts', properties: FTS_PROPERTIES },
|
|
23
|
+
// Remaining EMBEDDABLE_LABELS symbol tables — all CODE_ELEMENT_BASE-shaped
|
|
24
|
+
// (or a superset), so all carry name + content + description columns.
|
|
25
|
+
{ table: 'Constructor', indexName: 'constructor_fts', properties: FTS_PROPERTIES },
|
|
26
|
+
{ table: 'Struct', indexName: 'struct_fts', properties: FTS_PROPERTIES },
|
|
27
|
+
{ table: 'Enum', indexName: 'enum_fts', properties: FTS_PROPERTIES },
|
|
28
|
+
{ table: 'Trait', indexName: 'trait_fts', properties: FTS_PROPERTIES },
|
|
29
|
+
{ table: 'Impl', indexName: 'impl_fts', properties: FTS_PROPERTIES },
|
|
30
|
+
{ table: 'Macro', indexName: 'macro_fts', properties: FTS_PROPERTIES },
|
|
31
|
+
{ table: 'Namespace', indexName: 'namespace_fts', properties: FTS_PROPERTIES },
|
|
32
|
+
{ table: 'TypeAlias', indexName: 'type_alias_fts', properties: FTS_PROPERTIES },
|
|
33
|
+
{ table: 'Typedef', indexName: 'typedef_fts', properties: FTS_PROPERTIES },
|
|
34
|
+
{ table: 'Const', indexName: 'const_fts', properties: FTS_PROPERTIES },
|
|
35
|
+
{ table: 'Property', indexName: 'property_fts', properties: FTS_PROPERTIES },
|
|
36
|
+
{ table: 'Record', indexName: 'record_fts', properties: FTS_PROPERTIES },
|
|
37
|
+
{ table: 'Union', indexName: 'union_fts', properties: FTS_PROPERTIES },
|
|
38
|
+
{ table: 'Static', indexName: 'static_fts', properties: FTS_PROPERTIES },
|
|
39
|
+
{ table: 'Variable', indexName: 'variable_fts', properties: FTS_PROPERTIES },
|
|
7
40
|
];
|
|
@@ -52,7 +52,7 @@ import { fileURLToPath } from 'url';
|
|
|
52
52
|
// the main thread (the #1983 OOM). Because the two stores share this version,
|
|
53
53
|
// any future change to the `ParsedFile` serialization shape MUST bump
|
|
54
54
|
// SCHEMA_BUMP so both invalidate in lockstep.
|
|
55
|
-
const SCHEMA_BUMP =
|
|
55
|
+
const SCHEMA_BUMP = 8; // #2288: ParseWorkerResult gained `springTypes` (Spring interface-inheritance) + `decoratorRoutes` semantics changed (interface routes suppressed at extraction, inherited routes appended in parse-impl)
|
|
56
56
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
57
57
|
try {
|
|
58
58
|
// package.json sits at gitnexus/package.json — two levels up from
|
package/package.json
CHANGED
|
@@ -70,6 +70,7 @@ const LBUG_NATIVE = [
|
|
|
70
70
|
'test/integration/local-backend-calltool.test.ts',
|
|
71
71
|
'test/integration/search-core.test.ts',
|
|
72
72
|
'test/integration/search-pool.test.ts',
|
|
73
|
+
'test/integration/fts-description-search.test.ts',
|
|
73
74
|
'test/integration/staleness-and-stability.test.ts',
|
|
74
75
|
'test/integration/analyze-wal-checkpoint-failure.test.ts',
|
|
75
76
|
];
|