gitnexus 1.6.10-rc.217 → 1.6.10-rc.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/group/extractors/http-patterns/node.js +283 -39
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +2 -2
- package/dist/core/ingestion/route-extractors/constant-resolver.js +10 -6
- package/dist/core/ingestion/route-extractors/js-const-resolver.d.ts +192 -0
- package/dist/core/ingestion/route-extractors/js-const-resolver.js +1033 -0
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import Parser from 'tree-sitter';
|
|
1
2
|
import JavaScript from 'tree-sitter-javascript';
|
|
2
3
|
import TypeScript from 'tree-sitter-typescript';
|
|
3
4
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
5
|
+
import { MAX_FOLD_LENGTH } from '../../../ingestion/route-extractors/constant-resolver.js';
|
|
4
6
|
import { DATA_ROUTE_TABLE_SOURCE, scanDataRouteTables, } from '../../../ingestion/route-extractors/data-route-table.js';
|
|
7
|
+
import { buildJsRepoFacts, extractJsModuleFacts, isAxiosNamespace, isHttpClientRef, resolveJsPathExpression, } from '../../../ingestion/route-extractors/js-const-resolver.js';
|
|
5
8
|
/**
|
|
6
9
|
* Node.js / TypeScript HTTP plugin family. Handles:
|
|
7
10
|
* - NestJS `@Controller('prefix')` classes with `@Get(':id')` methods
|
|
@@ -79,15 +82,28 @@ const FETCH_WITH_OPTIONS_SPEC = {
|
|
|
79
82
|
value: (string) @http_method))))
|
|
80
83
|
`,
|
|
81
84
|
};
|
|
82
|
-
// ─── Consumer:
|
|
83
|
-
|
|
85
|
+
// ─── Consumer: <httpClient>.get/post/... ─────────────────────────────
|
|
86
|
+
// Widened from a literal `axios` receiver with a literal path. Application
|
|
87
|
+
// code satisfies neither: it calls through a configured instance
|
|
88
|
+
// (`const api = axios.create({ baseURL })`, imported at the call site under
|
|
89
|
+
// whatever name the app chose) and passes the path by reference from a shared
|
|
90
|
+
// route table (`api.get(API_ROUTE_PATH.LINKS)`). The query therefore matches
|
|
91
|
+
// ANY identifier receiver with an HTTP-verb method and ANY first argument;
|
|
92
|
+
// `scanBundle` admits a match only after PROVING the receiver is an axios
|
|
93
|
+
// instance and resolving the argument to a path.
|
|
94
|
+
//
|
|
95
|
+
// The proof gate is load-bearing, not belt-and-braces: EXPRESS_SPEC above
|
|
96
|
+
// matches `router.get('/x', handler)` / `app.post(...)` as PROVIDERS. A
|
|
97
|
+
// receiver admitted on spelling alone would re-emit every Express route in the
|
|
98
|
+
// repo as a consumer of itself, on both sides of every cross-repo pair.
|
|
99
|
+
const HTTP_CLIENT_SPEC = {
|
|
84
100
|
meta: {},
|
|
85
101
|
query: `
|
|
86
102
|
(call_expression
|
|
87
103
|
function: (member_expression
|
|
88
|
-
object: (identifier) @obj
|
|
104
|
+
object: (identifier) @obj
|
|
89
105
|
property: (property_identifier) @http_method (#match? @http_method "^(get|post|put|delete|patch)$"))
|
|
90
|
-
arguments: (arguments .
|
|
106
|
+
arguments: (arguments . (_) @path))
|
|
91
107
|
`,
|
|
92
108
|
};
|
|
93
109
|
// ─── Consumer: jQuery shorthand $.get(url) / $.post(url, ...) ────────
|
|
@@ -141,7 +157,7 @@ function compileBundle(language, name) {
|
|
|
141
157
|
express: mk(EXPRESS_SPEC, 'express'),
|
|
142
158
|
fetchNoOptions: mk(FETCH_NO_OPTIONS_SPEC, 'fetch-no-options'),
|
|
143
159
|
fetchWithOptions: mk(FETCH_WITH_OPTIONS_SPEC, 'fetch-with-options'),
|
|
144
|
-
|
|
160
|
+
httpClient: mk(HTTP_CLIENT_SPEC, 'http-client'),
|
|
145
161
|
jqueryShorthand: mk(JQUERY_SHORTHAND_SPEC, 'jquery-shorthand'),
|
|
146
162
|
jqueryAjax: mk(JQUERY_AJAX_SPEC, 'jquery-ajax'),
|
|
147
163
|
axiosObject: mk(AXIOS_OBJECT_SPEC, 'axios-object'),
|
|
@@ -281,12 +297,22 @@ function findDecoratedMethod(decoratorNode) {
|
|
|
281
297
|
*/
|
|
282
298
|
function buildImportMap(tree) {
|
|
283
299
|
const map = new Map();
|
|
284
|
-
|
|
300
|
+
// Both walks are explicit-stack, not recursive. They visit EVERY node of the
|
|
301
|
+
// file, so their depth is the source's nesting depth — and `scan` may not
|
|
302
|
+
// throw: a `RangeError` here escapes to `sync.ts`, which records the repo as
|
|
303
|
+
// an unexplained "missing repo" and drops every contract of every kind for
|
|
304
|
+
// it, silently. A file nesting template substitutions ~4 000 deep (well
|
|
305
|
+
// inside what tree-sitter will parse) was enough.
|
|
306
|
+
const stack = [tree.rootNode];
|
|
307
|
+
while (stack.length > 0) {
|
|
308
|
+
const node = stack.pop();
|
|
285
309
|
if (node.type === 'import_statement') {
|
|
286
310
|
const sourceNode = node.childForFieldName('source');
|
|
287
311
|
const module = sourceNode ? unquoteLiteral(sourceNode.text) : null;
|
|
288
312
|
if (module !== null) {
|
|
289
|
-
const
|
|
313
|
+
const inner = [node];
|
|
314
|
+
while (inner.length > 0) {
|
|
315
|
+
const n = inner.pop();
|
|
290
316
|
if (n.type === 'import_specifier') {
|
|
291
317
|
const nameNode = n.childForFieldName('name');
|
|
292
318
|
const aliasNode = n.childForFieldName('alias');
|
|
@@ -295,26 +321,213 @@ function buildImportMap(tree) {
|
|
|
295
321
|
map.set(local.text, { name: nameNode.text, module });
|
|
296
322
|
}
|
|
297
323
|
}
|
|
298
|
-
for (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
collect(c);
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
collect(node);
|
|
324
|
+
for (const c of n.namedChildren)
|
|
325
|
+
inner.push(c);
|
|
326
|
+
}
|
|
305
327
|
}
|
|
328
|
+
// An import statement cannot contain another one, and the inner loop has
|
|
329
|
+
// already visited its whole subtree.
|
|
330
|
+
continue;
|
|
306
331
|
}
|
|
307
|
-
for (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
332
|
+
for (const c of node.namedChildren)
|
|
333
|
+
stack.push(c);
|
|
334
|
+
}
|
|
335
|
+
return map;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Shared across the three JS/TS plugins.
|
|
339
|
+
*
|
|
340
|
+
* JS, TS and TSX are distinct `HttpLanguagePlugin` objects, and the
|
|
341
|
+
* orchestrator caches `prepareRepo` output per plugin NAME — so a polyglot
|
|
342
|
+
* frontend would build this identical map three times. All three are called
|
|
343
|
+
* with the same memoized file-list array within one extraction run, so keying
|
|
344
|
+
* on that array's identity collapses the work to a single pass. A later run
|
|
345
|
+
* passes a different array and correctly rebuilds; the weak key lets the old
|
|
346
|
+
* map be collected with it.
|
|
347
|
+
*/
|
|
348
|
+
const REPO_CONTEXT_BY_FILE_LIST = new WeakMap();
|
|
349
|
+
/**
|
|
350
|
+
* Skip ceiling for the pre-pass, mirroring the analyzer's default
|
|
351
|
+
* `--max-file-size`. A minified bundle is megabytes on one line and defines no
|
|
352
|
+
* route table a human wrote; parsing it costs far more than it can return.
|
|
353
|
+
*/
|
|
354
|
+
const MAX_PREPASS_FILE_BYTES = 512 * 1024;
|
|
355
|
+
/** Repo-relative path in the same POSIX form the fact map is keyed by. */
|
|
356
|
+
function normalizeRel(rel) {
|
|
357
|
+
return rel.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
358
|
+
}
|
|
359
|
+
/** The grammar a JS/TS-family file should be parsed with, or null if not one. */
|
|
360
|
+
function grammarForFile(rel) {
|
|
361
|
+
const lower = rel.toLowerCase();
|
|
362
|
+
if (lower.endsWith('.tsx'))
|
|
363
|
+
return TypeScript.tsx;
|
|
364
|
+
if (/\.[cm]?ts$/.test(lower))
|
|
365
|
+
return TypeScript.typescript;
|
|
366
|
+
if (/\.[cm]?jsx?$/.test(lower))
|
|
367
|
+
return JavaScript;
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
function buildNodeRepoContext(args) {
|
|
371
|
+
const cached = REPO_CONTEXT_BY_FILE_LIST.get(args.files);
|
|
372
|
+
if (cached)
|
|
373
|
+
return cached;
|
|
374
|
+
const byFile = new Map();
|
|
375
|
+
const parsers = new Map();
|
|
376
|
+
const parserFor = (language) => {
|
|
377
|
+
let parser = parsers.get(language);
|
|
378
|
+
if (!parser) {
|
|
379
|
+
parser = new Parser();
|
|
380
|
+
parser.setLanguage(language);
|
|
381
|
+
parsers.set(language, parser);
|
|
311
382
|
}
|
|
383
|
+
return parser;
|
|
312
384
|
};
|
|
313
|
-
|
|
314
|
-
|
|
385
|
+
// Cost gate, in the spirit of the sibling `python.ts` pre-pass: every fact
|
|
386
|
+
// this map holds exists to prove a receiver is an axios instance or to fold a
|
|
387
|
+
// path for one. A repo where the string `axios` appears nowhere can prove no
|
|
388
|
+
// receiver, so every parse below is dead work — and parsing is the expensive
|
|
389
|
+
// half (measured 4.36 s / +258 MB RSS over 827 TypeScript files, on top of
|
|
390
|
+
// the parse `getScanInput` already does).
|
|
391
|
+
// Only the file's identity is carried between the passes, never its text: a
|
|
392
|
+
// large monorepo's whole source tree held in one array at once is the shape
|
|
393
|
+
// that produced the analyzer's scale problems, and the second read is cheap
|
|
394
|
+
// beside the parse it gates.
|
|
395
|
+
const eligible = [];
|
|
396
|
+
let sawAxios = false;
|
|
397
|
+
for (const rel of args.files) {
|
|
398
|
+
const language = grammarForFile(rel);
|
|
399
|
+
if (language === null)
|
|
400
|
+
continue;
|
|
401
|
+
const content = args.readFile(rel);
|
|
402
|
+
// `MAX_PREPASS_FILE_BYTES` is a BYTE ceiling; `String.length` counts UTF-16
|
|
403
|
+
// code units, which under-counts every multi-byte source.
|
|
404
|
+
if (content === null || Buffer.byteLength(content, 'utf8') > MAX_PREPASS_FILE_BYTES)
|
|
405
|
+
continue;
|
|
406
|
+
if (!sawAxios && content.includes('axios'))
|
|
407
|
+
sawAxios = true;
|
|
408
|
+
eligible.push({ rel, language });
|
|
409
|
+
}
|
|
410
|
+
if (sawAxios) {
|
|
411
|
+
for (const { rel, language } of eligible) {
|
|
412
|
+
try {
|
|
413
|
+
const content = args.readFile(rel);
|
|
414
|
+
if (content === null)
|
|
415
|
+
continue;
|
|
416
|
+
// `parseSource` belongs INSIDE the guard: `safe-parse.ts` throws
|
|
417
|
+
// `ParseTimeoutError` and makes catching it a per-caller obligation, and
|
|
418
|
+
// `prepareRepo` is contractually non-throwing. One escape here left the
|
|
419
|
+
// fact map unwritten for the WHOLE repo — and, because the orchestrator
|
|
420
|
+
// caches per plugin NAME, made all three JS/TS plugins re-walk it and
|
|
421
|
+
// fail the same way before falling back to literal-only scanning.
|
|
422
|
+
const tree = args.parseSource(parserFor(language), content);
|
|
423
|
+
if (!tree)
|
|
424
|
+
continue;
|
|
425
|
+
byFile.set(normalizeRel(rel), extractJsModuleFacts(tree));
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// One malformed file must never abort the pre-pass — it simply stays
|
|
429
|
+
// unresolved, exactly as it is without this pass at all.
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const ctx = { facts: buildJsRepoFacts(byFile) };
|
|
434
|
+
REPO_CONTEXT_BY_FILE_LIST.set(args.files, ctx);
|
|
435
|
+
return ctx;
|
|
436
|
+
}
|
|
437
|
+
/** The repo facts to resolve against, or null when there was no pre-pass. */
|
|
438
|
+
function resolveFactsFor(repoContext, fileRel) {
|
|
439
|
+
const ctx = repoContext;
|
|
440
|
+
if (!ctx || fileRel === undefined)
|
|
441
|
+
return null;
|
|
442
|
+
return ctx.facts;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Whether a folded first argument is plausibly a URL path.
|
|
446
|
+
*
|
|
447
|
+
* The query now captures ANY first argument, and "it folded to a string" is not
|
|
448
|
+
* "it is a path" — `normalizeConsumerPath` is a canonicalizer, not a validator,
|
|
449
|
+
* and it happily turns non-paths into contracts that exact-match real provider
|
|
450
|
+
* routes:
|
|
451
|
+
*
|
|
452
|
+
* api.get(CONFIG.TIMEOUT) // "5000" -> http::GET::/{param}
|
|
453
|
+
* api.post(MSG.ERROR) // "Could not reach the …" -> http::POST::/could not reach the server
|
|
454
|
+
*
|
|
455
|
+
* `/{param}` matches every one-segment provider route in the group, and
|
|
456
|
+
* `matching.exclude_links_param_only_paths` defaults to `false`. A path whose
|
|
457
|
+
* leading term is an unresolved placeholder is refused for the same reason —
|
|
458
|
+
* nothing pins where it starts. (`resolveJsPathExpression` already refuses those
|
|
459
|
+
* it folded itself; this also covers the literal fallback below.)
|
|
460
|
+
*/
|
|
461
|
+
function looksLikeHttpPath(path) {
|
|
462
|
+
if (path === '')
|
|
463
|
+
return false;
|
|
464
|
+
if (/^https?:\/\//i.test(path))
|
|
465
|
+
return true;
|
|
466
|
+
// A `${…}` term is a runtime value that `normalizeConsumerPath` rewrites to
|
|
467
|
+
// `{param}`; its SOURCE text can be any expression (`${draft ? 'a' : 'b'}`,
|
|
468
|
+
// `${id ?? ''}`), so the checks below have to run against the normalized
|
|
469
|
+
// shape. Testing the raw source dropped every partially folded path whose
|
|
470
|
+
// unresolved term happened to contain a space.
|
|
471
|
+
const shape = path.replace(/\$\{[^}]+\}/g, '{param}');
|
|
472
|
+
if (/\s/.test(shape))
|
|
473
|
+
return false;
|
|
474
|
+
if (shape.startsWith('{param}'))
|
|
475
|
+
return false;
|
|
476
|
+
// An all-digit string is a path only when it is written as one. A leading
|
|
477
|
+
// slash is that evidence: `client.get('/123')` is a route whose segment the
|
|
478
|
+
// consumer normalizer reads as `{param}`, while a bare `"5000"` folded out of
|
|
479
|
+
// `CONFIG.TIMEOUT` is a timeout that would match every one-segment provider.
|
|
480
|
+
if (!shape.startsWith('/'))
|
|
481
|
+
return !/^\d+$/.test(shape);
|
|
482
|
+
return true;
|
|
315
483
|
}
|
|
316
|
-
|
|
484
|
+
/**
|
|
485
|
+
* The path a consumer call's first argument denotes.
|
|
486
|
+
*
|
|
487
|
+
* Prefers full resolution against the repo facts; falls back to the raw
|
|
488
|
+
* literal for a string/template node so a repo with no pre-pass (or an
|
|
489
|
+
* unresolvable reference) behaves exactly as it did before.
|
|
490
|
+
*
|
|
491
|
+
* `fileKey` is already `normalizeRel`-ed by the caller — see `scanBundle`.
|
|
492
|
+
*
|
|
493
|
+
* `legacyShape` marks the exact combination this pattern matched BEFORE it was
|
|
494
|
+
* widened: the literal receiver `axios` with a string or template-string first
|
|
495
|
+
* argument. That combination keeps its old output verbatim, so this PR adds
|
|
496
|
+
* detections without removing any — `axios.get(`${API_BASE}/users`)` still
|
|
497
|
+
* yields `/{param}/users`. Everything the widened query NEWLY admits (any other
|
|
498
|
+
* receiver, or any non-literal argument) has to clear the gates.
|
|
499
|
+
*/
|
|
500
|
+
function resolveConsumerPath(pathNode, facts, fileKey, legacyShape) {
|
|
501
|
+
if (facts && fileKey !== undefined) {
|
|
502
|
+
const resolved = resolveJsPathExpression(fileKey, pathNode, facts);
|
|
503
|
+
if (resolved !== null && looksLikeHttpPath(resolved))
|
|
504
|
+
return resolved;
|
|
505
|
+
}
|
|
506
|
+
// The fallback is deliberately gated on node TYPE: `unquoteLiteral` returns
|
|
507
|
+
// unrecognized input unchanged, so handing it a `member_expression` would
|
|
508
|
+
// yield the literal text `API_ROUTE_PATH.LINKS` as if it were a URL path.
|
|
509
|
+
if (pathNode.type !== 'string' && pathNode.type !== 'template_string')
|
|
510
|
+
return null;
|
|
511
|
+
const literal = unquoteLiteral(pathNode.text);
|
|
512
|
+
// The fold bails past `MAX_FOLD_LENGTH`; the raw source it falls back to has
|
|
513
|
+
// no such bound and lands in `contractId` and `meta.path` all the same.
|
|
514
|
+
if (literal === null || literal.length > MAX_FOLD_LENGTH)
|
|
515
|
+
return null;
|
|
516
|
+
return legacyShape || looksLikeHttpPath(literal) ? literal : null;
|
|
517
|
+
}
|
|
518
|
+
function scanBundle(bundle, tree, repoContext, fileRel) {
|
|
317
519
|
const out = [];
|
|
520
|
+
// Repo-wide constant / HTTP-client facts, when the orchestrator ran the
|
|
521
|
+
// `prepareRepo` pre-pass. Absent for a bare `scan(tree)` call, in which case
|
|
522
|
+
// every cross-file resolution below floors to the literal-only behavior.
|
|
523
|
+
const facts = resolveFactsFor(repoContext, fileRel);
|
|
524
|
+
// The fact map is keyed by `normalizeRel(rel)`. Normalizing at ONE place and
|
|
525
|
+
// using that value for every read keeps the two sides in step: the receiver
|
|
526
|
+
// gate used to read the raw `fileRel`, and `isHttpClientRef` cannot tell a key
|
|
527
|
+
// miss from "not a client", so any non-POSIX path (glob v13 has no
|
|
528
|
+
// `posix: true` and its walker joins with the platform separator; graph rows
|
|
529
|
+
// are a second unnormalized source) silently returned zero consumers.
|
|
530
|
+
const fileKey = fileRel === undefined ? undefined : normalizeRel(fileRel);
|
|
318
531
|
// Local-binding → { declared export name, module } for the file's named
|
|
319
532
|
// imports, so an express handler that is an imported (possibly aliased)
|
|
320
533
|
// symbol resolves to the real definition rather than its local alias text.
|
|
@@ -450,24 +663,52 @@ function scanBundle(bundle, tree) {
|
|
|
450
663
|
confidence: 0.7,
|
|
451
664
|
});
|
|
452
665
|
}
|
|
453
|
-
// Consumer:
|
|
454
|
-
|
|
666
|
+
// Consumer: <httpClient>.<verb>(url) — `axios` itself, or any receiver the
|
|
667
|
+
// repo pre-pass proves is an axios instance.
|
|
668
|
+
for (const match of runCompiledPatterns(bundle.httpClient, tree)) {
|
|
455
669
|
const methodNode = match.captures.http_method;
|
|
456
670
|
const pathNode = match.captures.path;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const path = unquoteLiteral(pathNode.text);
|
|
460
|
-
if (path === null)
|
|
671
|
+
const objNode = match.captures.obj;
|
|
672
|
+
if (!methodNode || !pathNode || !objNode)
|
|
461
673
|
continue;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
674
|
+
// Receiver gate. `axios.get(...)` needs no proof; anything else must be
|
|
675
|
+
// traced to an `axios.create(...)` binding, or it is not ours to claim.
|
|
676
|
+
const receiver = objNode.text;
|
|
677
|
+
// Cross-file resolution is the only work in this file that walks a
|
|
678
|
+
// repo-wide graph, and `HttpLanguagePlugin.scan` may not throw: a single
|
|
679
|
+
// hostile call site must cost its own detection, not the repo's whole
|
|
680
|
+
// contract set (`sync.ts` catches a throw here as an unexplained "missing
|
|
681
|
+
// repo", silently, for every contract type).
|
|
682
|
+
try {
|
|
683
|
+
// The receiver is admitted when it IS the axios module — the bare
|
|
684
|
+
// spelling this pattern trusted before it was widened, or a declared
|
|
685
|
+
// import/require of 'axios' under any name — or when it traces to an
|
|
686
|
+
// `axios.create(...)` instance. Nothing else.
|
|
687
|
+
const isModule = facts === null || fileKey === undefined
|
|
688
|
+
? receiver === 'axios'
|
|
689
|
+
: isAxiosNamespace(fileKey, receiver, facts);
|
|
690
|
+
if (!isModule) {
|
|
691
|
+
if (!facts || fileKey === undefined)
|
|
692
|
+
continue;
|
|
693
|
+
if (!isHttpClientRef(fileKey, receiver, facts))
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const path = resolveConsumerPath(pathNode, facts, fileKey, isModule && (pathNode.type === 'string' || pathNode.type === 'template_string'));
|
|
697
|
+
if (path === null)
|
|
698
|
+
continue;
|
|
699
|
+
out.push({
|
|
700
|
+
role: 'consumer',
|
|
701
|
+
framework: 'axios',
|
|
702
|
+
method: methodNode.text.toUpperCase(),
|
|
703
|
+
path,
|
|
704
|
+
name: null,
|
|
705
|
+
line: pathNode.startPosition.row + 1,
|
|
706
|
+
confidence: 0.7,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
// Unresolvable is the same outcome as unresolved — skip this call site.
|
|
711
|
+
}
|
|
471
712
|
}
|
|
472
713
|
// Consumer: jQuery shorthand $.get(url) / $.post(url, ...)
|
|
473
714
|
for (const match of runCompiledPatterns(bundle.jqueryShorthand, tree)) {
|
|
@@ -554,15 +795,18 @@ function scanBundle(bundle, tree) {
|
|
|
554
795
|
export const JAVASCRIPT_HTTP_PLUGIN = {
|
|
555
796
|
name: 'javascript-http',
|
|
556
797
|
language: JavaScript,
|
|
557
|
-
|
|
798
|
+
prepareRepo: buildNodeRepoContext,
|
|
799
|
+
scan: (tree, repoContext, fileRel) => scanBundle(JAVASCRIPT_BUNDLE, tree, repoContext, fileRel),
|
|
558
800
|
};
|
|
559
801
|
export const TYPESCRIPT_HTTP_PLUGIN = {
|
|
560
802
|
name: 'typescript-http',
|
|
561
803
|
language: TypeScript.typescript,
|
|
562
|
-
|
|
804
|
+
prepareRepo: buildNodeRepoContext,
|
|
805
|
+
scan: (tree, repoContext, fileRel) => scanBundle(TYPESCRIPT_BUNDLE, tree, repoContext, fileRel),
|
|
563
806
|
};
|
|
564
807
|
export const TSX_HTTP_PLUGIN = {
|
|
565
808
|
name: 'tsx-http',
|
|
566
809
|
language: TypeScript.tsx,
|
|
567
|
-
|
|
810
|
+
prepareRepo: buildNodeRepoContext,
|
|
811
|
+
scan: (tree, repoContext, fileRel) => scanBundle(TSX_BUNDLE, tree, repoContext, fileRel),
|
|
568
812
|
};
|
|
@@ -80,10 +80,10 @@ export type ImportResolver = (importingFileKey: string, moduleSpec: string, repo
|
|
|
80
80
|
* value, folding `+` concatenation and following import chains via
|
|
81
81
|
* `resolveImport`, or `null` when it cannot be fully folded.
|
|
82
82
|
*/
|
|
83
|
-
export declare function resolveConstant(fileKey: string, name: string, repo: RepoConstants, resolveImport: ImportResolver): string | null;
|
|
83
|
+
export declare function resolveConstant(fileKey: string, name: string, repo: RepoConstants, resolveImport: ImportResolver, repoKeys?: ReadonlySet<string>): string | null;
|
|
84
84
|
/**
|
|
85
85
|
* Resolve an inline operand list (an unnamed `+`-expression captured directly at
|
|
86
86
|
* a decorator/annotation argument, e.g. `@router.get(API_V1 + "/widgets")`)
|
|
87
87
|
* against `fileKey`.
|
|
88
88
|
*/
|
|
89
|
-
export declare function resolveOperands(fileKey: string, operands: readonly Operand[], repo: RepoConstants, resolveImport: ImportResolver): string | null;
|
|
89
|
+
export declare function resolveOperands(fileKey: string, operands: readonly Operand[], repo: RepoConstants, resolveImport: ImportResolver, repoKeys?: ReadonlySet<string>): string | null;
|
|
@@ -104,10 +104,14 @@ function computeFold(fileKey, name, state, depth) {
|
|
|
104
104
|
}
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
|
-
function newState(repo, resolveImport) {
|
|
107
|
+
function newState(repo, resolveImport, repoKeys) {
|
|
108
108
|
return {
|
|
109
109
|
repo,
|
|
110
|
-
|
|
110
|
+
// Materializing the key set here is O(files), and this runs once per fold —
|
|
111
|
+
// which is once per import hop, not once per scan. A binding that already
|
|
112
|
+
// holds the set (every one of them does; it is a projection of the same map
|
|
113
|
+
// it builds `repo` from) passes it in and skips the copy entirely.
|
|
114
|
+
repoKeys: repoKeys ?? new Set(repo.keys()),
|
|
111
115
|
resolveImport,
|
|
112
116
|
visited: new Set(),
|
|
113
117
|
memo: new Map(),
|
|
@@ -118,14 +122,14 @@ function newState(repo, resolveImport) {
|
|
|
118
122
|
* value, folding `+` concatenation and following import chains via
|
|
119
123
|
* `resolveImport`, or `null` when it cannot be fully folded.
|
|
120
124
|
*/
|
|
121
|
-
export function resolveConstant(fileKey, name, repo, resolveImport) {
|
|
122
|
-
return foldName(fileKey, name, newState(repo, resolveImport), 0);
|
|
125
|
+
export function resolveConstant(fileKey, name, repo, resolveImport, repoKeys) {
|
|
126
|
+
return foldName(fileKey, name, newState(repo, resolveImport, repoKeys), 0);
|
|
123
127
|
}
|
|
124
128
|
/**
|
|
125
129
|
* Resolve an inline operand list (an unnamed `+`-expression captured directly at
|
|
126
130
|
* a decorator/annotation argument, e.g. `@router.get(API_V1 + "/widgets")`)
|
|
127
131
|
* against `fileKey`.
|
|
128
132
|
*/
|
|
129
|
-
export function resolveOperands(fileKey, operands, repo, resolveImport) {
|
|
130
|
-
return foldExpr(fileKey, operands, newState(repo, resolveImport), 0);
|
|
133
|
+
export function resolveOperands(fileKey, operands, repo, resolveImport, repoKeys) {
|
|
134
|
+
return foldExpr(fileKey, operands, newState(repo, resolveImport, repoKeys), 0);
|
|
131
135
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript/TypeScript binding for the language-agnostic constant resolver.
|
|
3
|
+
*
|
|
4
|
+
* Supplies the two JS-specific pieces the shared fold in `constant-resolver.ts`
|
|
5
|
+
* needs — {@link resolveJsImport} (import specifier → file key, honoring
|
|
6
|
+
* relative paths, extensionless imports, directory `index` files and bare
|
|
7
|
+
* alias-style specifiers) and {@link extractJsModuleFacts} (tree →
|
|
8
|
+
* {@link ModuleConstants} plus the export/HTTP-client facts below) — mirroring
|
|
9
|
+
* how `python-const-resolver.ts` binds the same core for Python (#2391).
|
|
10
|
+
*
|
|
11
|
+
* Two JS-shaped facts the Python binding has no analogue for:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Object-literal path tables.** Python route constants are module-level
|
|
14
|
+
* scalars (`API_V1 = "/v1"`); the JS convention is one frozen table —
|
|
15
|
+
* `export const API_ROUTE_PATH = { LINKS: "/links", … } as const` — read at
|
|
16
|
+
* the call site as `API_ROUTE_PATH.LINKS`. The extractor flattens such a
|
|
17
|
+
* table into DOTTED literal keys (`API_ROUTE_PATH.LINKS` → `/links`) so the
|
|
18
|
+
* agnostic fold, which does a plain `literals.get(name)`, resolves a member
|
|
19
|
+
* reference with no changes to the core.
|
|
20
|
+
*
|
|
21
|
+
* 2. **Export aliasing.** `export default routeApiClient` and
|
|
22
|
+
* `export { a as b }` mean the name an importer writes is often not the
|
|
23
|
+
* name the defining file bound. {@link JsModuleFacts.exports} maps the
|
|
24
|
+
* EXPORTED name (including `default`) to the local one so a cross-file
|
|
25
|
+
* chase lands on the right binding.
|
|
26
|
+
*
|
|
27
|
+
* Both stay in this binding — the shared core keeps knowing nothing about any
|
|
28
|
+
* language.
|
|
29
|
+
*
|
|
30
|
+
* Keying matches the Python binding: the repo map is keyed by unique POSIX file
|
|
31
|
+
* path, and an import that cannot be pinned to exactly one file resolves to
|
|
32
|
+
* `null` (skip) rather than an arbitrary winner. An unresolved path is a
|
|
33
|
+
* missing contract; a wrongly-resolved one is a false cross-repo link, which is
|
|
34
|
+
* strictly worse.
|
|
35
|
+
*/
|
|
36
|
+
import type Parser from 'tree-sitter';
|
|
37
|
+
import { type ImportResolver, type ModuleConstants, type Operand, type RepoConstants } from './constant-resolver.js';
|
|
38
|
+
export type { ImportBinding, ModuleConstants, Operand, RepoConstants, } from './constant-resolver.js';
|
|
39
|
+
/**
|
|
40
|
+
* Per-file facts beyond the agnostic {@link ModuleConstants}: which exported
|
|
41
|
+
* name maps to which local binding, and which local bindings hold an HTTP
|
|
42
|
+
* client instance.
|
|
43
|
+
*/
|
|
44
|
+
export interface JsModuleFacts {
|
|
45
|
+
/** String constants, dotted table members, `+`-expressions and imports. */
|
|
46
|
+
readonly constants: ModuleConstants;
|
|
47
|
+
/** Exported name (incl. `default`) → local binding name in this file. */
|
|
48
|
+
readonly exports: Map<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Module specifiers this file re-exports wholesale (`export * from './m'`).
|
|
51
|
+
* A directory barrel is built almost entirely out of these, and a barrel is
|
|
52
|
+
* what application code imports — so without following them, every name
|
|
53
|
+
* reached through one resolves to nothing.
|
|
54
|
+
*/
|
|
55
|
+
readonly starExports: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Local names proven to hold an HTTP client INSTANCE — bound directly to
|
|
58
|
+
* `axios.create(...)`, or to another local name that is one. Cross-file
|
|
59
|
+
* chains are followed at query time by {@link isHttpClientRef}, not here.
|
|
60
|
+
*/
|
|
61
|
+
readonly clients: Set<string>;
|
|
62
|
+
/**
|
|
63
|
+
* True when this file declares its own top-level binding named `axios` that
|
|
64
|
+
* is NOT the axios module.
|
|
65
|
+
*
|
|
66
|
+
* The bare spelling `axios` is trusted without proof — it predates this
|
|
67
|
+
* binding and is what the original query matched on. That is right for
|
|
68
|
+
* `import axios from 'axios'` and for `const axios = require('axios')`, and
|
|
69
|
+
* wrong for `const axios = fakeFactory`, where the spelling is the only
|
|
70
|
+
* evidence and it is false. One flag, because the shortcut only ever applies
|
|
71
|
+
* to this one name.
|
|
72
|
+
*/
|
|
73
|
+
readonly axiosShadowed: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Repo-wide facts, with everything the shared fold needs precomputed.
|
|
77
|
+
*
|
|
78
|
+
* `constants`, `keys` and `resolveImport` are derived from `byFile` and built
|
|
79
|
+
* ONCE by {@link buildJsRepoFacts}, never per lookup: materializing a key set
|
|
80
|
+
* at each call site makes every resolution O(files) and the whole scan
|
|
81
|
+
* quadratic in a repo's file count. That was only half true before —
|
|
82
|
+
* `resolveConstant` rebuilt its own key set on every fold regardless, so the
|
|
83
|
+
* mitigation this comment describes was not in force for any resolution that
|
|
84
|
+
* went through the shared core. It now takes `keys` as an argument.
|
|
85
|
+
*/
|
|
86
|
+
export interface JsRepoFacts {
|
|
87
|
+
readonly byFile: ReadonlyMap<string, JsModuleFacts>;
|
|
88
|
+
readonly constants: RepoConstants;
|
|
89
|
+
readonly keys: ReadonlySet<string>;
|
|
90
|
+
/**
|
|
91
|
+
* {@link resolveJsImport} bound to a prebuilt basename index and memoized for
|
|
92
|
+
* the lifetime of the facts. Every resolution inside this module goes through
|
|
93
|
+
* it rather than the bare export: the widened consumer query matches every
|
|
94
|
+
* `<identifier>.<verb>(…)` call in the repo, so an unindexed lookup ran once
|
|
95
|
+
* per call site over every repo key.
|
|
96
|
+
*/
|
|
97
|
+
readonly resolveImport: ImportResolver;
|
|
98
|
+
}
|
|
99
|
+
/** Build the {@link JsRepoFacts} projections from per-file facts. */
|
|
100
|
+
export declare function buildJsRepoFacts(byFile: ReadonlyMap<string, JsModuleFacts>): JsRepoFacts;
|
|
101
|
+
/**
|
|
102
|
+
* Standalone {@link ImportResolver} — the same rules as {@link resolveImportWith}
|
|
103
|
+
* with the basename index built on the spot.
|
|
104
|
+
*
|
|
105
|
+
* Production goes through `JsRepoFacts.resolveImport`, which holds one index
|
|
106
|
+
* for the whole repo and memoizes; this export exists so the resolution rules
|
|
107
|
+
* can be exercised directly against a key set.
|
|
108
|
+
*/
|
|
109
|
+
export declare const resolveJsImport: ImportResolver;
|
|
110
|
+
/**
|
|
111
|
+
* Parse a `+`-concatenation / template string into an operand list the shared
|
|
112
|
+
* fold can resolve, or `null` when a term is not a string literal or a
|
|
113
|
+
* resolvable name reference.
|
|
114
|
+
*
|
|
115
|
+
* Handles the two shapes a JS route path is built with:
|
|
116
|
+
* `BASE + "/users"` → [ref BASE, literal /users]
|
|
117
|
+
* `` `${BASE}/users/${id}` `` → [ref BASE, literal /users/, ref id]
|
|
118
|
+
*
|
|
119
|
+
* A member reference inside either (`${API_ROUTE_PATH.LISTS}`) becomes a
|
|
120
|
+
* dotted `ref`, which the flattened table above resolves directly.
|
|
121
|
+
*/
|
|
122
|
+
export declare function parseJsConstOperands(node: Parser.SyntaxNode, depth?: number): Operand[] | null;
|
|
123
|
+
/**
|
|
124
|
+
* The dotted name a member expression denotes (`A.B.C`), or `null` for a
|
|
125
|
+
* computed / non-identifier chain (`A[key]`, `fn().B`) that has no stable
|
|
126
|
+
* textual key.
|
|
127
|
+
*/
|
|
128
|
+
export declare function dottedNameOf(node: Parser.SyntaxNode): string | null;
|
|
129
|
+
/**
|
|
130
|
+
* Extract one file's {@link JsModuleFacts} from its parsed tree.
|
|
131
|
+
*
|
|
132
|
+
* Only TOP-LEVEL declarations are collected. A route table or an API client
|
|
133
|
+
* defined inside a function body is not a module constant, and treating it as
|
|
134
|
+
* one would let an unrelated same-named local shadow the real export.
|
|
135
|
+
*/
|
|
136
|
+
export declare function extractJsModuleFacts(tree: Parser.Tree): JsModuleFacts;
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a path reference at a call site to its literal string, or `null`.
|
|
139
|
+
*
|
|
140
|
+
* `ref` is the dotted name as written (`API_ROUTE_PATH.LINKS`, or a bare
|
|
141
|
+
* `BASE_PATH`). Resolution order:
|
|
142
|
+
*
|
|
143
|
+
* 1. The dotted name as a constant of the CURRENT file — hits when the table
|
|
144
|
+
* is declared in the same file (flattened to dotted literal keys).
|
|
145
|
+
* 2. The base name as an IMPORT of the current file — hop to the defining
|
|
146
|
+
* file and look the dotted name up there, re-hopping through barrels that
|
|
147
|
+
* re-export it, bounded by {@link MAX_REEXPORT_HOPS}.
|
|
148
|
+
*
|
|
149
|
+
* Returns `null` on anything it cannot fully fold, which leaves the call site
|
|
150
|
+
* exactly as unmatched as it is today — never a guessed path.
|
|
151
|
+
*/
|
|
152
|
+
export declare function resolveJsMemberPath(fileKey: string, ref: string, facts: JsRepoFacts): string | null;
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the first argument of an HTTP call to a path string, or `null` when
|
|
155
|
+
* the expression is not a path shape this binding understands.
|
|
156
|
+
*
|
|
157
|
+
* Accepts a plain literal, a constant reference (`BASE_PATH`), a table member
|
|
158
|
+
* (`API_ROUTE_PATH.LINKS`), a template string, and a `+`-concatenation of any
|
|
159
|
+
* of those. Template and concat forms fold PARTIALLY — see
|
|
160
|
+
* {@link foldTermOrPlaceholder}.
|
|
161
|
+
*
|
|
162
|
+
* A reference that resolves to nothing returns `null` (skip), and so does a
|
|
163
|
+
* mixed expression whose leading term is unresolved — see {@link FoldedPath}.
|
|
164
|
+
*/
|
|
165
|
+
export declare function resolveJsPathExpression(fileKey: string, node: Parser.SyntaxNode, facts: JsRepoFacts): string | null;
|
|
166
|
+
/**
|
|
167
|
+
* Whether `name`, as referenced in `fileKey`, holds an HTTP client instance.
|
|
168
|
+
*
|
|
169
|
+
* Chases local aliases and import/export hops so the common app shape —
|
|
170
|
+
* `axios.create()` in `lib/axios.config.ts`, `export default apiClient`,
|
|
171
|
+
* `import apiClient from '@/lib/axios.config'` at the call site — is proven
|
|
172
|
+
* rather than pattern-matched on the receiver's spelling.
|
|
173
|
+
*
|
|
174
|
+
* Deliberately conservative: an unproven receiver returns `false`, which keeps
|
|
175
|
+
* today's behavior for it. The alternative — trusting any identifier with an
|
|
176
|
+
* HTTP-verb method — would classify every Express `router.get('/x', handler)`
|
|
177
|
+
* provider as a consumer of itself.
|
|
178
|
+
*/
|
|
179
|
+
/**
|
|
180
|
+
* Whether `name`, as a receiver in `fileKey`, IS the axios module — as opposed
|
|
181
|
+
* to an instance built from it, which is {@link isHttpClientRef}'s question.
|
|
182
|
+
*
|
|
183
|
+
* Two ways to be it. The bare spelling `axios` predates the widened query — it
|
|
184
|
+
* is what the original `(#eq? @obj "axios")` pattern matched — so it stays
|
|
185
|
+
* trusted by default, and a file with no facts keeps exactly that behavior; it
|
|
186
|
+
* is withdrawn only where the file itself binds that name to something else.
|
|
187
|
+
* The other way is a declared import or `require` of `'axios'` under any local
|
|
188
|
+
* name, which is proof rather than convention and covers the aliased form the
|
|
189
|
+
* spelling rule cannot see.
|
|
190
|
+
*/
|
|
191
|
+
export declare function isAxiosNamespace(fileKey: string, name: string, facts: JsRepoFacts): boolean;
|
|
192
|
+
export declare function isHttpClientRef(fileKey: string, name: string, facts: JsRepoFacts): boolean;
|