gitnexus 1.6.7 → 1.6.8-rc.10
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/README.md +20 -0
- package/dist/cli/analyze-config.js +39 -0
- package/dist/cli/analyze.d.ts +16 -0
- package/dist/cli/analyze.js +34 -6
- package/dist/cli/clean.d.ts +1 -0
- package/dist/cli/clean.js +43 -1
- package/dist/cli/eval-server.js +43 -0
- package/dist/cli/help-i18n.js +6 -0
- package/dist/cli/i18n/en.d.ts +11 -0
- package/dist/cli/i18n/en.js +11 -0
- package/dist/cli/i18n/resources.d.ts +22 -0
- package/dist/cli/i18n/zh-CN.d.ts +11 -0
- package/dist/cli/i18n/zh-CN.js +11 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/list.js +12 -0
- package/dist/cli/optional-grammars.d.ts +6 -8
- package/dist/cli/optional-grammars.js +8 -11
- package/dist/cli/status.js +26 -5
- package/dist/cli/tool.d.ts +5 -0
- package/dist/cli/tool.js +5 -0
- package/dist/core/embeddings/embedder.js +4 -0
- package/dist/core/embeddings/embedding-pipeline.js +27 -16
- package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
- package/dist/core/group/extractors/http-patterns/java.js +3 -50
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
- package/dist/core/group/extractors/include-extractor.js +7 -7
- package/dist/core/ingestion/language-provider.d.ts +25 -2
- package/dist/core/ingestion/languages/c-cpp.js +11 -4
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +3 -2
- package/dist/core/ingestion/languages/kotlin.js +5 -1
- package/dist/core/ingestion/parsing-processor.js +21 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
- package/dist/core/ingestion/pipeline.d.ts +9 -0
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
- package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
- package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
- package/dist/core/ingestion/route-extractors/spring.js +136 -0
- package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
- package/dist/core/ingestion/workers/clone-safety.js +465 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +10 -0
- package/dist/core/ingestion/workers/parse-worker.js +27 -52
- package/dist/core/ingestion/workers/post-result.d.ts +22 -0
- package/dist/core/ingestion/workers/post-result.js +87 -0
- package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
- package/dist/core/ingestion/workers/result-merge.js +43 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +6 -0
- package/dist/core/ingestion/workers/worker-pool.js +28 -14
- package/dist/core/lbug/lbug-adapter.d.ts +19 -0
- package/dist/core/lbug/lbug-adapter.js +56 -1
- package/dist/core/run-analyze.d.ts +45 -0
- package/dist/core/run-analyze.js +175 -24
- package/dist/core/tree-sitter/parser-loader.js +5 -4
- package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
- package/dist/core/tree-sitter/vendored-grammars.js +57 -0
- package/dist/mcp/core/embedder.js +4 -0
- package/dist/mcp/local/local-backend.d.ts +55 -2
- package/dist/mcp/local/local-backend.js +363 -25
- package/dist/mcp/tools.js +32 -0
- package/dist/server/analyze-worker-ipc.d.ts +58 -0
- package/dist/server/analyze-worker-ipc.js +16 -0
- package/dist/server/analyze-worker.js +7 -1
- package/dist/storage/branch-index.d.ts +52 -0
- package/dist/storage/branch-index.js +65 -0
- package/dist/storage/git.d.ts +11 -0
- package/dist/storage/git.js +28 -0
- package/dist/storage/parse-cache.js +3 -0
- package/dist/storage/repo-manager.d.ts +57 -2
- package/dist/storage/repo-manager.js +132 -18
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +24 -4
- package/hooks/claude/gitnexus-hook.cjs +18 -3
- package/package.json +2 -2
- package/scripts/assert-publish-grammar-coverage.cjs +31 -0
- package/scripts/build-tree-sitter-grammars.cjs +16 -10
- package/vendor/tree-sitter-c/package.json +1 -1
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-kotlin/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -1
- package/scripts/materialize-vendor-grammars.cjs +0 -97
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
|
-
import { createRequire } from 'node:module';
|
|
4
3
|
import { glob } from 'glob';
|
|
5
4
|
import Parser from 'tree-sitter';
|
|
6
5
|
import Cpp from 'tree-sitter-cpp';
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
import { requireVendoredGrammar } from '../../tree-sitter/vendored-grammars.js';
|
|
7
|
+
// `tree-sitter-c` is vendored (#2116), loaded from `vendor/` by absolute path
|
|
8
|
+
// (NEVER copied into node_modules — see vendored-grammars.ts / #2111). Load it
|
|
9
|
+
// via a guarded call rather than a top-level `import C from 'tree-sitter-c'`,
|
|
10
|
+
// which would throw ERR_MODULE_NOT_FOUND at module-load and crash analyze
|
|
11
|
+
// (#2091/#2093). It may be absent on a platform without a prebuild; when the
|
|
11
12
|
// binding is absent, `getLanguageForFile` returns null for `.c`/`.h` so C
|
|
12
13
|
// include-extraction is skipped (C++ is unaffected — its binding always ships).
|
|
13
|
-
const _require = createRequire(import.meta.url);
|
|
14
14
|
let C = null;
|
|
15
15
|
try {
|
|
16
|
-
C =
|
|
16
|
+
C = requireVendoredGrammar('tree-sitter-c');
|
|
17
17
|
}
|
|
18
18
|
catch {
|
|
19
19
|
/* C grammar unavailable — C include extraction degrades to a no-op. */
|
|
@@ -20,6 +20,8 @@ import type { VariableExtractor } from './variable-types.js';
|
|
|
20
20
|
import type { ImportResolverFn } from './import-resolvers/types.js';
|
|
21
21
|
import type { SyntaxNode } from './utils/ast-helpers.js';
|
|
22
22
|
import type { NodeLabel } from '../../_shared/index.js';
|
|
23
|
+
import type Parser from 'tree-sitter';
|
|
24
|
+
import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
|
|
23
25
|
/** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
|
|
24
26
|
export type CaptureMap = Record<string, SyntaxNode | undefined>;
|
|
25
27
|
/** Configuration for AST-based framework detection patterns. */
|
|
@@ -145,6 +147,12 @@ interface LanguageProviderConfig {
|
|
|
145
147
|
* `undefined` when no constraints exist / the node isn't a templated
|
|
146
148
|
* function. Languages without SFINAE / concept semantics leave this
|
|
147
149
|
* undefined and the disambiguation is a pass-through.
|
|
150
|
+
*
|
|
151
|
+
* Cloneability contract: the returned payload crosses the worker boundary
|
|
152
|
+
* via structured clone, so it MUST be structured-clone-safe (no functions,
|
|
153
|
+
* symbols, or tree-sitter `SyntaxNode`s — only plain data). Wrap the return
|
|
154
|
+
* with `assertCloneable` from `workers/clone-safety.ts` so a future leak is a
|
|
155
|
+
* compile error at the source instead of a runtime DataCloneError (#2143).
|
|
148
156
|
*/
|
|
149
157
|
readonly extractTemplateConstraints?: (definitionNode: SyntaxNode) => unknown;
|
|
150
158
|
/** Override the default node label for definition.function captures.
|
|
@@ -185,6 +193,17 @@ interface LanguageProviderConfig {
|
|
|
185
193
|
* When true, the worker extracts routes via the language's route extraction logic.
|
|
186
194
|
* Default: undefined (no route files). */
|
|
187
195
|
readonly isRouteFile?: (filePath: string) => boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Extract decorator-style route annotations from a parsed file.
|
|
198
|
+
*
|
|
199
|
+
* When defined, the parse worker calls this after per-file capture processing
|
|
200
|
+
* to extract framework route definitions that require AST-level analysis beyond
|
|
201
|
+
* generic `@decorator` captures (e.g., Java Spring class-level prefix joining,
|
|
202
|
+
* multi-class handling). The returned routes are appended to `decoratorRoutes`.
|
|
203
|
+
*
|
|
204
|
+
* Default: undefined (no language-specific decorator route extraction).
|
|
205
|
+
*/
|
|
206
|
+
readonly extractDecoratorRoutes?: (tree: Parser.Tree, filePath: string, lineOffset: number) => ExtractedDecoratorRoute[];
|
|
188
207
|
/** Built-in/stdlib names that should be filtered from the call graph for this language.
|
|
189
208
|
* Default: undefined (no language-specific filtering). */
|
|
190
209
|
readonly builtInNames?: ReadonlySet<string>;
|
|
@@ -255,8 +274,12 @@ interface LanguageProviderConfig {
|
|
|
255
274
|
* disk store WITHOUT a main-thread re-parse. The main thread restores them
|
|
256
275
|
* via the matching `ScopeResolver.applyCaptureSideChannel` hook.
|
|
257
276
|
*
|
|
258
|
-
* MUST return plain data (objects / arrays /
|
|
259
|
-
*
|
|
277
|
+
* Cloneability contract: MUST return plain data (objects / arrays /
|
|
278
|
+
* primitives — no functions, symbols, or tree-sitter `SyntaxNode`s) so it
|
|
279
|
+
* survives BOTH the worker→main structured clone AND `JSON.stringify` + the
|
|
280
|
+
* parsedfile-store interning reviver. Wrap the return with `assertCloneable`
|
|
281
|
+
* from `workers/clone-safety.ts` so a future non-serializable leak is a
|
|
282
|
+
* compile error at the source instead of a runtime DataCloneError (#2143).
|
|
260
283
|
*
|
|
261
284
|
* Default: undefined (provider has no capture-time module-level side effects).
|
|
262
285
|
*/
|
|
@@ -38,7 +38,8 @@ import { cCallConfig, cppCallConfig } from '../call-extractors/configs/c-cpp.js'
|
|
|
38
38
|
import { stripUeMacros } from '../cpp-ue-preprocessor.js';
|
|
39
39
|
import { emitCScopeCaptures, interpretCImport, interpretCTypeBinding, cArityCompatibility, cBindingScopeFor, cImportOwningScope, cReceiverBinding, collectCStaticLinkageSideChannel, } from './c/index.js';
|
|
40
40
|
import { emitCppScopeCaptures, interpretCppImport, interpretCppTypeBinding, cppArityCompatibility, cppBindingScopeFor, cppImportOwningScope, cppReceiverBinding, collectCppCaptureSideChannel, } from './cpp/index.js';
|
|
41
|
-
import { extractCppTemplateConstraints } from './cpp/constraint-extractor.js';
|
|
41
|
+
import { extractCppTemplateConstraints, } from './cpp/constraint-extractor.js';
|
|
42
|
+
import { assertCloneable } from '../workers/clone-safety.js';
|
|
42
43
|
const C_BUILT_INS = new Set([
|
|
43
44
|
'printf',
|
|
44
45
|
'fprintf',
|
|
@@ -358,7 +359,10 @@ export const cProvider = defineLanguage({
|
|
|
358
359
|
// `static` functions look non-file-local on the main thread and leak into
|
|
359
360
|
// cross-file global free-call resolution / wildcard imports. See
|
|
360
361
|
// `c/capture-side-channel.ts`.
|
|
361
|
-
|
|
362
|
+
// `assertCloneable` is a runtime identity; it makes a future non-serializable
|
|
363
|
+
// value in the side-channel payload a compile error here, at the source, rather
|
|
364
|
+
// than a DataCloneError at the worker boundary (#2143).
|
|
365
|
+
collectCaptureSideChannel: (filePath) => assertCloneable(collectCStaticLinkageSideChannel(filePath)),
|
|
362
366
|
interpretImport: interpretCImport,
|
|
363
367
|
interpretTypeBinding: interpretCTypeBinding,
|
|
364
368
|
bindingScopeFor: cBindingScopeFor,
|
|
@@ -431,7 +435,7 @@ export const cppProvider = defineLanguage({
|
|
|
431
435
|
// just populated for this file into plain data on `ParsedFile.captureSideChannel`,
|
|
432
436
|
// so the main thread can restore them via `applyCaptureSideChannel` WITHOUT a
|
|
433
437
|
// re-parse (#1983). See `cpp/capture-side-channel.ts`.
|
|
434
|
-
collectCaptureSideChannel: collectCppCaptureSideChannel,
|
|
438
|
+
collectCaptureSideChannel: (filePath) => assertCloneable(collectCppCaptureSideChannel(filePath)),
|
|
435
439
|
interpretImport: interpretCppImport,
|
|
436
440
|
interpretTypeBinding: interpretCppTypeBinding,
|
|
437
441
|
bindingScopeFor: cppBindingScopeFor,
|
|
@@ -482,5 +486,8 @@ function extractCppTemplateConstraintsForProvider(definitionNode) {
|
|
|
482
486
|
}
|
|
483
487
|
break;
|
|
484
488
|
}
|
|
485
|
-
|
|
489
|
+
// Guard the boundary at the source: a future non-cloneable member of the
|
|
490
|
+
// constraint payload becomes a compile error here, not a runtime
|
|
491
|
+
// DataCloneError at the worker post (#2143).
|
|
492
|
+
return assertCloneable(extractCppTemplateConstraints(templateDecl, declarator));
|
|
486
493
|
}
|
|
@@ -11,6 +11,7 @@ import { createClassExtractor } from '../class-extractors/generic.js';
|
|
|
11
11
|
import { javaClassConfig } from '../class-extractors/configs/jvm.js';
|
|
12
12
|
import { defineLanguage } from '../language-provider.js';
|
|
13
13
|
import { javaTypeConfig } from '../type-extractors/jvm.js';
|
|
14
|
+
import { extractSpringRoutes } from '../route-extractors/spring.js';
|
|
14
15
|
import { javaExportChecker } from '../export-detection.js';
|
|
15
16
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
16
17
|
import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
|
|
@@ -103,4 +104,6 @@ export const javaProvider = defineLanguage({
|
|
|
103
104
|
arityCompatibility: javaArityCompatibility,
|
|
104
105
|
resolveImportTarget: resolveJavaImportTarget,
|
|
105
106
|
orderSameNameTypeCandidates: orderJavaSameNameTypeCandidates,
|
|
107
|
+
// ── Route extraction ──
|
|
108
|
+
extractDecoratorRoutes: extractSpringRoutes,
|
|
106
109
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
2
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
3
|
-
// `tree-sitter-kotlin` is
|
|
4
|
-
//
|
|
3
|
+
// `tree-sitter-kotlin` is a vendored grammar (loaded from vendor/ by absolute
|
|
4
|
+
// path, never node_modules — vendored-grammars.ts / #2111) that may be absent on
|
|
5
|
+
// a platform without a matching prebuild. Loaded lazily + guarded via parser-loader
|
|
5
6
|
// rather than statically imported: this module is pulled onto the main thread
|
|
6
7
|
// eagerly by the scope-resolution registry and the language-provider index, so
|
|
7
8
|
// a top-level `import Kotlin from 'tree-sitter-kotlin'` would throw
|
|
@@ -10,6 +10,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
|
|
|
10
10
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
11
11
|
import { kotlinClassConfig } from '../class-extractors/configs/jvm.js';
|
|
12
12
|
import { defineLanguage } from '../language-provider.js';
|
|
13
|
+
import { assertCloneable } from '../workers/clone-safety.js';
|
|
13
14
|
import { kotlinTypeConfig } from '../type-extractors/jvm.js';
|
|
14
15
|
import { kotlinExportChecker } from '../export-detection.js';
|
|
15
16
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
@@ -166,7 +167,10 @@ export const kotlinProvider = defineLanguage({
|
|
|
166
167
|
// so the main thread can restore them via `applyCaptureSideChannel` WITHOUT a
|
|
167
168
|
// re-parse (#1983). Without this, companion/static dispatch emits no CALLS
|
|
168
169
|
// edges on the worker path. See `kotlin/capture-side-channel.ts`.
|
|
169
|
-
|
|
170
|
+
// `assertCloneable` is a runtime identity; it makes a future non-serializable
|
|
171
|
+
// value in the side-channel payload a compile error here, at the source, rather
|
|
172
|
+
// than a DataCloneError at the worker boundary (#2143).
|
|
173
|
+
collectCaptureSideChannel: (filePath) => assertCloneable(collectKotlinCaptureSideChannel(filePath)),
|
|
170
174
|
interpretImport: interpretKotlinImport,
|
|
171
175
|
interpretTypeBinding: interpretKotlinTypeBinding,
|
|
172
176
|
bindingScopeFor: kotlinBindingScopeFor,
|
|
@@ -144,6 +144,27 @@ chunkHash) => {
|
|
|
144
144
|
.join(', ');
|
|
145
145
|
logger.warn(` Skipped unsupported languages: ${summary}`);
|
|
146
146
|
}
|
|
147
|
+
// Clone-safety telemetry (#2112): files whose parse output carried a value
|
|
148
|
+
// the structured-clone algorithm couldn't serialize across the worker
|
|
149
|
+
// boundary. The worker sanitized/dropped the offending value so the run
|
|
150
|
+
// could complete; surface the (rare) data loss so it's visible and the
|
|
151
|
+
// offending extractor can be fixed at source.
|
|
152
|
+
const skippedPaths = [];
|
|
153
|
+
for (const result of chunkResults) {
|
|
154
|
+
for (const entry of result.skippedPaths ?? [])
|
|
155
|
+
skippedPaths.push(entry);
|
|
156
|
+
}
|
|
157
|
+
if (skippedPaths.length > 0) {
|
|
158
|
+
// Keep the per-file reason ("stripped N value(s) from nodes" /
|
|
159
|
+
// "dropped non-serializable parsedFiles entry") — it distinguishes a
|
|
160
|
+
// recoverable strip from a whole-record drop, which a path-only line loses.
|
|
161
|
+
const shown = skippedPaths
|
|
162
|
+
.slice(0, 10)
|
|
163
|
+
.map((e) => `${e.path} (${e.reason})`)
|
|
164
|
+
.join(', ');
|
|
165
|
+
const more = skippedPaths.length > 10 ? ` …and ${skippedPaths.length - 10} more` : '';
|
|
166
|
+
logger.warn(` Sanitized ${skippedPaths.length} file(s) with non-serializable parse output: ${shown}${more}`);
|
|
167
|
+
}
|
|
147
168
|
onFileProgress?.(total, total, 'done');
|
|
148
169
|
return chunkResults;
|
|
149
170
|
};
|
|
@@ -297,22 +297,72 @@ export const routesPhase = {
|
|
|
297
297
|
// scan JS/TS consumer files for calls to those wrapper functions with
|
|
298
298
|
// URL-like string arguments and add them to allFetchCalls so
|
|
299
299
|
// processNextjsFetchRoutes can create FETCHES edges.
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
// Wrapper names come from two sources: functions the parse phase
|
|
301
|
+
// auto-detected as calling the bare global `fetch()`, plus any names the
|
|
302
|
+
// user declared in `.gitnexusrc` `fetchWrappers` (#1589/#1852 residual).
|
|
303
|
+
// Config names let an axios/custom-client wrapper — or one named outside the
|
|
304
|
+
// built-in convention — still produce route_map consumers; without them it
|
|
305
|
+
// silently falls back to `consumers: []`. Configured names alone are enough
|
|
306
|
+
// to run the scan even when nothing was auto-detected.
|
|
307
|
+
// Configured names are already validated/trimmed/de-duped/capped by
|
|
308
|
+
// analyze-config.ts — trusted as-is (#1589/#1852 review F9, dropped the
|
|
309
|
+
// redundant re-trim/re-filter). The single filter below guards only the
|
|
310
|
+
// auto-detected `functionName`s, which have no shape guarantee.
|
|
311
|
+
const configuredWrappers = ctx.options?.fetchWrappers ?? [];
|
|
312
|
+
const wrapperNames = new Set([...(allFetchWrapperDefs ?? []).map((d) => d.functionName), ...configuredWrappers].filter((n) => typeof n === 'string' && n.trim().length > 0));
|
|
313
|
+
if (wrapperNames.size > 0 && routeRegistry.size > 0) {
|
|
302
314
|
const jsFiles = allPaths.filter((p) => /\.[jt]sx?$/.test(p));
|
|
303
|
-
if (jsFiles.length > 0
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
+
if (jsFiles.length > 0) {
|
|
316
|
+
// Reuse contents already read for handler extraction; only read the
|
|
317
|
+
// remainder (mirrors the Expo block above). Avoids a second full read of
|
|
318
|
+
// files we already have in memory.
|
|
319
|
+
const unreadJsFiles = jsFiles.filter((p) => !handlerContents?.has(p));
|
|
320
|
+
const extraContents = unreadJsFiles.length > 0
|
|
321
|
+
? await readFileContents(ctx.repoPath, unreadJsFiles)
|
|
322
|
+
: new Map();
|
|
323
|
+
// One alternation regex over every wrapper name per file — O(files), not
|
|
324
|
+
// O(files × wrappers) (#1852 review F3). Names are escaped and grouped
|
|
325
|
+
// non-capturing so capture group 1 stays the URL. The left boundary is a
|
|
326
|
+
// negative lookbehind, not `\b`: a bare configured name like `get` must
|
|
327
|
+
// match the free call `get('/x')` but NOT a member access `client.get(`
|
|
328
|
+
// (a `.get(` on an unrelated object), and `apiFetch` must not match
|
|
329
|
+
// `myApiFetch`. Member-style wrappers are configured with the dot
|
|
330
|
+
// (`client.get`), where the `.` is part of the pattern. The `u` flag +
|
|
331
|
+
// Unicode property classes make the boundary cover non-ASCII identifier
|
|
332
|
+
// characters too — ASCII `\w` would let `caféget('/x')` match `get`
|
|
333
|
+
// (#1852 review F10).
|
|
334
|
+
const alternation = [...wrapperNames].map(escapeRegex).join('|');
|
|
335
|
+
const wrapperCallRegex = new RegExp(`(?<![.\\p{L}\\p{N}_$])(?:${alternation})\\s*\\(\\s*['"\`](/[^'"\`\\s)]+)['"\`]`, 'gu');
|
|
336
|
+
const scanContent = (filePath, content) => {
|
|
337
|
+
wrapperCallRegex.lastIndex = 0;
|
|
338
|
+
// 1-based line number via a running newline counter: matches arrive in
|
|
339
|
+
// ascending index, so accumulate newlines incrementally instead of
|
|
340
|
+
// re-allocating `content.substring(0, match.index).split('\n')` on
|
|
341
|
+
// every match (#1852 review F12). Output is identical.
|
|
342
|
+
let line = 1;
|
|
343
|
+
let scanned = 0;
|
|
344
|
+
let match;
|
|
345
|
+
while ((match = wrapperCallRegex.exec(content)) !== null) {
|
|
346
|
+
for (; scanned < match.index; scanned++) {
|
|
347
|
+
if (content.charCodeAt(scanned) === 10 /* '\n' */)
|
|
348
|
+
line++;
|
|
315
349
|
}
|
|
350
|
+
allFetchCalls.push({
|
|
351
|
+
filePath,
|
|
352
|
+
fetchURL: match[1],
|
|
353
|
+
lineNumber: line,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
for (const [filePath, content] of extraContents)
|
|
358
|
+
scanContent(filePath, content);
|
|
359
|
+
// Also scan already-read JS/TS handler files (a handler can itself
|
|
360
|
+
// consume another route through a wrapper).
|
|
361
|
+
if (handlerContents) {
|
|
362
|
+
for (const p of jsFiles) {
|
|
363
|
+
const cached = handlerContents.get(p);
|
|
364
|
+
if (cached !== undefined)
|
|
365
|
+
scanContent(p, cached);
|
|
316
366
|
}
|
|
317
367
|
}
|
|
318
368
|
}
|
|
@@ -104,6 +104,15 @@ export interface PipelineOptions {
|
|
|
104
104
|
* `process.env` state across invocations. When undefined, the env var decides.
|
|
105
105
|
*/
|
|
106
106
|
keepLocalValueSymbols?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Extra fetch-wrapper function names to treat as HTTP consumers, threaded
|
|
109
|
+
* from `.gitnexusrc` `fetchWrappers` via `AnalyzeOptions` (#1589/#1852
|
|
110
|
+
* residual). The routes phase unions these with the auto-detected `fetch()`
|
|
111
|
+
* wrappers when scanning for `route_map` consumers, so a wrapper named outside
|
|
112
|
+
* the built-in convention (or built on axios / a custom client) is still
|
|
113
|
+
* traced. Empty/undefined leaves behavior unchanged.
|
|
114
|
+
*/
|
|
115
|
+
fetchWrappers?: readonly string[];
|
|
107
116
|
}
|
|
108
117
|
/**
|
|
109
118
|
* All pipeline phases with their dependency relationships.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Spring route-annotation primitives.
|
|
3
|
+
*
|
|
4
|
+
* These are the low-level building blocks the two Spring route extractors —
|
|
5
|
+
* the ingestion-layer `route-extractors/spring.ts` (produces graph `Route`
|
|
6
|
+
* nodes) and the group-layer `group/extractors/http-patterns/java.ts`
|
|
7
|
+
* (produces cross-repo HTTP contracts) — would otherwise each maintain
|
|
8
|
+
* independently. Centralising the annotation→method map, the enclosing-class
|
|
9
|
+
* lookup, and the route-key filter keeps those semantics in one place so the
|
|
10
|
+
* two extractors can't drift apart.
|
|
11
|
+
*
|
|
12
|
+
* This module lives in `ingestion/` (the lower layer); the group layer imports
|
|
13
|
+
* from it, matching the existing `group → ingestion` dependency direction
|
|
14
|
+
* (e.g. `group/extractors/include-extractor.ts` already imports
|
|
15
|
+
* `ingestion/import-resolvers/utils.ts`). It MUST NOT import anything from
|
|
16
|
+
* `group/` to avoid a dependency cycle.
|
|
17
|
+
*/
|
|
18
|
+
import type Parser from 'tree-sitter';
|
|
19
|
+
/**
|
|
20
|
+
* Spring shortcut method-annotation → HTTP verb.
|
|
21
|
+
*
|
|
22
|
+
* `@RequestMapping` is intentionally absent: on a method it carries no implicit
|
|
23
|
+
* verb (the verb lives in its `method = RequestMethod.X` attribute), and on a
|
|
24
|
+
* class it is a URL prefix rather than a route. Callers handle `@RequestMapping`
|
|
25
|
+
* separately.
|
|
26
|
+
*/
|
|
27
|
+
export declare const METHOD_ANNOTATION_TO_HTTP: Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* A named annotation argument contributes a route only when its member key is
|
|
30
|
+
* `path` or `value`; a positional argument (no key node) always qualifies.
|
|
31
|
+
* Drops Spring's non-route string attributes (`produces`, `consumes`,
|
|
32
|
+
* `headers`, `name`, `params`) that would otherwise be mis-read as routes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isRouteMemberKey(keyNode: Parser.SyntaxNode | undefined): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Find the nearest enclosing `class_declaration` ancestor for a node, or null
|
|
37
|
+
* if the node is top-level. Tree-sitter's `SyntaxNode.parent` walks one level
|
|
38
|
+
* at a time.
|
|
39
|
+
*/
|
|
40
|
+
export declare function findEnclosingClass(node: Parser.SyntaxNode): Parser.SyntaxNode | null;
|
|
41
|
+
/**
|
|
42
|
+
* Strip enclosing quotes from a tree-sitter string-literal node's text.
|
|
43
|
+
* Handles single / double / template (backtick) quotes and triple-quoted
|
|
44
|
+
* strings. Mirrors the safer semantics of the group layer's `unquoteLiteral`:
|
|
45
|
+
* returns `null` for empty / nullish input so callers can uniformly skip
|
|
46
|
+
* captures whose value is missing, and returns the text unchanged when it
|
|
47
|
+
* carries no recognisable surrounding quotes (some grammars expose string
|
|
48
|
+
* content without quotes already).
|
|
49
|
+
*/
|
|
50
|
+
export declare function unquoteSpringLiteral(raw: string): string | null;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Spring route-annotation primitives.
|
|
3
|
+
*
|
|
4
|
+
* These are the low-level building blocks the two Spring route extractors —
|
|
5
|
+
* the ingestion-layer `route-extractors/spring.ts` (produces graph `Route`
|
|
6
|
+
* nodes) and the group-layer `group/extractors/http-patterns/java.ts`
|
|
7
|
+
* (produces cross-repo HTTP contracts) — would otherwise each maintain
|
|
8
|
+
* independently. Centralising the annotation→method map, the enclosing-class
|
|
9
|
+
* lookup, and the route-key filter keeps those semantics in one place so the
|
|
10
|
+
* two extractors can't drift apart.
|
|
11
|
+
*
|
|
12
|
+
* This module lives in `ingestion/` (the lower layer); the group layer imports
|
|
13
|
+
* from it, matching the existing `group → ingestion` dependency direction
|
|
14
|
+
* (e.g. `group/extractors/include-extractor.ts` already imports
|
|
15
|
+
* `ingestion/import-resolvers/utils.ts`). It MUST NOT import anything from
|
|
16
|
+
* `group/` to avoid a dependency cycle.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Spring shortcut method-annotation → HTTP verb.
|
|
20
|
+
*
|
|
21
|
+
* `@RequestMapping` is intentionally absent: on a method it carries no implicit
|
|
22
|
+
* verb (the verb lives in its `method = RequestMethod.X` attribute), and on a
|
|
23
|
+
* class it is a URL prefix rather than a route. Callers handle `@RequestMapping`
|
|
24
|
+
* separately.
|
|
25
|
+
*/
|
|
26
|
+
export const METHOD_ANNOTATION_TO_HTTP = {
|
|
27
|
+
GetMapping: 'GET',
|
|
28
|
+
PostMapping: 'POST',
|
|
29
|
+
PutMapping: 'PUT',
|
|
30
|
+
DeleteMapping: 'DELETE',
|
|
31
|
+
PatchMapping: 'PATCH',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A named annotation argument contributes a route only when its member key is
|
|
35
|
+
* `path` or `value`; a positional argument (no key node) always qualifies.
|
|
36
|
+
* Drops Spring's non-route string attributes (`produces`, `consumes`,
|
|
37
|
+
* `headers`, `name`, `params`) that would otherwise be mis-read as routes.
|
|
38
|
+
*/
|
|
39
|
+
export function isRouteMemberKey(keyNode) {
|
|
40
|
+
if (!keyNode)
|
|
41
|
+
return true;
|
|
42
|
+
return keyNode.text === 'path' || keyNode.text === 'value';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Find the nearest enclosing `class_declaration` ancestor for a node, or null
|
|
46
|
+
* if the node is top-level. Tree-sitter's `SyntaxNode.parent` walks one level
|
|
47
|
+
* at a time.
|
|
48
|
+
*/
|
|
49
|
+
export function findEnclosingClass(node) {
|
|
50
|
+
let cur = node.parent;
|
|
51
|
+
while (cur) {
|
|
52
|
+
if (cur.type === 'class_declaration')
|
|
53
|
+
return cur;
|
|
54
|
+
cur = cur.parent;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Strip enclosing quotes from a tree-sitter string-literal node's text.
|
|
60
|
+
* Handles single / double / template (backtick) quotes and triple-quoted
|
|
61
|
+
* strings. Mirrors the safer semantics of the group layer's `unquoteLiteral`:
|
|
62
|
+
* returns `null` for empty / nullish input so callers can uniformly skip
|
|
63
|
+
* captures whose value is missing, and returns the text unchanged when it
|
|
64
|
+
* carries no recognisable surrounding quotes (some grammars expose string
|
|
65
|
+
* content without quotes already).
|
|
66
|
+
*/
|
|
67
|
+
export function unquoteSpringLiteral(raw) {
|
|
68
|
+
if (!raw)
|
|
69
|
+
return null;
|
|
70
|
+
if ((raw.startsWith('"""') && raw.endsWith('"""')) ||
|
|
71
|
+
(raw.startsWith("'''") && raw.endsWith("'''"))) {
|
|
72
|
+
return raw.slice(3, -3);
|
|
73
|
+
}
|
|
74
|
+
const first = raw[0];
|
|
75
|
+
const last = raw[raw.length - 1];
|
|
76
|
+
if ((first === '"' || first === "'" || first === '`') && last === first && raw.length >= 2) {
|
|
77
|
+
return raw.slice(1, -1);
|
|
78
|
+
}
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring route annotation extractor for the ingestion pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Extracts `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`,
|
|
5
|
+
* `@PatchMapping`, and `@RequestMapping` annotations from Java source files
|
|
6
|
+
* and returns `ExtractedDecoratorRoute[]` with class-level `@RequestMapping`
|
|
7
|
+
* prefixes already resolved per-class.
|
|
8
|
+
*
|
|
9
|
+
* This module is the ingestion-layer counterpart of
|
|
10
|
+
* `group/extractors/http-patterns/java.ts` (which extracts HTTP contracts
|
|
11
|
+
* for cross-repo matching). It uses the same tree-sitter capture approach:
|
|
12
|
+
* a single predicate-free query matches all route annotations generically,
|
|
13
|
+
* then a for-loop discriminates class-level prefixes from method-level routes
|
|
14
|
+
* by reading `@node.type` and the annotation name.
|
|
15
|
+
*
|
|
16
|
+
* The query is predicate-free to avoid the tree-sitter 0.21.x hazard where
|
|
17
|
+
* `#match?` / `#eq?` predicates in a top-level `[...]` alternation silently
|
|
18
|
+
* drop sibling-branch matches (see group-layer `JAVA_ROUTE_ANNOTATION_PATTERNS`
|
|
19
|
+
* header comment for details).
|
|
20
|
+
*/
|
|
21
|
+
import Parser from 'tree-sitter';
|
|
22
|
+
import type { ExtractedDecoratorRoute } from '../workers/parse-worker.js';
|
|
23
|
+
/**
|
|
24
|
+
* Extract Spring route annotations from a parsed Java file.
|
|
25
|
+
*
|
|
26
|
+
* Uses a single tree-sitter query pass to capture all annotations, then
|
|
27
|
+
* discriminates class-level prefixes from method-level routes in a loop.
|
|
28
|
+
* Handles multiple classes per file, each with its own prefix.
|
|
29
|
+
*
|
|
30
|
+
* @param tree - tree-sitter parse tree
|
|
31
|
+
* @param filePath - relative file path (for `ExtractedDecoratorRoute.filePath`)
|
|
32
|
+
* @param lineOffset - line offset for pre-processing (usually 0)
|
|
33
|
+
* @returns Decorator routes with prefix already set per-class
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractSpringRoutes(tree: Parser.Tree, filePath: string, lineOffset?: number): ExtractedDecoratorRoute[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring route annotation extractor for the ingestion pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Extracts `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`,
|
|
5
|
+
* `@PatchMapping`, and `@RequestMapping` annotations from Java source files
|
|
6
|
+
* and returns `ExtractedDecoratorRoute[]` with class-level `@RequestMapping`
|
|
7
|
+
* prefixes already resolved per-class.
|
|
8
|
+
*
|
|
9
|
+
* This module is the ingestion-layer counterpart of
|
|
10
|
+
* `group/extractors/http-patterns/java.ts` (which extracts HTTP contracts
|
|
11
|
+
* for cross-repo matching). It uses the same tree-sitter capture approach:
|
|
12
|
+
* a single predicate-free query matches all route annotations generically,
|
|
13
|
+
* then a for-loop discriminates class-level prefixes from method-level routes
|
|
14
|
+
* by reading `@node.type` and the annotation name.
|
|
15
|
+
*
|
|
16
|
+
* The query is predicate-free to avoid the tree-sitter 0.21.x hazard where
|
|
17
|
+
* `#match?` / `#eq?` predicates in a top-level `[...]` alternation silently
|
|
18
|
+
* drop sibling-branch matches (see group-layer `JAVA_ROUTE_ANNOTATION_PATTERNS`
|
|
19
|
+
* header comment for details).
|
|
20
|
+
*/
|
|
21
|
+
import Parser from 'tree-sitter';
|
|
22
|
+
import Java from 'tree-sitter-java';
|
|
23
|
+
import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, unquoteSpringLiteral, } from './spring-shared.js';
|
|
24
|
+
/**
|
|
25
|
+
* Single predicate-free tree-sitter query that captures all route annotations
|
|
26
|
+
* on classes and methods. Discrimination by annotation name and node type
|
|
27
|
+
* happens in the loop below.
|
|
28
|
+
*
|
|
29
|
+
* Captures:
|
|
30
|
+
* @ann → annotation name identifier (RequestMapping, GetMapping, etc.)
|
|
31
|
+
* @node → enclosing declaration (class_declaration | method_declaration)
|
|
32
|
+
* @value → the string-literal argument
|
|
33
|
+
* @key → the named-argument member key (absent for positional form)
|
|
34
|
+
*/
|
|
35
|
+
const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
|
|
36
|
+
[
|
|
37
|
+
(class_declaration
|
|
38
|
+
(modifiers
|
|
39
|
+
(annotation
|
|
40
|
+
name: (identifier) @ann
|
|
41
|
+
arguments: (annotation_argument_list (string_literal) @value)))) @node
|
|
42
|
+
(class_declaration
|
|
43
|
+
(modifiers
|
|
44
|
+
(annotation
|
|
45
|
+
name: (identifier) @ann
|
|
46
|
+
arguments: (annotation_argument_list
|
|
47
|
+
(element_value_pair
|
|
48
|
+
key: (identifier) @key
|
|
49
|
+
value: (string_literal) @value))))) @node
|
|
50
|
+
(method_declaration
|
|
51
|
+
(modifiers
|
|
52
|
+
(annotation
|
|
53
|
+
name: (identifier) @ann
|
|
54
|
+
arguments: (annotation_argument_list (string_literal) @value)))) @node
|
|
55
|
+
(method_declaration
|
|
56
|
+
(modifiers
|
|
57
|
+
(annotation
|
|
58
|
+
name: (identifier) @ann
|
|
59
|
+
arguments: (annotation_argument_list
|
|
60
|
+
(element_value_pair
|
|
61
|
+
key: (identifier) @key
|
|
62
|
+
value: (string_literal) @value))))) @node
|
|
63
|
+
]
|
|
64
|
+
`);
|
|
65
|
+
/**
|
|
66
|
+
* Extract Spring route annotations from a parsed Java file.
|
|
67
|
+
*
|
|
68
|
+
* Uses a single tree-sitter query pass to capture all annotations, then
|
|
69
|
+
* discriminates class-level prefixes from method-level routes in a loop.
|
|
70
|
+
* Handles multiple classes per file, each with its own prefix.
|
|
71
|
+
*
|
|
72
|
+
* @param tree - tree-sitter parse tree
|
|
73
|
+
* @param filePath - relative file path (for `ExtractedDecoratorRoute.filePath`)
|
|
74
|
+
* @param lineOffset - line offset for pre-processing (usually 0)
|
|
75
|
+
* @returns Decorator routes with prefix already set per-class
|
|
76
|
+
*/
|
|
77
|
+
export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
|
|
78
|
+
const matches = ROUTE_ANNOTATION_QUERY.matches(tree.rootNode);
|
|
79
|
+
// Phase 1: collect class-level @RequestMapping prefixes keyed by node id
|
|
80
|
+
const prefixByClassId = new Map();
|
|
81
|
+
for (const match of matches) {
|
|
82
|
+
const caps = {};
|
|
83
|
+
for (const { name, node } of match.captures) {
|
|
84
|
+
caps[name] = node;
|
|
85
|
+
}
|
|
86
|
+
const annNode = caps['ann'];
|
|
87
|
+
const node = caps['node'];
|
|
88
|
+
const valueNode = caps['value'];
|
|
89
|
+
const keyNode = caps['key'];
|
|
90
|
+
if (!annNode || !node || !valueNode)
|
|
91
|
+
continue;
|
|
92
|
+
if (node.type === 'class_declaration' && annNode.text === 'RequestMapping') {
|
|
93
|
+
if (!isRouteMemberKey(keyNode))
|
|
94
|
+
continue;
|
|
95
|
+
const prefix = unquoteSpringLiteral(valueNode.text);
|
|
96
|
+
if (prefix !== null)
|
|
97
|
+
prefixByClassId.set(node.id, prefix);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Phase 2: collect method-level routes and resolve their class prefix
|
|
101
|
+
const routes = [];
|
|
102
|
+
for (const match of matches) {
|
|
103
|
+
const caps = {};
|
|
104
|
+
for (const { name, node } of match.captures) {
|
|
105
|
+
caps[name] = node;
|
|
106
|
+
}
|
|
107
|
+
const annNode = caps['ann'];
|
|
108
|
+
const node = caps['node'];
|
|
109
|
+
const valueNode = caps['value'];
|
|
110
|
+
const keyNode = caps['key'];
|
|
111
|
+
if (!annNode || !node || !valueNode)
|
|
112
|
+
continue;
|
|
113
|
+
if (node.type !== 'method_declaration')
|
|
114
|
+
continue;
|
|
115
|
+
const ann = annNode.text;
|
|
116
|
+
const httpMethod = METHOD_ANNOTATION_TO_HTTP[ann];
|
|
117
|
+
if (!httpMethod)
|
|
118
|
+
continue; // skip @RequestMapping on methods (ambiguous verb)
|
|
119
|
+
if (!isRouteMemberKey(keyNode))
|
|
120
|
+
continue;
|
|
121
|
+
const routePath = unquoteSpringLiteral(valueNode.text);
|
|
122
|
+
if (routePath === null)
|
|
123
|
+
continue;
|
|
124
|
+
const enclosingClass = findEnclosingClass(node);
|
|
125
|
+
const classPrefix = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? '') : '';
|
|
126
|
+
routes.push({
|
|
127
|
+
filePath,
|
|
128
|
+
routePath,
|
|
129
|
+
httpMethod,
|
|
130
|
+
decoratorName: ann,
|
|
131
|
+
lineNumber: annNode.startPosition.row + lineOffset,
|
|
132
|
+
...(classPrefix ? { prefix: classPrefix } : {}),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return routes;
|
|
136
|
+
}
|