gitnexus 1.6.6-rc.75 → 1.6.6-rc.77

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.
@@ -1,18 +1,26 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
3
3
  /**
4
- * Kotlin HTTP plugin (Spring providers).
4
+ * Kotlin HTTP plugin (Spring providers + consumers).
5
5
  *
6
- * Mirrors the Java plugin for Spring `@RequestMapping` class prefixes
7
- * and `@(Get|Post|...)Mapping` method annotations on Kotlin Spring
8
- * Boot controllers. Both positional shorthand (`@GetMapping("/x")`)
9
- * and named annotation arguments (`@GetMapping(value = "/x")` and
6
+ * **Providers** (#1849) Spring `@RequestMapping` class prefixes and
7
+ * `@(Get|Post|...)Mapping` method annotations on Kotlin Spring Boot
8
+ * controllers. Both positional shorthand (`@GetMapping("/x")`) and
9
+ * named annotation arguments (`@GetMapping(value = "/x")` and
10
10
  * `@GetMapping(path = "/x")`) are supported.
11
11
  *
12
- * Consumer detection (RestTemplate / WebClient / OkHttp) is intentionally
13
- * out of scope for this plugin — Kotlin call-site ASTs are sufficiently
14
- * different from Java's `method_invocation` shape that they warrant a
15
- * separate, focused follow-up.
12
+ * **Consumers** (this PR) three call-site patterns common in Kotlin
13
+ * Spring projects:
14
+ *
15
+ * 1. `restTemplate.getForObject("/x", ...)` and friends
16
+ * 2. `webClient.get().uri("/x")` (short form, 1 verb hop + 1 uri hop)
17
+ * 3. `Request.Builder().url("/x")` (OkHttp)
18
+ *
19
+ * The long-form `webClient.method(HttpMethod.X).uri("/y")` chain is
20
+ * intentionally deferred to a follow-up: it requires walk-up logic
21
+ * to recover the verb from a sibling `call_expression`, and we can
22
+ * land 80% of real-world Kotlin Spring consumer coverage with the
23
+ * three simpler patterns above.
16
24
  *
17
25
  * tree-sitter-kotlin (fwcd) AST shapes used here:
18
26
  * class_declaration
@@ -26,6 +34,20 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
26
34
  * string_literal
27
35
  * type_identifier ← class name
28
36
  *
37
+ * Consumer call shape (Kotlin chains everything via `navigation_expression`):
38
+ * call_expression ← outer `.uri("/x")` or `.url("/x")`
39
+ * navigation_expression
40
+ * call_expression ← inner `.get()` / `Request.Builder()` / `restTemplate.x`
41
+ * navigation_expression
42
+ * simple_identifier ← receiver: `webClient` / `Request` / `restTemplate`
43
+ * navigation_suffix ← `.method` / `.Builder` / `.getForObject`
44
+ * call_suffix (value_arguments)
45
+ * navigation_suffix ← `.uri` / `.url`
46
+ * call_suffix
47
+ * value_arguments
48
+ * value_argument
49
+ * string_literal ← the path
50
+ *
29
51
  * tree-sitter-kotlin is an optional npm dependency — when its native
30
52
  * binding is unavailable the plugin gracefully exports `null` and
31
53
  * `http-patterns/index.ts` skips registration for `.kt`/`.kts` files.
@@ -46,6 +68,34 @@ const METHOD_ANNOTATION_TO_HTTP = {
46
68
  DeleteMapping: 'DELETE',
47
69
  PatchMapping: 'PATCH',
48
70
  };
71
+ /**
72
+ * RestTemplate method-name → HTTP verb. Mirrors the Java plugin's
73
+ * `REST_TEMPLATE_TO_HTTP` (java.ts) so a polyglot repo emits the
74
+ * same contract IDs from .java and .kt sources.
75
+ */
76
+ const REST_TEMPLATE_TO_HTTP = {
77
+ getForObject: 'GET',
78
+ getForEntity: 'GET',
79
+ postForObject: 'POST',
80
+ postForEntity: 'POST',
81
+ put: 'PUT',
82
+ delete: 'DELETE',
83
+ patchForObject: 'PATCH',
84
+ };
85
+ /**
86
+ * WebClient short-form verb → HTTP verb. The reactive WebClient API
87
+ * exposes `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()` as
88
+ * one-liners that return a `RequestHeadersUriSpec` whose `.uri(...)`
89
+ * carries the path. We capture both pieces in a single query (see
90
+ * `WEB_CLIENT_SHORT_PATTERNS` below) and translate the verb here.
91
+ */
92
+ const WEB_CLIENT_SHORT_TO_HTTP = {
93
+ get: 'GET',
94
+ post: 'POST',
95
+ put: 'PUT',
96
+ delete: 'DELETE',
97
+ patch: 'PATCH',
98
+ };
49
99
  /**
50
100
  * Build the plugin only if the Kotlin grammar is available. Compiling
51
101
  * the queries against a null grammar would throw at module load time
@@ -144,6 +194,127 @@ function buildKotlinPlugin(language) {
144
194
  },
145
195
  ],
146
196
  });
197
+ // ─── Consumer: Spring RestTemplate ────────────────────────────────────
198
+ // Kotlin call-site shape mirrors the Java plugin's
199
+ // `REST_TEMPLATE_PATTERNS`, but goes through tree-sitter-kotlin's
200
+ // `navigation_expression` instead of Java's `method_invocation`:
201
+ //
202
+ // restTemplate.getForObject("/x", User::class.java)
203
+ //
204
+ // becomes
205
+ //
206
+ // call_expression
207
+ // navigation_expression
208
+ // simple_identifier "restTemplate"
209
+ // navigation_suffix → simple_identifier "getForObject"
210
+ // call_suffix
211
+ // value_arguments
212
+ // value_argument . string_literal "/x" ← captured
213
+ // value_argument User::class.java
214
+ //
215
+ // The receiver name is constrained to `restTemplate` (#eq? @obj),
216
+ // matching the Java plugin's heuristic. This means a non-conventional
217
+ // field name (e.g. `userServiceTemplate`) will not be picked up;
218
+ // that's the same trade-off already accepted on the Java side.
219
+ const REST_TEMPLATE_PATTERNS = compilePatterns({
220
+ name: 'kotlin-rest-template',
221
+ language,
222
+ patterns: [
223
+ {
224
+ meta: {},
225
+ query: `
226
+ (call_expression
227
+ (navigation_expression
228
+ (simple_identifier) @obj (#eq? @obj "restTemplate")
229
+ (navigation_suffix (simple_identifier) @method))
230
+ (call_suffix
231
+ (value_arguments . (value_argument . (string_literal) @path))))
232
+ `,
233
+ },
234
+ ],
235
+ });
236
+ // ─── Consumer: Spring WebClient (short form) ──────────────────────────
237
+ // Reactive WebClient exposes one-liner verb helpers:
238
+ //
239
+ // webClient.get().uri("/x").retrieve().awaitBody<T>()
240
+ // webClient.post().uri("/x")...
241
+ //
242
+ // The chain `webClient.get().uri("/x")` parses as two nested
243
+ // `call_expression` nodes — the OUTER call is `.uri("/x")` and the
244
+ // INNER call is `webClient.get()`. We anchor on the outer call and
245
+ // require:
246
+ // - inner receiver is `webClient`
247
+ // - inner suffix is one of the HTTP verbs (#match?)
248
+ // - outer suffix is exactly `uri`
249
+ // - outer call's first value_argument is a string literal
250
+ //
251
+ // The long-form `webClient.method(HttpMethod.GET).uri("/x")` chain
252
+ // uses an extra navigation hop and an enum field access — it's
253
+ // intentionally out of scope here (see file header).
254
+ const WEB_CLIENT_SHORT_PATTERNS = compilePatterns({
255
+ name: 'kotlin-web-client-short',
256
+ language,
257
+ patterns: [
258
+ {
259
+ meta: {},
260
+ query: `
261
+ (call_expression
262
+ (navigation_expression
263
+ (call_expression
264
+ (navigation_expression
265
+ (simple_identifier) @obj (#eq? @obj "webClient")
266
+ (navigation_suffix
267
+ (simple_identifier) @verb (#match? @verb "^(get|post|put|delete|patch)$")))
268
+ (call_suffix (value_arguments)))
269
+ (navigation_suffix (simple_identifier) @uri (#eq? @uri "uri")))
270
+ (call_suffix
271
+ (value_arguments . (value_argument . (string_literal) @path))))
272
+ `,
273
+ },
274
+ ],
275
+ });
276
+ // ─── Consumer: OkHttp Request.Builder().url("/x") ─────────────────────
277
+ // Kotlin parses `Request.Builder()` as a `call_expression` whose
278
+ // callee is a `navigation_expression` (Request → .Builder), NOT as
279
+ // Java's `object_creation_expression`. The chain `.url("/x")` then
280
+ // wraps that in another `call_expression`. The query mirrors Java's
281
+ // `OK_HTTP_PATTERNS` (java.ts) but adapts the node types.
282
+ //
283
+ // Receiver `Request` is constrained by name (#eq? @cls); a project
284
+ // that imports OkHttp's `Request` under an alias (`import okhttp3.Request as OkRequest`)
285
+ // would not be picked up — this matches the Java plugin's heuristic.
286
+ //
287
+ // **Known limitation — verb defaults to GET.** OkHttp encodes the
288
+ // verb on a *sibling* call further down the builder chain (e.g.
289
+ // `.post(body)` / `.get()` / `.delete()`), not on `.url(...)` itself.
290
+ // This query intentionally does not walk the chain to recover the
291
+ // verb — it emits `method: 'GET'` for every match, mirroring
292
+ // `java.ts:OK_HTTP_PATTERNS`. So a `Request.Builder().url("/x").post(body).build()`
293
+ // call becomes `http::GET::/x`, not `http::POST::/x`. This is the
294
+ // same trade-off Java has accepted; pinned by an anti-overreach
295
+ // test in `http-route-extractor.test.ts` so a future verb-walk
296
+ // implementation has to update this comment in lockstep.
297
+ const OK_HTTP_PATTERNS = compilePatterns({
298
+ name: 'kotlin-okhttp',
299
+ language,
300
+ patterns: [
301
+ {
302
+ meta: {},
303
+ query: `
304
+ (call_expression
305
+ (navigation_expression
306
+ (call_expression
307
+ (navigation_expression
308
+ (simple_identifier) @cls (#eq? @cls "Request")
309
+ (navigation_suffix (simple_identifier) @builder (#eq? @builder "Builder")))
310
+ (call_suffix (value_arguments)))
311
+ (navigation_suffix (simple_identifier) @method (#eq? @method "url")))
312
+ (call_suffix
313
+ (value_arguments . (value_argument . (string_literal) @path))))
314
+ `,
315
+ },
316
+ ],
317
+ });
147
318
  /**
148
319
  * Find the nearest enclosing class_declaration ancestor for a node, or
149
320
  * null if the node is top-level. Mirrors the Java plugin's helper.
@@ -212,6 +383,65 @@ function buildKotlinPlugin(language) {
212
383
  confidence: 0.8,
213
384
  });
214
385
  }
386
+ // ─── Consumers: RestTemplate ────────────────────────────────────
387
+ for (const match of runCompiledPatterns(REST_TEMPLATE_PATTERNS, tree)) {
388
+ const methodNode = match.captures.method;
389
+ const pathNode = match.captures.path;
390
+ if (!methodNode || !pathNode)
391
+ continue;
392
+ const httpMethod = REST_TEMPLATE_TO_HTTP[methodNode.text];
393
+ if (!httpMethod)
394
+ continue;
395
+ const path = unquoteLiteral(pathNode.text);
396
+ if (path === null)
397
+ continue;
398
+ out.push({
399
+ role: 'consumer',
400
+ framework: 'spring-rest-template',
401
+ method: httpMethod,
402
+ path,
403
+ name: null,
404
+ confidence: 0.7,
405
+ });
406
+ }
407
+ // ─── Consumers: WebClient short form (.get()/.post()/etc → .uri) ─
408
+ for (const match of runCompiledPatterns(WEB_CLIENT_SHORT_PATTERNS, tree)) {
409
+ const verbNode = match.captures.verb;
410
+ const pathNode = match.captures.path;
411
+ if (!verbNode || !pathNode)
412
+ continue;
413
+ const httpMethod = WEB_CLIENT_SHORT_TO_HTTP[verbNode.text];
414
+ if (!httpMethod)
415
+ continue;
416
+ const path = unquoteLiteral(pathNode.text);
417
+ if (path === null)
418
+ continue;
419
+ out.push({
420
+ role: 'consumer',
421
+ framework: 'spring-web-client',
422
+ method: httpMethod,
423
+ path,
424
+ name: null,
425
+ confidence: 0.7,
426
+ });
427
+ }
428
+ // ─── Consumers: OkHttp Request.Builder().url("path") ────────────
429
+ for (const match of runCompiledPatterns(OK_HTTP_PATTERNS, tree)) {
430
+ const pathNode = match.captures.path;
431
+ if (!pathNode)
432
+ continue;
433
+ const path = unquoteLiteral(pathNode.text);
434
+ if (path === null)
435
+ continue;
436
+ out.push({
437
+ role: 'consumer',
438
+ framework: 'okhttp',
439
+ method: 'GET',
440
+ path,
441
+ name: null,
442
+ confidence: 0.7,
443
+ });
444
+ }
215
445
  return out;
216
446
  },
217
447
  };
@@ -19,6 +19,7 @@ import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/as
19
19
  import { splitImportStatement } from './import-decomposer.js';
20
20
  import { getPythonParser, getPythonScopeQuery } from './query.js';
21
21
  import { synthesizeReceiverTypeBinding } from './receiver-binding.js';
22
+ import { synthesizeDependsReferences } from './depends-references.js';
22
23
  import { computePythonArityMetadata } from './arity-metadata.js';
23
24
  import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
24
25
  import { getTreeSitterBufferSize } from '../../constants.js';
@@ -88,6 +89,8 @@ export function emitPythonScopeCaptures(sourceText, _filePath, cachedTree) {
88
89
  const synth = synthesizeReceiverTypeBinding(fnNode);
89
90
  if (synth !== null)
90
91
  out.push(synth);
92
+ for (const depRef of synthesizeDependsReferences(fnNode))
93
+ out.push(depRef);
91
94
  }
92
95
  continue;
93
96
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Synthesize `@reference.call.free` captures for FastAPI `Depends(callable)`
3
+ * parameter defaults.
4
+ *
5
+ * `Depends(get_db)` passes `get_db` as a callable that the DI framework
6
+ * calls on every request. The route handler is functionally a caller of
7
+ * the dependency — impact analysis needs that edge.
8
+ *
9
+ * Tree-sitter can't express "the first argument of a call named Depends
10
+ * inside a parameter default" in a single static query, so we synthesize
11
+ * reference captures in code, mirroring the receiver-binding pattern.
12
+ */
13
+ import type { CaptureMatch } from '../../../../_shared/index.js';
14
+ import { type SyntaxNode } from '../../utils/ast-helpers.js';
15
+ /**
16
+ * Inspect a `function_definition` node's parameters for `Depends(callable)`
17
+ * defaults. Returns one `@reference.call.free` CaptureMatch per dependency.
18
+ */
19
+ export declare function synthesizeDependsReferences(fnNode: SyntaxNode): readonly CaptureMatch[];
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Synthesize `@reference.call.free` captures for FastAPI `Depends(callable)`
3
+ * parameter defaults.
4
+ *
5
+ * `Depends(get_db)` passes `get_db` as a callable that the DI framework
6
+ * calls on every request. The route handler is functionally a caller of
7
+ * the dependency — impact analysis needs that edge.
8
+ *
9
+ * Tree-sitter can't express "the first argument of a call named Depends
10
+ * inside a parameter default" in a single static query, so we synthesize
11
+ * reference captures in code, mirroring the receiver-binding pattern.
12
+ */
13
+ import { nodeToCapture } from '../../utils/ast-helpers.js';
14
+ /**
15
+ * Inspect a `function_definition` node's parameters for `Depends(callable)`
16
+ * defaults. Returns one `@reference.call.free` CaptureMatch per dependency.
17
+ */
18
+ export function synthesizeDependsReferences(fnNode) {
19
+ const params = fnNode.childForFieldName('parameters');
20
+ if (params === null)
21
+ return [];
22
+ const results = [];
23
+ for (let i = 0; i < params.namedChildCount; i++) {
24
+ const param = params.namedChild(i);
25
+ if (param === null)
26
+ continue;
27
+ if (param.type !== 'typed_default_parameter' && param.type !== 'default_parameter') {
28
+ continue;
29
+ }
30
+ const defaultValue = param.childForFieldName('value') ?? param.childForFieldName('default');
31
+ if (defaultValue === null)
32
+ continue;
33
+ const callNode = defaultValue.type === 'call' ? defaultValue : null;
34
+ if (callNode === null)
35
+ continue;
36
+ const fnIdent = callNode.childForFieldName('function');
37
+ if (fnIdent === null || fnIdent.type !== 'identifier' || fnIdent.text !== 'Depends')
38
+ continue;
39
+ const args = callNode.childForFieldName('arguments');
40
+ if (args === null || args.namedChildCount === 0)
41
+ continue;
42
+ const firstArg = args.namedChild(0);
43
+ if (firstArg === null)
44
+ continue;
45
+ if (firstArg.type === 'identifier') {
46
+ results.push({
47
+ '@reference.call.free': nodeToCapture('@reference.call.free', firstArg),
48
+ '@reference.name': nodeToCapture('@reference.name', firstArg),
49
+ });
50
+ continue;
51
+ }
52
+ if (firstArg.type === 'attribute') {
53
+ const attrName = firstArg.childForFieldName('attribute');
54
+ const obj = firstArg.childForFieldName('object');
55
+ if (attrName !== null && obj !== null) {
56
+ results.push({
57
+ '@reference.call.member': nodeToCapture('@reference.call.member', attrName),
58
+ '@reference.name': nodeToCapture('@reference.name', attrName),
59
+ '@reference.receiver': nodeToCapture('@reference.receiver', obj),
60
+ });
61
+ }
62
+ }
63
+ }
64
+ return results;
65
+ }
@@ -3,7 +3,7 @@ import type { SymbolTableWriter, ExtractedHeritage } from './model/index.js';
3
3
  import { ASTCache } from './ast-cache.js';
4
4
  import type { ParsedFile } from '../../_shared/index.js';
5
5
  import { WorkerPool } from './workers/worker-pool.js';
6
- import type { ParseWorkerResult, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileScopeBindings, ExtractedORMQuery } from './workers/parse-worker.js';
6
+ import type { ParseWorkerResult, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileScopeBindings, ExtractedORMQuery, FetchWrapperDef } from './workers/parse-worker.js';
7
7
  export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
8
8
  export interface WorkerExtractedData {
9
9
  imports: ExtractedImport[];
@@ -12,6 +12,7 @@ export interface WorkerExtractedData {
12
12
  heritage: ExtractedHeritage[];
13
13
  routes: ExtractedRoute[];
14
14
  fetchCalls: ExtractedFetchCall[];
15
+ fetchWrapperDefs: FetchWrapperDef[];
15
16
  decoratorRoutes: ExtractedDecoratorRoute[];
16
17
  toolDefs: ExtractedToolDef[];
17
18
  ormQueries: ExtractedORMQuery[];
@@ -35,6 +35,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults) => {
35
35
  const allHeritage = [];
36
36
  const allRoutes = [];
37
37
  const allFetchCalls = [];
38
+ const allFetchWrapperDefs = [];
38
39
  const allDecoratorRoutes = [];
39
40
  const allToolDefs = [];
40
41
  const allORMQueries = [];
@@ -77,6 +78,8 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults) => {
77
78
  allRoutes.push(item);
78
79
  for (const item of result.fetchCalls)
79
80
  allFetchCalls.push(item);
81
+ for (const item of result.fetchWrapperDefs ?? [])
82
+ allFetchWrapperDefs.push(item);
80
83
  for (const item of result.decoratorRoutes)
81
84
  allDecoratorRoutes.push(item);
82
85
  for (const item of result.toolDefs)
@@ -100,6 +103,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults) => {
100
103
  heritage: allHeritage,
101
104
  routes: allRoutes,
102
105
  fetchCalls: allFetchCalls,
106
+ fetchWrapperDefs: allFetchWrapperDefs,
103
107
  decoratorRoutes: allDecoratorRoutes,
104
108
  toolDefs: allToolDefs,
105
109
  ormQueries: allORMQueries,
@@ -132,6 +136,7 @@ outRawResults) => {
132
136
  heritage: [],
133
137
  routes: [],
134
138
  fetchCalls: [],
139
+ fetchWrapperDefs: [],
135
140
  decoratorRoutes: [],
136
141
  toolDefs: [],
137
142
  ormQueries: [],
@@ -16,7 +16,7 @@ import { type ExportedTypeMap } from '../call-processor.js';
16
16
  import { createResolutionContext } from '../model/resolution-context.js';
17
17
  import { ASTCache } from '../ast-cache.js';
18
18
  import { type PipelineProgress } from '../../../_shared/index.js';
19
- import type { ExtractedDecoratorRoute, ExtractedFetchCall, ExtractedORMQuery, ExtractedRoute, ExtractedToolDef } from '../workers/parse-worker.js';
19
+ import type { ExtractedDecoratorRoute, ExtractedFetchCall, ExtractedORMQuery, ExtractedRoute, ExtractedToolDef, FetchWrapperDef } from '../workers/parse-worker.js';
20
20
  import type { KnowledgeGraph } from '../../graph/types.js';
21
21
  import type { PipelineOptions } from '../pipeline.js';
22
22
  type ScannedFile = {
@@ -38,6 +38,7 @@ type ProgressFn = (progress: PipelineProgress) => void;
38
38
  export declare function runChunkedParseAndResolve(graph: KnowledgeGraph, scannedFiles: ScannedFile[], allPaths: string[], totalFiles: number, repoPath: string, pipelineStart: number, onProgress: ProgressFn, options?: PipelineOptions): Promise<{
39
39
  exportedTypeMap: ExportedTypeMap;
40
40
  allFetchCalls: ExtractedFetchCall[];
41
+ allFetchWrapperDefs: FetchWrapperDef[];
41
42
  allExtractedRoutes: ExtractedRoute[];
42
43
  allDecoratorRoutes: ExtractedDecoratorRoute[];
43
44
  allToolDefs: ExtractedToolDef[];
@@ -234,6 +234,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
234
234
  // it, and later wildcard chunks re-run it themselves.
235
235
  let hasSynthesized = false;
236
236
  const allFetchCalls = [];
237
+ const allFetchWrapperDefs = [];
237
238
  const allExtractedRoutes = [];
238
239
  const allDecoratorRoutes = [];
239
240
  const allToolDefs = [];
@@ -530,6 +531,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
530
531
  for (const item of chunkWorkerData.fetchCalls)
531
532
  allFetchCalls.push(item);
532
533
  }
534
+ if (chunkWorkerData.fetchWrapperDefs?.length) {
535
+ for (const item of chunkWorkerData.fetchWrapperDefs)
536
+ allFetchWrapperDefs.push(item);
537
+ }
533
538
  if (chunkWorkerData.routes?.length) {
534
539
  for (const item of chunkWorkerData.routes)
535
540
  allExtractedRoutes.push(item);
@@ -876,6 +881,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
876
881
  return {
877
882
  exportedTypeMap,
878
883
  allFetchCalls,
884
+ allFetchWrapperDefs,
879
885
  allExtractedRoutes,
880
886
  allDecoratorRoutes,
881
887
  allToolDefs,
@@ -18,7 +18,7 @@
18
18
  import type { PipelinePhase } from './types.js';
19
19
  import type { BindingAccumulator } from '../binding-accumulator.js';
20
20
  import type { ParsedFile } from '../../../_shared/index.js';
21
- import type { ExtractedFetchCall, ExtractedRoute, ExtractedDecoratorRoute, ExtractedToolDef, ExtractedORMQuery } from '../workers/parse-worker.js';
21
+ import type { ExtractedFetchCall, ExtractedRoute, ExtractedDecoratorRoute, ExtractedToolDef, ExtractedORMQuery, FetchWrapperDef } from '../workers/parse-worker.js';
22
22
  import type { createResolutionContext } from '../model/resolution-context.js';
23
23
  import type { ASTCache } from '../ast-cache.js';
24
24
  export interface ParseOutput {
@@ -34,6 +34,7 @@ export interface ParseOutput {
34
34
  */
35
35
  readonly exportedTypeMap: ReadonlyMap<string, ReadonlyMap<string, string>>;
36
36
  readonly allFetchCalls: readonly ExtractedFetchCall[];
37
+ readonly allFetchWrapperDefs: readonly FetchWrapperDef[];
37
38
  readonly allExtractedRoutes: readonly ExtractedRoute[];
38
39
  readonly allDecoratorRoutes: readonly ExtractedDecoratorRoute[];
39
40
  readonly allToolDefs: readonly ExtractedToolDef[];
@@ -94,11 +94,14 @@ export function normalizeExtractedRoutePath(routePath, prefix) {
94
94
  const joined = prefixPart ? `/${prefixPart}${pathPart ? `/${pathPart}` : ''}` : `/${pathPart}`;
95
95
  return joined.replace(/\/+/g, '/') || '/';
96
96
  }
97
+ function escapeRegex(s) {
98
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
99
+ }
97
100
  export const routesPhase = {
98
101
  name: 'routes',
99
102
  deps: ['parse'],
100
103
  async execute(ctx, deps) {
101
- const { allPaths, allFetchCalls: parseFetchCalls, allExtractedRoutes, allDecoratorRoutes, } = getPhaseOutput(deps, 'parse');
104
+ const { allPaths, allFetchCalls: parseFetchCalls, allFetchWrapperDefs, allExtractedRoutes, allDecoratorRoutes, } = getPhaseOutput(deps, 'parse');
102
105
  // Local copy — routes phase must not mutate upstream ParseOutput
103
106
  const allFetchCalls = [...parseFetchCalls];
104
107
  const routeRegistry = new Map();
@@ -289,6 +292,31 @@ export const routesPhase = {
289
292
  }
290
293
  }
291
294
  }
295
+ // ── Cross-file fetch wrapper consumer extraction ──
296
+ // When the parse phase discovered functions that internally call fetch(),
297
+ // scan JS/TS consumer files for calls to those wrapper functions with
298
+ // URL-like string arguments and add them to allFetchCalls so
299
+ // processNextjsFetchRoutes can create FETCHES edges.
300
+ if (allFetchWrapperDefs && allFetchWrapperDefs.length > 0 && routeRegistry.size > 0) {
301
+ const wrapperNames = new Set(allFetchWrapperDefs.map((d) => d.functionName));
302
+ const jsFiles = allPaths.filter((p) => /\.[jt]sx?$/.test(p));
303
+ if (jsFiles.length > 0 && wrapperNames.size > 0) {
304
+ const jsContents = await readFileContents(ctx.repoPath, jsFiles);
305
+ for (const [filePath, content] of jsContents) {
306
+ for (const name of wrapperNames) {
307
+ const regex = new RegExp(`\\b${escapeRegex(name)}\\s*\\(\\s*['"\`](/[^'"\`\\s)]+)['"\`]`, 'g');
308
+ let match;
309
+ while ((match = regex.exec(content)) !== null) {
310
+ allFetchCalls.push({
311
+ filePath,
312
+ fetchURL: match[1],
313
+ lineNumber: content.substring(0, match.index).split('\n').length,
314
+ });
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
292
320
  if (routeRegistry.size > 0 && allFetchCalls.length > 0) {
293
321
  const routeURLToFile = new Map();
294
322
  for (const [url, entry] of routeRegistry)
@@ -1,5 +1,5 @@
1
- export declare const TYPESCRIPT_QUERIES = "\n(class_declaration\n name: (type_identifier) @name) @definition.class\n\n(abstract_class_declaration\n name: (type_identifier) @name) @definition.class\n\n(interface_declaration\n name: (type_identifier) @name) @definition.interface\n\n(function_declaration\n name: (identifier) @name) @definition.function\n\n; TypeScript overload signatures (function_signature is a separate node type from function_declaration)\n(function_signature\n name: (identifier) @name) @definition.function\n\n(method_definition\n name: (property_identifier) @name) @definition.method\n\n; ES2022 #private methods (private_property_identifier not matched by property_identifier)\n(method_definition\n name: (private_property_identifier) @name) @definition.method\n\n; Abstract method signatures in abstract classes\n(abstract_method_signature\n name: (property_identifier) @name) @definition.method\n\n; Interface method signatures\n(method_signature\n name: (property_identifier) @name) @definition.method\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (arrow_function))) @definition.function\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (function_expression))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (arrow_function)))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (function_expression)))) @definition.function\n\n; Object-property arrows / function expressions: `{ addItem: () => ... }`.\n; The pair's key field carries the meaningful name. Without these patterns,\n; calls inside the arrow are attributed to the file (issue #1166), and the\n; arrow itself is invisible to context() / impact() despite carrying real\n; behaviour (Zustand actions, TanStack queryFn, React Context providers).\n; String-key variant covers `\"add-item\": () => ...`; computed keys\n; (`[K]: () => ...`) intentionally fall through anonymous.\n(pair\n key: (property_identifier) @name\n value: (arrow_function)) @definition.function\n\n(pair\n key: (property_identifier) @name\n value: (function_expression)) @definition.function\n\n(pair\n key: (string (string_fragment) @name)\n value: (arrow_function)) @definition.function\n\n(pair\n key: (string (string_fragment) @name)\n value: (function_expression)) @definition.function\n\n; HOC-wrapped variable declarations: `const X = HOC((args) => { ... })`.\n; Mirrors the registry-primary patterns in `languages/typescript/query.ts`\n; so the legacy Call-Resolution DAG and the registry-primary pipeline\n; produce the same set of `Function` nodes \u2014 required for the CI parity\n; gate. Covers React.forwardRef / memo / useCallback / useMemo / observer\n; / debounce / user-defined HOC factories. The `var X = HOC(...)` form is\n; mirrored too (registry-primary has it) so that codebases mixing `var` and\n; `const` see identical attribution on both pipelines. See\n; `tsExtractFunctionName` for the resolution logic and the `query.ts`\n; comment for the full anchor-discipline rationale and the chained-\n; array-method trade-off.\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function))))) @definition.function\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression))))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function)))))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression)))))) @definition.function\n\n; `var X = HOC(...)` parity with registry-primary. Legacy code (and any\n; transpiler output that downlevels `const` to `var`) hits this shape.\n(variable_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function))))) @definition.function\n\n(variable_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression))))) @definition.function\n\n; Variable/constant declarations (non-function values).\n; Overlap with @definition.function patterns is handled by parse-worker dedup.\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name)) @definition.const\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name))) @definition.const\n\n; var declarations (mutable, function-scoped)\n(variable_declaration\n (variable_declarator\n name: (identifier) @name)) @definition.variable\n\n(import_statement\n source: (string) @import.source) @import\n\n; Re-export statements: export { X } from './y'\n(export_statement\n source: (string) @import.source) @import\n\n(call_expression\n function: (identifier) @call.name) @call\n\n(call_expression\n function: (member_expression\n property: (property_identifier) @call.name)) @call\n\n; Generic awaited free call: await fn<T>(args)\n; tree-sitter-typescript parses \"await fn<T>(args)\" as a call_expression whose\n; \"function\" field is an await_expression (not a bare identifier), because the\n; grammar resolves the ambiguity between generics and comparisons by consuming\n; \"await fn\" as an expression before attaching <T> as type_arguments.\n(call_expression\n function: (await_expression\n (identifier) @call.name)\n (type_arguments)) @call\n\n; Generic awaited member call: await obj.fn<T>(args)\n(call_expression\n function: (await_expression\n (member_expression\n property: (property_identifier) @call.name))\n (type_arguments)) @call\n\n; Constructor calls: new Foo()\n(new_expression\n constructor: (identifier) @call.name) @call\n\n; Class properties \u2014 public_field_definition covers most TS class fields\n(public_field_definition\n name: (property_identifier) @name) @definition.property\n\n; Private class fields: #address: Address\n(public_field_definition\n name: (private_property_identifier) @name) @definition.property\n\n; Constructor parameter properties: constructor(public address: Address)\n(required_parameter\n (accessibility_modifier)\n pattern: (identifier) @name) @definition.property\n\n; Heritage queries - class extends\n(class_declaration\n name: (type_identifier) @heritage.class\n (class_heritage\n (extends_clause\n value: (identifier) @heritage.extends))) @heritage\n\n; Heritage queries - class implements interface\n(class_declaration\n name: (type_identifier) @heritage.class\n (class_heritage\n (implements_clause\n (type_identifier) @heritage.implements))) @heritage.impl\n\n; Write access: obj.field = value\n(assignment_expression\n left: (member_expression\n object: (_) @assignment.receiver\n property: (property_identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(augmented_assignment_expression\n left: (member_expression\n object: (_) @assignment.receiver\n property: (property_identifier) @assignment.property)\n right: (_)) @assignment\n\n; HTTP consumers: fetch('/path'), axios.get('/path'), $.get('/path'), etc.\n; fetch() \u2014 global function\n(call_expression\n function: (identifier) @_fetch_fn (#eq? @_fetch_fn \"fetch\")\n arguments: (arguments\n [(string (string_fragment) @route.url)\n (template_string) @route.template_url])) @route.fetch\n\n; axios.get/post/put/delete/patch('/path'), $.get/post/ajax({url:'/path'})\n(call_expression\n function: (member_expression\n property: (property_identifier) @http_client.method)\n arguments: (arguments\n (string (string_fragment) @http_client.url))) @http_client\n\n; Decorators: @Controller, @Get, @Post, etc.\n(decorator\n (call_expression\n function: (identifier) @decorator.name\n arguments: (arguments (string (string_fragment) @decorator.arg)?))) @decorator\n\n; Express/Hono route registration: app.get('/path', handler), router.post('/path', fn)\n(call_expression\n function: (member_expression\n property: (property_identifier) @express_route.method)\n arguments: (arguments\n (string (string_fragment) @express_route.path))) @express_route\n";
2
- export declare const JAVASCRIPT_QUERIES = "\n(class_declaration\n name: (identifier) @name) @definition.class\n\n(function_declaration\n name: (identifier) @name) @definition.function\n\n(method_definition\n name: (property_identifier) @name) @definition.method\n\n; ES2022 #private methods\n(method_definition\n name: (private_property_identifier) @name) @definition.method\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (arrow_function))) @definition.function\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (function_expression))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (arrow_function)))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (function_expression)))) @definition.function\n\n; Object-property arrows / function expressions: `{ addItem: () => ... }`.\n; See TYPESCRIPT_QUERIES for rationale (issue #1166).\n(pair\n key: (property_identifier) @name\n value: (arrow_function)) @definition.function\n\n(pair\n key: (property_identifier) @name\n value: (function_expression)) @definition.function\n\n(pair\n key: (string (string_fragment) @name)\n value: (arrow_function)) @definition.function\n\n(pair\n key: (string (string_fragment) @name)\n value: (function_expression)) @definition.function\n\n; HOC-wrapped variable declarations: `const X = HOC((args) => { ... })`.\n; See TYPESCRIPT_QUERIES section above for the full rationale (issue #1166\n; follow-up \u2014 covers forwardRef / memo / useCallback / useMemo / observer\n; / debounce / user-defined HOC factories). Both `const` and `var` forms\n; are mirrored so JS code that uses `var` (or transpiler output) gets the\n; same attribution as the registry-primary path.\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function))))) @definition.function\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression))))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function)))))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression)))))) @definition.function\n\n; `var X = HOC(...)` parity with registry-primary.\n(variable_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function))))) @definition.function\n\n(variable_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression))))) @definition.function\n\n; Variable/constant declarations (non-function values).\n; Overlap with @definition.function patterns is handled by parse-worker dedup.\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name)) @definition.const\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name))) @definition.const\n\n; var declarations (mutable, function-scoped)\n(variable_declaration\n (variable_declarator\n name: (identifier) @name)) @definition.variable\n\n(import_statement\n source: (string) @import.source) @import\n\n; Re-export statements: export { X } from './y'\n(export_statement\n source: (string) @import.source) @import\n\n(call_expression\n function: (identifier) @call.name) @call\n\n(call_expression\n function: (member_expression\n property: (property_identifier) @call.name)) @call\n\n; Constructor calls: new Foo()\n(new_expression\n constructor: (identifier) @call.name) @call\n\n; Class fields \u2014 field_definition captures JS class fields (class User { address = ... })\n(field_definition\n property: (property_identifier) @name) @definition.property\n\n; Heritage queries - class extends (JavaScript uses different AST than TypeScript)\n; In tree-sitter-javascript, class_heritage directly contains the parent identifier\n(class_declaration\n name: (identifier) @heritage.class\n (class_heritage\n (identifier) @heritage.extends)) @heritage\n\n; Write access: obj.field = value\n(assignment_expression\n left: (member_expression\n object: (_) @assignment.receiver\n property: (property_identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(augmented_assignment_expression\n left: (member_expression\n object: (_) @assignment.receiver\n property: (property_identifier) @assignment.property)\n right: (_)) @assignment\n\n; HTTP consumers: fetch('/path'), axios.get('/path'), $.get('/path'), etc.\n(call_expression\n function: (identifier) @_fetch_fn (#eq? @_fetch_fn \"fetch\")\n arguments: (arguments\n [(string (string_fragment) @route.url)\n (template_string) @route.template_url])) @route.fetch\n\n; axios.get/post, $.get/post/ajax\n(call_expression\n function: (member_expression\n property: (property_identifier) @http_client.method)\n arguments: (arguments\n (string (string_fragment) @http_client.url))) @http_client\n\n; Express/Hono route registration\n(call_expression\n function: (member_expression\n property: (property_identifier) @express_route.method)\n arguments: (arguments\n (string (string_fragment) @express_route.path))) @express_route\n";
1
+ export declare const TYPESCRIPT_QUERIES = "\n(class_declaration\n name: (type_identifier) @name) @definition.class\n\n(abstract_class_declaration\n name: (type_identifier) @name) @definition.class\n\n(interface_declaration\n name: (type_identifier) @name) @definition.interface\n\n(function_declaration\n name: (identifier) @name) @definition.function\n\n; TypeScript overload signatures (function_signature is a separate node type from function_declaration)\n(function_signature\n name: (identifier) @name) @definition.function\n\n(method_definition\n name: (property_identifier) @name) @definition.method\n\n; ES2022 #private methods (private_property_identifier not matched by property_identifier)\n(method_definition\n name: (private_property_identifier) @name) @definition.method\n\n; Abstract method signatures in abstract classes\n(abstract_method_signature\n name: (property_identifier) @name) @definition.method\n\n; Interface method signatures\n(method_signature\n name: (property_identifier) @name) @definition.method\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (arrow_function))) @definition.function\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (function_expression))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (arrow_function)))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (function_expression)))) @definition.function\n\n; Object-property arrows / function expressions: `{ addItem: () => ... }`.\n; The pair's key field carries the meaningful name. Without these patterns,\n; calls inside the arrow are attributed to the file (issue #1166), and the\n; arrow itself is invisible to context() / impact() despite carrying real\n; behaviour (Zustand actions, TanStack queryFn, React Context providers).\n; String-key variant covers `\"add-item\": () => ...`; computed keys\n; (`[K]: () => ...`) intentionally fall through anonymous.\n(pair\n key: (property_identifier) @name\n value: (arrow_function)) @definition.function\n\n(pair\n key: (property_identifier) @name\n value: (function_expression)) @definition.function\n\n(pair\n key: (string (string_fragment) @name)\n value: (arrow_function)) @definition.function\n\n(pair\n key: (string (string_fragment) @name)\n value: (function_expression)) @definition.function\n\n; HOC-wrapped variable declarations: `const X = HOC((args) => { ... })`.\n; Mirrors the registry-primary patterns in `languages/typescript/query.ts`\n; so the legacy Call-Resolution DAG and the registry-primary pipeline\n; produce the same set of `Function` nodes \u2014 required for the CI parity\n; gate. Covers React.forwardRef / memo / useCallback / useMemo / observer\n; / debounce / user-defined HOC factories. The `var X = HOC(...)` form is\n; mirrored too (registry-primary has it) so that codebases mixing `var` and\n; `const` see identical attribution on both pipelines. See\n; `tsExtractFunctionName` for the resolution logic and the `query.ts`\n; comment for the full anchor-discipline rationale and the chained-\n; array-method trade-off.\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function))))) @definition.function\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression))))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function)))))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression)))))) @definition.function\n\n; `var X = HOC(...)` parity with registry-primary. Legacy code (and any\n; transpiler output that downlevels `const` to `var`) hits this shape.\n(variable_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function))))) @definition.function\n\n(variable_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression))))) @definition.function\n\n; Variable/constant declarations (non-function values).\n; Overlap with @definition.function patterns is handled by parse-worker dedup.\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name)) @definition.const\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name))) @definition.const\n\n; var declarations (mutable, function-scoped)\n(variable_declaration\n (variable_declarator\n name: (identifier) @name)) @definition.variable\n\n(import_statement\n source: (string) @import.source) @import\n\n; Re-export statements: export { X } from './y'\n(export_statement\n source: (string) @import.source) @import\n\n(call_expression\n function: (identifier) @call.name) @call\n\n(call_expression\n function: (member_expression\n property: (property_identifier) @call.name)) @call\n\n; Generic awaited free call: await fn<T>(args)\n; tree-sitter-typescript parses \"await fn<T>(args)\" as a call_expression whose\n; \"function\" field is an await_expression (not a bare identifier), because the\n; grammar resolves the ambiguity between generics and comparisons by consuming\n; \"await fn\" as an expression before attaching <T> as type_arguments.\n(call_expression\n function: (await_expression\n (identifier) @call.name)\n (type_arguments)) @call\n\n; Generic awaited member call: await obj.fn<T>(args)\n(call_expression\n function: (await_expression\n (member_expression\n property: (property_identifier) @call.name))\n (type_arguments)) @call\n\n; Constructor calls: new Foo()\n(new_expression\n constructor: (identifier) @call.name) @call\n\n; Class properties \u2014 public_field_definition covers most TS class fields\n(public_field_definition\n name: (property_identifier) @name) @definition.property\n\n; Private class fields: #address: Address\n(public_field_definition\n name: (private_property_identifier) @name) @definition.property\n\n; Constructor parameter properties: constructor(public address: Address)\n(required_parameter\n (accessibility_modifier)\n pattern: (identifier) @name) @definition.property\n\n; Heritage queries - class extends\n(class_declaration\n name: (type_identifier) @heritage.class\n (class_heritage\n (extends_clause\n value: (identifier) @heritage.extends))) @heritage\n\n; Heritage queries - class implements interface\n(class_declaration\n name: (type_identifier) @heritage.class\n (class_heritage\n (implements_clause\n (type_identifier) @heritage.implements))) @heritage.impl\n\n; Write access: obj.field = value\n(assignment_expression\n left: (member_expression\n object: (_) @assignment.receiver\n property: (property_identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(augmented_assignment_expression\n left: (member_expression\n object: (_) @assignment.receiver\n property: (property_identifier) @assignment.property)\n right: (_)) @assignment\n\n; HTTP consumers: fetch('/path'), axios.get('/path'), $.get('/path'), etc.\n; fetch() \u2014 global function\n(call_expression\n function: (identifier) @_fetch_fn (#eq? @_fetch_fn \"fetch\")\n arguments: (arguments\n [(string (string_fragment) @route.url)\n (template_string) @route.template_url])) @route.fetch\n\n; Custom fetch wrappers: apiFetch('/path'), fetchJSON('/api/data'), httpGet('/users'), etc.\n(call_expression\n function: (identifier) @_wrapper_fn (#match? @_wrapper_fn \"^(api(Fetch|Get|Post|Put|Delete|Patch|Request)|fetch(API|JSON|Data|Endpoint|Resource|Url)|http(Fetch|Get|Post|Put|Delete|Patch|Request))$\")\n arguments: (arguments\n (string (string_fragment) @route.url))) @route.fetch\n\n; axios.get/post/put/delete/patch('/path'), $.get/post/ajax({url:'/path'})\n(call_expression\n function: (member_expression\n property: (property_identifier) @http_client.method)\n arguments: (arguments\n (string (string_fragment) @http_client.url))) @http_client\n\n; Decorators: @Controller, @Get, @Post, etc.\n(decorator\n (call_expression\n function: (identifier) @decorator.name\n arguments: (arguments (string (string_fragment) @decorator.arg)?))) @decorator\n\n; Express/Hono route registration: app.get('/path', handler), router.post('/path', fn)\n(call_expression\n function: (member_expression\n property: (property_identifier) @express_route.method)\n arguments: (arguments\n (string (string_fragment) @express_route.path))) @express_route\n";
2
+ export declare const JAVASCRIPT_QUERIES = "\n(class_declaration\n name: (identifier) @name) @definition.class\n\n(function_declaration\n name: (identifier) @name) @definition.function\n\n(method_definition\n name: (property_identifier) @name) @definition.method\n\n; ES2022 #private methods\n(method_definition\n name: (private_property_identifier) @name) @definition.method\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (arrow_function))) @definition.function\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (function_expression))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (arrow_function)))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (function_expression)))) @definition.function\n\n; Object-property arrows / function expressions: `{ addItem: () => ... }`.\n; See TYPESCRIPT_QUERIES for rationale (issue #1166).\n(pair\n key: (property_identifier) @name\n value: (arrow_function)) @definition.function\n\n(pair\n key: (property_identifier) @name\n value: (function_expression)) @definition.function\n\n(pair\n key: (string (string_fragment) @name)\n value: (arrow_function)) @definition.function\n\n(pair\n key: (string (string_fragment) @name)\n value: (function_expression)) @definition.function\n\n; HOC-wrapped variable declarations: `const X = HOC((args) => { ... })`.\n; See TYPESCRIPT_QUERIES section above for the full rationale (issue #1166\n; follow-up \u2014 covers forwardRef / memo / useCallback / useMemo / observer\n; / debounce / user-defined HOC factories). Both `const` and `var` forms\n; are mirrored so JS code that uses `var` (or transpiler output) gets the\n; same attribution as the registry-primary path.\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function))))) @definition.function\n\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression))))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function)))))) @definition.function\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression)))))) @definition.function\n\n; `var X = HOC(...)` parity with registry-primary.\n(variable_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (arrow_function))))) @definition.function\n\n(variable_declaration\n (variable_declarator\n name: (identifier) @name\n value: (call_expression\n arguments: (arguments\n (function_expression))))) @definition.function\n\n; Variable/constant declarations (non-function values).\n; Overlap with @definition.function patterns is handled by parse-worker dedup.\n(lexical_declaration\n (variable_declarator\n name: (identifier) @name)) @definition.const\n\n(export_statement\n declaration: (lexical_declaration\n (variable_declarator\n name: (identifier) @name))) @definition.const\n\n; var declarations (mutable, function-scoped)\n(variable_declaration\n (variable_declarator\n name: (identifier) @name)) @definition.variable\n\n(import_statement\n source: (string) @import.source) @import\n\n; Re-export statements: export { X } from './y'\n(export_statement\n source: (string) @import.source) @import\n\n(call_expression\n function: (identifier) @call.name) @call\n\n(call_expression\n function: (member_expression\n property: (property_identifier) @call.name)) @call\n\n; Constructor calls: new Foo()\n(new_expression\n constructor: (identifier) @call.name) @call\n\n; Class fields \u2014 field_definition captures JS class fields (class User { address = ... })\n(field_definition\n property: (property_identifier) @name) @definition.property\n\n; Heritage queries - class extends (JavaScript uses different AST than TypeScript)\n; In tree-sitter-javascript, class_heritage directly contains the parent identifier\n(class_declaration\n name: (identifier) @heritage.class\n (class_heritage\n (identifier) @heritage.extends)) @heritage\n\n; Write access: obj.field = value\n(assignment_expression\n left: (member_expression\n object: (_) @assignment.receiver\n property: (property_identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(augmented_assignment_expression\n left: (member_expression\n object: (_) @assignment.receiver\n property: (property_identifier) @assignment.property)\n right: (_)) @assignment\n\n; HTTP consumers: fetch('/path'), axios.get('/path'), $.get('/path'), etc.\n(call_expression\n function: (identifier) @_fetch_fn (#eq? @_fetch_fn \"fetch\")\n arguments: (arguments\n [(string (string_fragment) @route.url)\n (template_string) @route.template_url])) @route.fetch\n\n; Custom fetch wrappers: apiFetch('/path'), fetchJSON('/api/data'), httpGet('/users'), etc.\n(call_expression\n function: (identifier) @_wrapper_fn (#match? @_wrapper_fn \"^(api(Fetch|Get|Post|Put|Delete|Patch|Request)|fetch(API|JSON|Data|Endpoint|Resource|Url)|http(Fetch|Get|Post|Put|Delete|Patch|Request))$\")\n arguments: (arguments\n (string (string_fragment) @route.url))) @route.fetch\n\n; axios.get/post, $.get/post/ajax\n(call_expression\n function: (member_expression\n property: (property_identifier) @http_client.method)\n arguments: (arguments\n (string (string_fragment) @http_client.url))) @http_client\n\n; Express/Hono route registration\n(call_expression\n function: (member_expression\n property: (property_identifier) @express_route.method)\n arguments: (arguments\n (string (string_fragment) @express_route.path))) @express_route\n";
3
3
  export declare const PYTHON_QUERIES = "\n(class_definition\n name: (identifier) @name) @definition.class\n\n(function_definition\n name: (identifier) @name) @definition.function\n\n(import_statement\n name: (dotted_name) @import.source) @import\n\n; import numpy as np \u2192 aliased_import captures the module name so the\n; import path is resolved and named-binding extraction stores \"np\" \u2192 \"numpy\".\n(import_statement\n name: (aliased_import\n name: (dotted_name) @import.source)) @import\n\n(import_from_statement\n module_name: (dotted_name) @import.source) @import\n\n(import_from_statement\n module_name: (relative_import) @import.source) @import\n\n(call\n function: (identifier) @call.name) @call\n\n(call\n function: (attribute\n attribute: (identifier) @call.name)) @call\n\n; Class attribute type annotations \u2014 PEP 526: address: Address or address: Address = Address()\n; Both bare annotations (address: Address) and annotated assignments (name: str = \"test\")\n; are parsed as (assignment left: ... type: ...) in tree-sitter-python.\n(expression_statement\n (assignment\n left: (identifier) @name\n type: (type)) @definition.property)\n\n; Plain variable assignments without type annotation: x = 5, MAX_SIZE = 100\n; Overlap with @definition.property (typed) is handled by parse-worker dedup.\n(expression_statement\n (assignment\n left: (identifier) @name)) @definition.variable\n\n; Heritage queries - Python class inheritance\n(class_definition\n name: (identifier) @heritage.class\n superclasses: (argument_list\n (identifier) @heritage.extends)) @heritage\n\n; Write access: obj.field = value\n(assignment\n left: (attribute\n object: (_) @assignment.receiver\n attribute: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(augmented_assignment\n left: (attribute\n object: (_) @assignment.receiver\n attribute: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; Python HTTP clients: requests.get('/path'), httpx.post('/path'), session.get('/path')\n(call\n function: (attribute\n attribute: (identifier) @http_client.method)\n arguments: (argument_list\n (string (string_content) @http_client.url))) @http_client\n\n; Python decorators: @app.route, @router.get, etc.\n(decorator\n (call\n function: (attribute\n object: (identifier) @decorator.receiver\n attribute: (identifier) @decorator.name)\n arguments: (argument_list\n (string (string_content) @decorator.arg)?))) @decorator\n";
4
4
  export declare const JAVA_QUERIES = "\n; Classes, Interfaces, Enums, Annotations\n(class_declaration name: (identifier) @name) @definition.class\n(interface_declaration name: (identifier) @name) @definition.interface\n(enum_declaration name: (identifier) @name) @definition.enum\n(annotation_type_declaration name: (identifier) @name) @definition.annotation\n\n; Methods & Constructors\n(method_declaration name: (identifier) @name) @definition.method\n(constructor_declaration name: (identifier) @name) @definition.constructor\n\n; Fields \u2014 typed field declarations inside class bodies\n(field_declaration\n declarator: (variable_declarator\n name: (identifier) @name)) @definition.property\n\n; Imports - capture any import declaration child as source\n(import_declaration (_) @import.source) @import\n\n; Calls\n(method_invocation name: (identifier) @call.name) @call\n(method_invocation object: (_) name: (identifier) @call.name) @call\n(method_reference) @call\n\n; Constructor calls: new Foo()\n(object_creation_expression type: (type_identifier) @call.name) @call\n\n; Local variable declarations inside method bodies\n(local_variable_declaration\n declarator: (variable_declarator\n name: (identifier) @name)) @definition.variable\n\n; Heritage - extends class\n(class_declaration name: (identifier) @heritage.class\n (superclass (type_identifier) @heritage.extends)) @heritage\n\n; Heritage - implements interfaces\n(class_declaration name: (identifier) @heritage.class\n (super_interfaces (type_list (type_identifier) @heritage.implements))) @heritage.impl\n\n; Write access: obj.field = value\n(assignment_expression\n left: (field_access\n object: (_) @assignment.receiver\n field: (identifier) @assignment.property)\n right: (_)) @assignment\n";
5
5
  export declare const C_QUERIES = "\n; Functions (direct declarator)\n(function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function\n(declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function\n\n; Functions returning pointers (pointer_declarator wraps function_declarator)\n(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function\n(declaration declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function\n\n; Functions returning double pointers (nested pointer_declarator)\n(function_definition declarator: (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name)))) @definition.function\n\n; Structs, Unions, Enums, Typedefs\n(struct_specifier name: (type_identifier) @name) @definition.struct\n(union_specifier name: (type_identifier) @name) @definition.union\n(enum_specifier name: (type_identifier) @name) @definition.enum\n(type_definition declarator: (type_identifier) @name) @definition.typedef\n\n; Macros\n(preproc_function_def name: (identifier) @name) @definition.macro\n(preproc_def name: (identifier) @name) @definition.macro\n\n; Includes\n(preproc_include path: (_) @import.source) @import\n\n; Calls\n(call_expression function: (identifier) @call.name) @call\n(call_expression function: (field_expression field: (field_identifier) @call.name)) @call\n\n; Variable declarations: int x = 5; or int x;\n(declaration\n declarator: (init_declarator\n declarator: (identifier) @name)) @definition.variable\n";
@@ -240,6 +240,12 @@ export const TYPESCRIPT_QUERIES = `
240
240
  [(string (string_fragment) @route.url)
241
241
  (template_string) @route.template_url])) @route.fetch
242
242
 
243
+ ; Custom fetch wrappers: apiFetch('/path'), fetchJSON('/api/data'), httpGet('/users'), etc.
244
+ (call_expression
245
+ function: (identifier) @_wrapper_fn (#match? @_wrapper_fn "^(api(Fetch|Get|Post|Put|Delete|Patch|Request)|fetch(API|JSON|Data|Endpoint|Resource|Url)|http(Fetch|Get|Post|Put|Delete|Patch|Request))$")
246
+ arguments: (arguments
247
+ (string (string_fragment) @route.url))) @route.fetch
248
+
243
249
  ; axios.get/post/put/delete/patch('/path'), $.get/post/ajax({url:'/path'})
244
250
  (call_expression
245
251
  function: (member_expression
@@ -432,6 +438,12 @@ export const JAVASCRIPT_QUERIES = `
432
438
  [(string (string_fragment) @route.url)
433
439
  (template_string) @route.template_url])) @route.fetch
434
440
 
441
+ ; Custom fetch wrappers: apiFetch('/path'), fetchJSON('/api/data'), httpGet('/users'), etc.
442
+ (call_expression
443
+ function: (identifier) @_wrapper_fn (#match? @_wrapper_fn "^(api(Fetch|Get|Post|Put|Delete|Patch|Request)|fetch(API|JSON|Data|Endpoint|Resource|Url)|http(Fetch|Get|Post|Put|Delete|Patch|Request))$")
444
+ arguments: (arguments
445
+ (string (string_fragment) @route.url))) @route.fetch
446
+
435
447
  ; axios.get/post, $.get/post/ajax
436
448
  (call_expression
437
449
  function: (member_expression
@@ -101,6 +101,10 @@ export interface ExtractedFetchCall {
101
101
  fetchURL: string;
102
102
  lineNumber: number;
103
103
  }
104
+ export interface FetchWrapperDef {
105
+ filePath: string;
106
+ functionName: string;
107
+ }
104
108
  export interface ExtractedDecoratorRoute {
105
109
  filePath: string;
106
110
  routePath: string;
@@ -166,6 +170,7 @@ export interface ParseWorkerResult {
166
170
  heritage: ExtractedHeritage[];
167
171
  routes: ExtractedRoute[];
168
172
  fetchCalls: ExtractedFetchCall[];
173
+ fetchWrapperDefs: FetchWrapperDef[];
169
174
  decoratorRoutes: ExtractedDecoratorRoute[];
170
175
  toolDefs: ExtractedToolDef[];
171
176
  ormQueries: ExtractedORMQuery[];
@@ -432,6 +432,7 @@ const processBatch = (files, onProgress) => {
432
432
  heritage: [],
433
433
  routes: [],
434
434
  fetchCalls: [],
435
+ fetchWrapperDefs: [],
435
436
  decoratorRoutes: [],
436
437
  toolDefs: [],
437
438
  ormQueries: [],
@@ -539,6 +540,25 @@ const EXPRESS_ROUTE_METHODS = new Set([
539
540
  'use',
540
541
  'route',
541
542
  ]);
543
+ /**
544
+ * Walk a tree-sitter AST subtree looking for a call to the global `fetch()` function.
545
+ * Returns `true` if found within `maxDepth` levels of nesting — keeps the check
546
+ * lightweight so it doesn't slow down parse-worker on large function bodies.
547
+ */
548
+ const checkForFetchCall = (node, depth = 0, maxDepth = 5) => {
549
+ if (depth > maxDepth)
550
+ return false;
551
+ if (node.type === 'call_expression') {
552
+ const fn = node.childForFieldName('function');
553
+ if (fn?.type === 'identifier' && fn.text === 'fetch')
554
+ return true;
555
+ }
556
+ for (let i = 0; i < node.childCount; i++) {
557
+ if (checkForFetchCall(node.child(i), depth + 1, maxDepth))
558
+ return true;
559
+ }
560
+ return false;
561
+ };
542
562
  // HTTP client methods that are ONLY used by clients, not Express route registration.
543
563
  // Methods like get/post/put/delete/patch overlap with Express — those are captured by
544
564
  // the express_route handler as route definitions, not consumers. The fetch() global
@@ -1505,6 +1525,18 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1505
1525
  : '',
1506
1526
  });
1507
1527
  }
1528
+ // ── Fetch wrapper detection: record functions that call fetch() internally ──
1529
+ if (nodeLabel === 'Function' &&
1530
+ definitionNode &&
1531
+ nameNode &&
1532
+ (language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript)) {
1533
+ if (checkForFetchCall(definitionNode)) {
1534
+ result.fetchWrapperDefs.push({
1535
+ filePath: file.path,
1536
+ functionName: nameNode.text,
1537
+ });
1538
+ }
1539
+ }
1508
1540
  }
1509
1541
  // Extract framework routes via provider detection (e.g., Laravel routes.php)
1510
1542
  if (provider.isRouteFile?.(file.path)) {
@@ -1542,6 +1574,7 @@ let accumulated = {
1542
1574
  heritage: [],
1543
1575
  routes: [],
1544
1576
  fetchCalls: [],
1577
+ fetchWrapperDefs: [],
1545
1578
  decoratorRoutes: [],
1546
1579
  toolDefs: [],
1547
1580
  ormQueries: [],
@@ -1569,6 +1602,7 @@ const mergeResult = (target, src) => {
1569
1602
  appendAll(target.heritage, src.heritage);
1570
1603
  appendAll(target.routes, src.routes);
1571
1604
  appendAll(target.fetchCalls, src.fetchCalls);
1605
+ appendAll(target.fetchWrapperDefs, src.fetchWrapperDefs);
1572
1606
  appendAll(target.decoratorRoutes, src.decoratorRoutes);
1573
1607
  appendAll(target.toolDefs, src.toolDefs);
1574
1608
  appendAll(target.ormQueries, src.ormQueries);
@@ -1651,6 +1685,7 @@ parentPort.on('message', (msg) => {
1651
1685
  heritage: [],
1652
1686
  routes: [],
1653
1687
  fetchCalls: [],
1688
+ fetchWrapperDefs: [],
1654
1689
  decoratorRoutes: [],
1655
1690
  toolDefs: [],
1656
1691
  ormQueries: [],
@@ -41,7 +41,7 @@ import { fileURLToPath } from 'url';
41
41
  * On version mismatch, `loadParseCache` returns an empty cache and the
42
42
  * next save overwrites the on-disk file with the new version baked in.
43
43
  */
44
- const SCHEMA_BUMP = 1;
44
+ const SCHEMA_BUMP = 2;
45
45
  const GITNEXUS_PKG_VERSION = (() => {
46
46
  try {
47
47
  // package.json sits at gitnexus/package.json — two levels up from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.75",
3
+ "version": "1.6.6-rc.77",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",