@wrongstack/tools 0.298.3 → 0.300.0
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/audit.js +6 -2
- package/dist/bash.js +20 -2
- package/dist/builtin.js +1901 -327
- package/dist/codebase-index/background-indexer.d.ts +6 -1
- package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +34 -0
- package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +34 -0
- package/dist/codebase-index/import-extractor.d.ts +39 -0
- package/dist/codebase-index/index-service.d.ts +24 -2
- package/dist/codebase-index/index.d.ts +4 -2
- package/dist/codebase-index/index.js +1779 -256
- package/dist/codebase-index/languages.d.ts +24 -0
- package/dist/codebase-index/module-resolver.d.ts +78 -0
- package/dist/codebase-index/module-roots.d.ts +81 -0
- package/dist/codebase-index/parser-output.d.ts +29 -0
- package/dist/codebase-index/project-server.js +1556 -237
- package/dist/codebase-index/rs-parser.d.ts +22 -0
- package/dist/codebase-index/schema.d.ts +47 -1
- package/dist/codebase-index/worker-protocol.d.ts +14 -0
- package/dist/codebase-index/worker.js +1545 -238
- package/dist/codebase-index/writer-graph-helpers.d.ts +17 -5
- package/dist/codebase-index/writer-graph-reader.d.ts +24 -1
- package/dist/codebase-index/writer-ref-mapper.d.ts +3 -0
- package/dist/codebase-index/writer-schema.d.ts +15 -3
- package/dist/codebase-index/writer.d.ts +97 -4
- package/dist/exec.js +39 -2
- package/dist/format.js +6 -2
- package/dist/index.js +1901 -327
- package/dist/install.js +6 -2
- package/dist/json.js +51 -2
- package/dist/languages/index.js +6 -2
- package/dist/lint.js +6 -2
- package/dist/outdated.js +6 -2
- package/dist/pack.js +1899 -327
- package/dist/process-registry.d.ts +6 -0
- package/dist/process-registry.js +6 -2
- package/dist/ps-slash.js +10 -2
- package/dist/read.js +1551 -244
- package/dist/test.js +6 -2
- package/dist/tool-tier.js +1901 -327
- package/dist/typecheck.js +6 -2
- package/package.json +3 -3
- package/dist/codebase-index/refs-extractor.d.ts +0 -11
package/dist/read.js
CHANGED
|
@@ -27,7 +27,10 @@ function detectLang(file) {
|
|
|
27
27
|
if (!ext) return null;
|
|
28
28
|
return EXT_TO_LANG[ext] ?? null;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
function languageFamily(lang) {
|
|
31
|
+
return LANG_FAMILY[lang] ?? "other";
|
|
32
|
+
}
|
|
33
|
+
var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
|
|
31
34
|
var init_languages = __esm({
|
|
32
35
|
"src/codebase-index/languages.ts"() {
|
|
33
36
|
"use strict";
|
|
@@ -118,6 +121,52 @@ var init_languages = __esm({
|
|
|
118
121
|
procfile: "other",
|
|
119
122
|
justfile: "other"
|
|
120
123
|
};
|
|
124
|
+
LANG_FAMILY = {
|
|
125
|
+
// Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
|
|
126
|
+
// imports from — and is imported by — plain .ts files.
|
|
127
|
+
ts: "js",
|
|
128
|
+
tsx: "js",
|
|
129
|
+
js: "js",
|
|
130
|
+
jsx: "js",
|
|
131
|
+
vue: "js",
|
|
132
|
+
svelte: "js",
|
|
133
|
+
go: "go",
|
|
134
|
+
py: "py",
|
|
135
|
+
rs: "rs",
|
|
136
|
+
// The JVM resolves across languages: Kotlin and Scala call Java directly.
|
|
137
|
+
java: "jvm",
|
|
138
|
+
kotlin: "jvm",
|
|
139
|
+
scala: "jvm",
|
|
140
|
+
csharp: "dotnet",
|
|
141
|
+
// A .h header is consumed by both C and C++ translation units.
|
|
142
|
+
c: "c",
|
|
143
|
+
cpp: "c",
|
|
144
|
+
ruby: "ruby",
|
|
145
|
+
php: "php",
|
|
146
|
+
swift: "swift",
|
|
147
|
+
dart: "dart",
|
|
148
|
+
elixir: "elixir",
|
|
149
|
+
haskell: "haskell",
|
|
150
|
+
zig: "zig",
|
|
151
|
+
lua: "lua",
|
|
152
|
+
r: "r",
|
|
153
|
+
shell: "shell",
|
|
154
|
+
sql: "sql",
|
|
155
|
+
json: "data",
|
|
156
|
+
yaml: "data",
|
|
157
|
+
toml: "data",
|
|
158
|
+
html: "web",
|
|
159
|
+
css: "web",
|
|
160
|
+
proto: "proto",
|
|
161
|
+
graphql: "graphql",
|
|
162
|
+
md: "other",
|
|
163
|
+
other: "other"
|
|
164
|
+
};
|
|
165
|
+
LANG_FAMILY_ENTRIES = Object.freeze(
|
|
166
|
+
Object.entries(LANG_FAMILY).map(
|
|
167
|
+
([lang, family]) => Object.freeze([lang, family])
|
|
168
|
+
)
|
|
169
|
+
);
|
|
121
170
|
}
|
|
122
171
|
});
|
|
123
172
|
|
|
@@ -282,7 +331,7 @@ function getTypeName(name) {
|
|
|
282
331
|
function deduplicateRefs(refs) {
|
|
283
332
|
const seen = /* @__PURE__ */ new Set();
|
|
284
333
|
return refs.filter((r) => {
|
|
285
|
-
const key = `${r.toName}:${r.callType}:${r.line}`;
|
|
334
|
+
const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
|
|
286
335
|
if (seen.has(key)) return false;
|
|
287
336
|
seen.add(key);
|
|
288
337
|
return true;
|
|
@@ -292,10 +341,16 @@ function getImportSpecifierName(spec) {
|
|
|
292
341
|
return spec.propertyName?.text ?? spec.name.text;
|
|
293
342
|
}
|
|
294
343
|
function emitImportSpecifierRefs(node, refs, lineNum) {
|
|
344
|
+
const module = moduleSpecifierOf(node.moduleSpecifier);
|
|
295
345
|
const clause = node.importClause;
|
|
296
|
-
if (!clause)
|
|
346
|
+
if (!clause) {
|
|
347
|
+
if (module) {
|
|
348
|
+
refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
297
352
|
if (clause.name) {
|
|
298
|
-
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
|
|
353
|
+
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
|
|
299
354
|
}
|
|
300
355
|
const bindings = clause.namedBindings;
|
|
301
356
|
if (!bindings) return;
|
|
@@ -305,26 +360,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
|
|
|
305
360
|
fromId: 0,
|
|
306
361
|
toName: getImportSpecifierName(element),
|
|
307
362
|
callType: "import",
|
|
308
|
-
line: lineNum
|
|
363
|
+
line: lineNum,
|
|
364
|
+
module
|
|
309
365
|
});
|
|
310
366
|
}
|
|
311
367
|
} else if (ts.isNamespaceImport(bindings)) {
|
|
312
|
-
refs.push({
|
|
368
|
+
refs.push({
|
|
369
|
+
fromId: 0,
|
|
370
|
+
toName: bindings.name.text,
|
|
371
|
+
callType: "import",
|
|
372
|
+
line: lineNum,
|
|
373
|
+
module
|
|
374
|
+
});
|
|
313
375
|
}
|
|
314
376
|
}
|
|
377
|
+
function moduleSpecifierOf(node) {
|
|
378
|
+
return node && ts.isStringLiteral(node) ? node.text : void 0;
|
|
379
|
+
}
|
|
315
380
|
function emitExportSpecifierRefs(node, refs, lineNum) {
|
|
381
|
+
const module = moduleSpecifierOf(node.moduleSpecifier);
|
|
316
382
|
const clause = node.exportClause;
|
|
317
383
|
if (clause && ts.isNamespaceExport(clause)) {
|
|
318
|
-
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
|
|
384
|
+
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
|
|
319
385
|
return;
|
|
320
386
|
}
|
|
321
387
|
if (clause && ts.isNamedExports(clause)) {
|
|
322
388
|
for (const element of clause.elements) {
|
|
323
389
|
const originalName = element.propertyName?.text ?? element.name.text;
|
|
324
|
-
refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
|
|
390
|
+
refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
|
|
325
391
|
}
|
|
326
392
|
return;
|
|
327
393
|
}
|
|
394
|
+
if (module) {
|
|
395
|
+
refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
|
|
396
|
+
}
|
|
328
397
|
}
|
|
329
398
|
var ts, tsLoad, kindMapCache;
|
|
330
399
|
var init_ts_parser = __esm({
|
|
@@ -337,21 +406,21 @@ var init_ts_parser = __esm({
|
|
|
337
406
|
});
|
|
338
407
|
|
|
339
408
|
// src/_win32-resolve.ts
|
|
340
|
-
import * as
|
|
341
|
-
import * as
|
|
409
|
+
import * as fs3 from "node:fs";
|
|
410
|
+
import * as path6 from "node:path";
|
|
342
411
|
function resolveWin32Command(cmd) {
|
|
343
412
|
if (process.platform !== "win32") return cmd;
|
|
344
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
413
|
+
if (cmd.includes("/") || cmd.includes("\\") || path6.extname(cmd.replace(/\//g, "\\"))) {
|
|
345
414
|
return cmd;
|
|
346
415
|
}
|
|
347
416
|
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
348
|
-
const pathDirs = (process.env["PATH"] ?? "").split(
|
|
417
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path6.delimiter);
|
|
349
418
|
for (const dir of pathDirs) {
|
|
350
|
-
const base =
|
|
419
|
+
const base = path6.join(dir, cmd);
|
|
351
420
|
for (const ext of pathext) {
|
|
352
421
|
const full = `${base}${ext}`;
|
|
353
422
|
try {
|
|
354
|
-
|
|
423
|
+
fs3.accessSync(full, fs3.constants.X_OK);
|
|
355
424
|
return full;
|
|
356
425
|
} catch {
|
|
357
426
|
}
|
|
@@ -365,6 +434,82 @@ var init_win32_resolve = __esm({
|
|
|
365
434
|
}
|
|
366
435
|
});
|
|
367
436
|
|
|
437
|
+
// src/codebase-index/parser-output.ts
|
|
438
|
+
function coerceSymbols(value) {
|
|
439
|
+
if (!Array.isArray(value)) return [];
|
|
440
|
+
return value.flatMap((entry) => {
|
|
441
|
+
const candidate = entry;
|
|
442
|
+
if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
name: candidate.name,
|
|
446
|
+
kind: candidate.kind,
|
|
447
|
+
line: typeof candidate.line === "number" ? candidate.line : 1,
|
|
448
|
+
col: typeof candidate.col === "number" ? candidate.col : 0,
|
|
449
|
+
signature: typeof candidate.signature === "string" ? candidate.signature : "",
|
|
450
|
+
scope: typeof candidate.scope === "string" ? candidate.scope : ""
|
|
451
|
+
}
|
|
452
|
+
];
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function coerceRefs(value, lang) {
|
|
456
|
+
if (!Array.isArray(value)) return [];
|
|
457
|
+
return value.flatMap((entry) => {
|
|
458
|
+
const candidate = entry;
|
|
459
|
+
if (typeof candidate.toName !== "string" || !candidate.toName) return [];
|
|
460
|
+
if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
|
|
461
|
+
const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
|
|
462
|
+
return [
|
|
463
|
+
{
|
|
464
|
+
fromId: 0,
|
|
465
|
+
toName: candidate.toName,
|
|
466
|
+
callType: candidate.callType,
|
|
467
|
+
line: typeof candidate.line === "number" ? candidate.line : 1,
|
|
468
|
+
lang,
|
|
469
|
+
module
|
|
470
|
+
}
|
|
471
|
+
];
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function parseParserOutput(stdout, lang) {
|
|
475
|
+
const trimmed = stdout.trim();
|
|
476
|
+
if (!trimmed) return { symbols: [], refs: [] };
|
|
477
|
+
let parsed;
|
|
478
|
+
try {
|
|
479
|
+
parsed = JSON.parse(trimmed);
|
|
480
|
+
} catch {
|
|
481
|
+
return { symbols: [], refs: [] };
|
|
482
|
+
}
|
|
483
|
+
if (Array.isArray(parsed)) return { symbols: coerceSymbols(parsed), refs: [] };
|
|
484
|
+
const record = parsed;
|
|
485
|
+
return {
|
|
486
|
+
symbols: coerceSymbols(record.symbols),
|
|
487
|
+
refs: dedupeRefs(coerceRefs(record.refs, lang))
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function dedupeRefs(refs) {
|
|
491
|
+
const seen = /* @__PURE__ */ new Set();
|
|
492
|
+
return refs.filter((ref) => {
|
|
493
|
+
const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
|
|
494
|
+
if (seen.has(key)) return false;
|
|
495
|
+
seen.add(key);
|
|
496
|
+
return true;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
var CALL_TYPES;
|
|
500
|
+
var init_parser_output = __esm({
|
|
501
|
+
"src/codebase-index/parser-output.ts"() {
|
|
502
|
+
"use strict";
|
|
503
|
+
CALL_TYPES = /* @__PURE__ */ new Set([
|
|
504
|
+
"call",
|
|
505
|
+
"type_ref",
|
|
506
|
+
"inherit",
|
|
507
|
+
"implement",
|
|
508
|
+
"import"
|
|
509
|
+
]);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
368
513
|
// src/codebase-index/spawn-gate.ts
|
|
369
514
|
function withSpawnGate(fn) {
|
|
370
515
|
const run = chain.then(fn, fn);
|
|
@@ -390,8 +535,8 @@ __export(go_parser_exports, {
|
|
|
390
535
|
});
|
|
391
536
|
import { spawn } from "node:child_process";
|
|
392
537
|
import * as os from "node:os";
|
|
393
|
-
import * as
|
|
394
|
-
import * as
|
|
538
|
+
import * as path7 from "node:path";
|
|
539
|
+
import * as fs4 from "node:fs/promises";
|
|
395
540
|
async function parseSymbols2(opts) {
|
|
396
541
|
const { file, content, lang } = opts;
|
|
397
542
|
try {
|
|
@@ -399,7 +544,8 @@ async function parseSymbols2(opts) {
|
|
|
399
544
|
if (parsed.symbols.length > 0) {
|
|
400
545
|
return parsed;
|
|
401
546
|
}
|
|
402
|
-
|
|
547
|
+
const fallback = fallbackParse(file, content, lang);
|
|
548
|
+
return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
|
|
403
549
|
} catch {
|
|
404
550
|
return fallbackParse(file, content, lang);
|
|
405
551
|
}
|
|
@@ -463,9 +609,9 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
463
609
|
try {
|
|
464
610
|
let scriptPath = _cachedGoScriptPath;
|
|
465
611
|
if (!scriptPath) {
|
|
466
|
-
const tmpDir = await
|
|
467
|
-
scriptPath =
|
|
468
|
-
await
|
|
612
|
+
const tmpDir = await fs4.mkdtemp(path7.join(os.tmpdir(), "ws-go-parse-"));
|
|
613
|
+
scriptPath = path7.join(tmpDir, "parse.go");
|
|
614
|
+
await fs4.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
469
615
|
_cachedGoScriptPath = scriptPath;
|
|
470
616
|
}
|
|
471
617
|
const goBinary = resolveWin32Command("go");
|
|
@@ -507,8 +653,8 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
507
653
|
if (code !== 0 || !stdout.trim()) {
|
|
508
654
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
509
655
|
}
|
|
510
|
-
const
|
|
511
|
-
const symbols =
|
|
656
|
+
const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
|
|
657
|
+
const symbols = rawSymbols.map((s) => ({
|
|
512
658
|
id: 0,
|
|
513
659
|
lang,
|
|
514
660
|
kind: s.kind,
|
|
@@ -521,7 +667,7 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
521
667
|
scope: s.scope ?? "",
|
|
522
668
|
text: `${s.name} ${s.signature ?? ""}`.trim()
|
|
523
669
|
}));
|
|
524
|
-
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
670
|
+
return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
|
|
525
671
|
} catch {
|
|
526
672
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
527
673
|
}
|
|
@@ -531,6 +677,7 @@ var init_go_parser = __esm({
|
|
|
531
677
|
"src/codebase-index/go-parser.ts"() {
|
|
532
678
|
"use strict";
|
|
533
679
|
init_win32_resolve();
|
|
680
|
+
init_parser_output();
|
|
534
681
|
init_spawn_gate();
|
|
535
682
|
init_languages();
|
|
536
683
|
GO_PARSE_SCRIPT = `
|
|
@@ -544,6 +691,7 @@ import (
|
|
|
544
691
|
"go/token"
|
|
545
692
|
"io"
|
|
546
693
|
"os"
|
|
694
|
+
"strconv"
|
|
547
695
|
"strings"
|
|
548
696
|
)
|
|
549
697
|
|
|
@@ -556,16 +704,34 @@ type Sym struct {
|
|
|
556
704
|
Scope string \`json:"scope"\`
|
|
557
705
|
}
|
|
558
706
|
|
|
707
|
+
// Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
|
|
708
|
+
// yields both. Module is the import path for CallType "import", else empty.
|
|
709
|
+
type Ref struct {
|
|
710
|
+
ToName string \`json:"toName"\`
|
|
711
|
+
CallType string \`json:"callType"\`
|
|
712
|
+
Line int \`json:"line"\`
|
|
713
|
+
Module string \`json:"module"\`
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
type Result struct {
|
|
717
|
+
Symbols []Sym \`json:"symbols"\`
|
|
718
|
+
Refs []Ref \`json:"refs"\`
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
func emptyResult() string {
|
|
722
|
+
return "{\\"symbols\\":[],\\"refs\\":[]}"
|
|
723
|
+
}
|
|
724
|
+
|
|
559
725
|
func main() {
|
|
560
726
|
src, err := io.ReadAll(os.Stdin)
|
|
561
727
|
if err != nil {
|
|
562
|
-
fmt.Print(
|
|
728
|
+
fmt.Print(emptyResult())
|
|
563
729
|
return
|
|
564
730
|
}
|
|
565
731
|
fset := token.NewFileSet()
|
|
566
732
|
node, err := parser.ParseFile(fset, "src.go", src, 0)
|
|
567
733
|
if err != nil {
|
|
568
|
-
fmt.Print(
|
|
734
|
+
fmt.Print(emptyResult())
|
|
569
735
|
return
|
|
570
736
|
}
|
|
571
737
|
|
|
@@ -629,9 +795,43 @@ func main() {
|
|
|
629
795
|
}
|
|
630
796
|
}
|
|
631
797
|
|
|
632
|
-
|
|
798
|
+
refs := []Ref{}
|
|
799
|
+
ast.Inspect(node, func(n ast.Node) bool {
|
|
800
|
+
switch expr := n.(type) {
|
|
801
|
+
case *ast.CallExpr:
|
|
802
|
+
line := fset.Position(expr.Pos()).Line
|
|
803
|
+
switch fun := expr.Fun.(type) {
|
|
804
|
+
case *ast.Ident:
|
|
805
|
+
refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
|
|
806
|
+
case *ast.SelectorExpr:
|
|
807
|
+
// Record the selected name (\`Join\` of \`filepath.Join\`): it is the
|
|
808
|
+
// declared symbol name, so it resolves the same way the TypeScript
|
|
809
|
+
// and Python extractors' call refs do.
|
|
810
|
+
refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
|
|
811
|
+
}
|
|
812
|
+
case *ast.ImportSpec:
|
|
813
|
+
if expr.Path != nil {
|
|
814
|
+
if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
|
|
815
|
+
line := fset.Position(expr.Pos()).Line
|
|
816
|
+
// A Go import names a package, not a symbol; the package's
|
|
817
|
+
// last path segment is the name it is referenced by.
|
|
818
|
+
name := importPath
|
|
819
|
+
if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
|
|
820
|
+
name = importPath[idx+1:]
|
|
821
|
+
}
|
|
822
|
+
refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return true
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
if syms == nil {
|
|
830
|
+
syms = []Sym{}
|
|
831
|
+
}
|
|
832
|
+
data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
|
|
633
833
|
if err != nil {
|
|
634
|
-
fmt.Print(
|
|
834
|
+
fmt.Print(emptyResult())
|
|
635
835
|
return
|
|
636
836
|
}
|
|
637
837
|
fmt.Print(string(data))
|
|
@@ -983,9 +1183,13 @@ var init_generic_parser = __esm({
|
|
|
983
1183
|
],
|
|
984
1184
|
elixir: [
|
|
985
1185
|
{ re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
|
|
986
|
-
|
|
1186
|
+
// Dotted module names must be captured whole: `alias Foo.Bar` resolves
|
|
1187
|
+
// against this symbol, and a `Foo`-only capture never matches it.
|
|
1188
|
+
{ re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
|
|
987
1189
|
],
|
|
988
1190
|
haskell: [
|
|
1191
|
+
// Target of `import Data.List`.
|
|
1192
|
+
{ re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
|
|
989
1193
|
{ re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
|
|
990
1194
|
{ re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
|
|
991
1195
|
{ re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
|
|
@@ -1076,9 +1280,9 @@ __export(py_parser_exports, {
|
|
|
1076
1280
|
parseSymbols: () => parseSymbols4
|
|
1077
1281
|
});
|
|
1078
1282
|
import { spawn as spawn2 } from "node:child_process";
|
|
1079
|
-
import * as
|
|
1283
|
+
import * as fs5 from "node:fs/promises";
|
|
1080
1284
|
import * as os2 from "node:os";
|
|
1081
|
-
import * as
|
|
1285
|
+
import * as path8 from "node:path";
|
|
1082
1286
|
async function parseSymbols4(opts) {
|
|
1083
1287
|
const { file, content, lang } = opts;
|
|
1084
1288
|
try {
|
|
@@ -1156,10 +1360,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
|
1156
1360
|
async function syncPyParse(filePath, content, lang) {
|
|
1157
1361
|
try {
|
|
1158
1362
|
if (!_cachedScriptPath) {
|
|
1159
|
-
const tmpDir =
|
|
1160
|
-
await
|
|
1161
|
-
_cachedScriptPath =
|
|
1162
|
-
await
|
|
1363
|
+
const tmpDir = path8.join(os2.tmpdir(), "ws-py-parse");
|
|
1364
|
+
await fs5.mkdir(tmpDir, { recursive: true });
|
|
1365
|
+
_cachedScriptPath = path8.join(tmpDir, "parse.py");
|
|
1366
|
+
await fs5.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
1163
1367
|
}
|
|
1164
1368
|
cachedPyBinary ??= resolvePython();
|
|
1165
1369
|
const pyBinary = await cachedPyBinary;
|
|
@@ -1173,7 +1377,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1173
1377
|
if (code !== 0 || !stdout.trim()) {
|
|
1174
1378
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1175
1379
|
}
|
|
1176
|
-
const raw =
|
|
1380
|
+
const { symbols: raw, refs } = parseParserOutput(stdout, lang);
|
|
1177
1381
|
const symbols = raw.map((s) => ({
|
|
1178
1382
|
id: 0,
|
|
1179
1383
|
lang,
|
|
@@ -1187,7 +1391,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1187
1391
|
scope: s.scope ?? "",
|
|
1188
1392
|
text: `${s.name} ${s.signature ?? ""}`.trim()
|
|
1189
1393
|
}));
|
|
1190
|
-
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
1394
|
+
return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
|
|
1191
1395
|
} catch {
|
|
1192
1396
|
return null;
|
|
1193
1397
|
}
|
|
@@ -1198,6 +1402,7 @@ var init_py_parser = __esm({
|
|
|
1198
1402
|
"use strict";
|
|
1199
1403
|
init_win32_resolve();
|
|
1200
1404
|
init_generic_parser();
|
|
1405
|
+
init_parser_output();
|
|
1201
1406
|
init_spawn_gate();
|
|
1202
1407
|
init_languages();
|
|
1203
1408
|
PY_PARSE_SCRIPT = `import ast, json, sys, os
|
|
@@ -1259,7 +1464,18 @@ class Sym:
|
|
|
1259
1464
|
def is_private(name):
|
|
1260
1465
|
return name.startswith("__") and not name.endswith("__")
|
|
1261
1466
|
|
|
1467
|
+
def leaf_name(node):
|
|
1468
|
+
# Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
|
|
1469
|
+
# TypeScript and Go extractors record call refs, so resolution behaves the
|
|
1470
|
+
# same across languages.
|
|
1471
|
+
if isinstance(node, ast.Attribute):
|
|
1472
|
+
return node.attr
|
|
1473
|
+
if isinstance(node, ast.Name):
|
|
1474
|
+
return node.id
|
|
1475
|
+
return get_name(node).split(".")[-1]
|
|
1476
|
+
|
|
1262
1477
|
syms = []
|
|
1478
|
+
refs = []
|
|
1263
1479
|
errors = []
|
|
1264
1480
|
|
|
1265
1481
|
try:
|
|
@@ -1267,7 +1483,7 @@ try:
|
|
|
1267
1483
|
tree = ast.parse(source, filename=sys.argv[1])
|
|
1268
1484
|
except Exception as e:
|
|
1269
1485
|
errors.append(str(e))
|
|
1270
|
-
print("[]")
|
|
1486
|
+
print(json.dumps({"symbols": [], "refs": []}))
|
|
1271
1487
|
sys.exit(0)
|
|
1272
1488
|
|
|
1273
1489
|
# Module-level scope
|
|
@@ -1401,7 +1617,42 @@ class ModuleVisitor(ast.NodeVisitor):
|
|
|
1401
1617
|
visitor = ModuleVisitor()
|
|
1402
1618
|
visitor.visit(tree)
|
|
1403
1619
|
|
|
1404
|
-
|
|
1620
|
+
# Refs need a separate full walk: ModuleVisitor deliberately does not descend
|
|
1621
|
+
# into function bodies (it would index locals as symbols), but that is exactly
|
|
1622
|
+
# where the calls are.
|
|
1623
|
+
for node in ast.walk(tree):
|
|
1624
|
+
if isinstance(node, ast.Call):
|
|
1625
|
+
name = leaf_name(node.func)
|
|
1626
|
+
if name:
|
|
1627
|
+
refs.append({"toName": name, "callType": "call", "line": node.lineno})
|
|
1628
|
+
elif isinstance(node, ast.Import):
|
|
1629
|
+
for alias in node.names:
|
|
1630
|
+
refs.append({
|
|
1631
|
+
"toName": alias.name.split(".")[-1],
|
|
1632
|
+
"callType": "import",
|
|
1633
|
+
"line": node.lineno,
|
|
1634
|
+
"module": alias.name,
|
|
1635
|
+
})
|
|
1636
|
+
elif isinstance(node, ast.ImportFrom):
|
|
1637
|
+
# PEP 328: node.level is the number of leading dots. Preserving them is
|
|
1638
|
+
# what lets the resolver walk up from the importing file's package \u2014
|
|
1639
|
+
# dropping them made \`from .foo import X\` indistinguishable from an
|
|
1640
|
+
# absolute \`foo\`.
|
|
1641
|
+
module = ("." * (node.level or 0)) + (node.module or "")
|
|
1642
|
+
for alias in node.names:
|
|
1643
|
+
refs.append({
|
|
1644
|
+
"toName": alias.name,
|
|
1645
|
+
"callType": "import",
|
|
1646
|
+
"line": node.lineno,
|
|
1647
|
+
"module": module,
|
|
1648
|
+
})
|
|
1649
|
+
elif isinstance(node, ast.ClassDef):
|
|
1650
|
+
for base in node.bases:
|
|
1651
|
+
name = leaf_name(base)
|
|
1652
|
+
if name:
|
|
1653
|
+
refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
|
|
1654
|
+
|
|
1655
|
+
print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
|
|
1405
1656
|
`;
|
|
1406
1657
|
_cachedScriptPath = null;
|
|
1407
1658
|
}
|
|
@@ -1414,107 +1665,10 @@ __export(rs_parser_exports, {
|
|
|
1414
1665
|
parseSymbols: () => parseSymbols5
|
|
1415
1666
|
});
|
|
1416
1667
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
1417
|
-
import { execFile, spawn as spawn3 } from "node:child_process";
|
|
1418
|
-
import * as fs5 from "node:fs/promises";
|
|
1419
|
-
import * as path7 from "node:path";
|
|
1420
1668
|
async function parseSymbols5(opts) {
|
|
1421
1669
|
const { file, content, lang } = opts;
|
|
1422
|
-
const nativeAvailable = await checkNativeParser();
|
|
1423
|
-
if (nativeAvailable) {
|
|
1424
|
-
const result = await withSpawnGate(() => tryNativeParse(file, content));
|
|
1425
|
-
if (result) return result;
|
|
1426
|
-
}
|
|
1427
1670
|
return regexParse({ file, content, lang });
|
|
1428
1671
|
}
|
|
1429
|
-
function probe(command, args) {
|
|
1430
|
-
return new Promise((resolve4, reject) => {
|
|
1431
|
-
execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
|
|
1432
|
-
if (error) reject(error);
|
|
1433
|
-
else resolve4();
|
|
1434
|
-
});
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
function checkNativeParser() {
|
|
1438
|
-
nativeParserAvailability ??= (async () => {
|
|
1439
|
-
try {
|
|
1440
|
-
await probe("rustc", ["--version"]);
|
|
1441
|
-
const toolsDir = path7.join(process.cwd(), "tools");
|
|
1442
|
-
await probe(
|
|
1443
|
-
"cargo",
|
|
1444
|
-
[
|
|
1445
|
-
"metadata",
|
|
1446
|
-
"--no-deps",
|
|
1447
|
-
"--format-version",
|
|
1448
|
-
"1",
|
|
1449
|
-
"--manifest-path",
|
|
1450
|
-
path7.join(toolsDir, "Cargo.toml")
|
|
1451
|
-
]
|
|
1452
|
-
);
|
|
1453
|
-
return true;
|
|
1454
|
-
} catch {
|
|
1455
|
-
return false;
|
|
1456
|
-
}
|
|
1457
|
-
})();
|
|
1458
|
-
return nativeParserAvailability;
|
|
1459
|
-
}
|
|
1460
|
-
async function tryNativeParse(file, content) {
|
|
1461
|
-
try {
|
|
1462
|
-
const toolsDir = path7.join(process.cwd(), "tools");
|
|
1463
|
-
const crateDir = path7.join(toolsDir, "syn-parser");
|
|
1464
|
-
const tmpFile = path7.join(crateDir, "src", "input.rs");
|
|
1465
|
-
await fs5.writeFile(tmpFile, content, "utf8");
|
|
1466
|
-
const cargoBinary = resolveWin32Command("cargo");
|
|
1467
|
-
const result = await new Promise(
|
|
1468
|
-
(resolve4, reject) => {
|
|
1469
|
-
let settled = false;
|
|
1470
|
-
const proc = spawn3(
|
|
1471
|
-
cargoBinary,
|
|
1472
|
-
["run", "--manifest-path", path7.join(toolsDir, "Cargo.toml")],
|
|
1473
|
-
{
|
|
1474
|
-
cwd: process.cwd(),
|
|
1475
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1476
|
-
windowsHide: true
|
|
1477
|
-
}
|
|
1478
|
-
);
|
|
1479
|
-
proc.on("error", (err) => {
|
|
1480
|
-
if (settled) return;
|
|
1481
|
-
settled = true;
|
|
1482
|
-
reject(err);
|
|
1483
|
-
});
|
|
1484
|
-
let stdout2 = "";
|
|
1485
|
-
proc.stdout?.on("data", (chunk) => {
|
|
1486
|
-
stdout2 += chunk.toString();
|
|
1487
|
-
});
|
|
1488
|
-
proc.stderr?.resume();
|
|
1489
|
-
const timer = setTimeout(() => {
|
|
1490
|
-
if (settled) return;
|
|
1491
|
-
settled = true;
|
|
1492
|
-
proc.kill("SIGKILL");
|
|
1493
|
-
reject(new Error("timeout"));
|
|
1494
|
-
}, 15e3);
|
|
1495
|
-
timer.unref?.();
|
|
1496
|
-
proc.on("close", (c) => {
|
|
1497
|
-
if (settled) return;
|
|
1498
|
-
settled = true;
|
|
1499
|
-
clearTimeout(timer);
|
|
1500
|
-
resolve4({ code: c, stdout: stdout2 });
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
);
|
|
1504
|
-
const { code, stdout } = result;
|
|
1505
|
-
if (code === 0 && stdout.trim()) {
|
|
1506
|
-
const symbols = JSON.parse(stdout.trim());
|
|
1507
|
-
return {
|
|
1508
|
-
file,
|
|
1509
|
-
lang: "rs",
|
|
1510
|
-
symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
|
|
1511
|
-
mtimeMs: Date.now()
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
} catch {
|
|
1515
|
-
}
|
|
1516
|
-
return null;
|
|
1517
|
-
}
|
|
1518
1672
|
function regexParse(opts) {
|
|
1519
1673
|
const { file, content, lang } = opts;
|
|
1520
1674
|
const symbols = [];
|
|
@@ -1570,12 +1724,10 @@ function regexParse(opts) {
|
|
|
1570
1724
|
});
|
|
1571
1725
|
return { file, lang, symbols: deduped, mtimeMs: Date.now() };
|
|
1572
1726
|
}
|
|
1573
|
-
var
|
|
1727
|
+
var RS_PATTERNS;
|
|
1574
1728
|
var init_rs_parser = __esm({
|
|
1575
1729
|
"src/codebase-index/rs-parser.ts"() {
|
|
1576
1730
|
"use strict";
|
|
1577
|
-
init_win32_resolve();
|
|
1578
|
-
init_spawn_gate();
|
|
1579
1731
|
init_languages();
|
|
1580
1732
|
RS_PATTERNS = [
|
|
1581
1733
|
{ regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
|
|
@@ -1598,7 +1750,7 @@ __export(json_parser_exports, {
|
|
|
1598
1750
|
parseSymbols: () => parseSymbols6
|
|
1599
1751
|
});
|
|
1600
1752
|
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
1601
|
-
import * as
|
|
1753
|
+
import * as path9 from "node:path";
|
|
1602
1754
|
function parseSymbols6(opts) {
|
|
1603
1755
|
const { file, content, lang } = opts;
|
|
1604
1756
|
try {
|
|
@@ -1610,7 +1762,7 @@ function parseSymbols6(opts) {
|
|
|
1610
1762
|
function regexParse2(opts) {
|
|
1611
1763
|
const { file, content, lang } = opts;
|
|
1612
1764
|
const symbols = [];
|
|
1613
|
-
const basename6 =
|
|
1765
|
+
const basename6 = path9.basename(file).toLowerCase();
|
|
1614
1766
|
const isPackageJson = basename6 === "package.json";
|
|
1615
1767
|
const isTsconfig = basename6 === "tsconfig.json" || basename6 === "tsconfig.build.json";
|
|
1616
1768
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
@@ -1636,11 +1788,11 @@ function regexParse2(opts) {
|
|
|
1636
1788
|
const line = lineFromOffset(offset);
|
|
1637
1789
|
symbols.push(
|
|
1638
1790
|
makeSymbol({
|
|
1639
|
-
name:
|
|
1791
|
+
name: path9.basename(file),
|
|
1640
1792
|
kind: "object",
|
|
1641
1793
|
line,
|
|
1642
1794
|
col: 0,
|
|
1643
|
-
signature: `"${
|
|
1795
|
+
signature: `"${path9.basename(file)}" = { ... }`,
|
|
1644
1796
|
file,
|
|
1645
1797
|
lang
|
|
1646
1798
|
})
|
|
@@ -2006,18 +2158,18 @@ async function resolveRealInsideRoot(absPath, ctx) {
|
|
|
2006
2158
|
const realRoots = await Promise.all(
|
|
2007
2159
|
allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
|
|
2008
2160
|
);
|
|
2009
|
-
let
|
|
2161
|
+
let probe = absPath;
|
|
2010
2162
|
const pendingTail = [];
|
|
2011
2163
|
for (; ; ) {
|
|
2012
2164
|
let real;
|
|
2013
2165
|
try {
|
|
2014
|
-
real = await fsp.realpath(
|
|
2166
|
+
real = await fsp.realpath(probe);
|
|
2015
2167
|
} catch (err) {
|
|
2016
2168
|
if (err.code === "ENOENT") {
|
|
2017
|
-
const parent = path.dirname(
|
|
2018
|
-
if (parent ===
|
|
2019
|
-
pendingTail.unshift(path.basename(
|
|
2020
|
-
|
|
2169
|
+
const parent = path.dirname(probe);
|
|
2170
|
+
if (parent === probe) return absPath;
|
|
2171
|
+
pendingTail.unshift(path.basename(probe));
|
|
2172
|
+
probe = parent;
|
|
2021
2173
|
continue;
|
|
2022
2174
|
}
|
|
2023
2175
|
throw err;
|
|
@@ -2126,7 +2278,7 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
|
2126
2278
|
|
|
2127
2279
|
// src/codebase-index/indexer.ts
|
|
2128
2280
|
import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
|
|
2129
|
-
import { execFile
|
|
2281
|
+
import { execFile } from "node:child_process";
|
|
2130
2282
|
import * as fs8 from "node:fs/promises";
|
|
2131
2283
|
import { availableParallelism } from "node:os";
|
|
2132
2284
|
import * as path12 from "node:path";
|
|
@@ -2194,8 +2346,738 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
2194
2346
|
// src/codebase-index/indexer.ts
|
|
2195
2347
|
init_languages();
|
|
2196
2348
|
|
|
2349
|
+
// src/codebase-index/module-resolver.ts
|
|
2350
|
+
init_languages();
|
|
2351
|
+
import * as path5 from "node:path";
|
|
2352
|
+
|
|
2353
|
+
// src/codebase-index/module-roots.ts
|
|
2354
|
+
init_languages();
|
|
2355
|
+
import * as fs2 from "node:fs/promises";
|
|
2356
|
+
import * as path4 from "node:path";
|
|
2357
|
+
function toPortablePath(file) {
|
|
2358
|
+
return file.replace(/\\/g, "/");
|
|
2359
|
+
}
|
|
2360
|
+
async function readTextIfPresent(file) {
|
|
2361
|
+
try {
|
|
2362
|
+
return await fs2.readFile(file, "utf8");
|
|
2363
|
+
} catch {
|
|
2364
|
+
return void 0;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
function parsePackageJsonName(source) {
|
|
2368
|
+
try {
|
|
2369
|
+
const parsed = JSON.parse(source);
|
|
2370
|
+
return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
|
|
2371
|
+
} catch {
|
|
2372
|
+
return void 0;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
function parseGoModulePath(source) {
|
|
2376
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
2377
|
+
const line = rawLine.replace(/\/\/.*$/, "").trim();
|
|
2378
|
+
const match = /^module\s+(\S+)/.exec(line);
|
|
2379
|
+
if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
|
|
2380
|
+
}
|
|
2381
|
+
return void 0;
|
|
2382
|
+
}
|
|
2383
|
+
function parseTomlTableName(source, tables) {
|
|
2384
|
+
let current = "";
|
|
2385
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
2386
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
2387
|
+
if (line.startsWith("[[")) {
|
|
2388
|
+
current = "\0";
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
const table = /^\[([^\]]+)\]$/.exec(line);
|
|
2392
|
+
if (table?.[1]) {
|
|
2393
|
+
current = table[1].trim();
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
if (!tables.includes(current)) continue;
|
|
2397
|
+
const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
|
|
2398
|
+
if (match?.[1]) return match[1];
|
|
2399
|
+
}
|
|
2400
|
+
return void 0;
|
|
2401
|
+
}
|
|
2402
|
+
function parsePomArtifactId(source) {
|
|
2403
|
+
const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
2404
|
+
return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
|
|
2405
|
+
}
|
|
2406
|
+
var LANGS_BY_KIND = {
|
|
2407
|
+
npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
|
|
2408
|
+
cargo: ["rs"],
|
|
2409
|
+
go: ["go"],
|
|
2410
|
+
python: ["py"],
|
|
2411
|
+
maven: ["java", "kotlin", "scala"],
|
|
2412
|
+
gradle: ["java", "kotlin", "scala"],
|
|
2413
|
+
dotnet: ["csharp"]
|
|
2414
|
+
};
|
|
2415
|
+
function ancestorsOf(dir, stopAt) {
|
|
2416
|
+
const out = [];
|
|
2417
|
+
let current = dir;
|
|
2418
|
+
for (; ; ) {
|
|
2419
|
+
out.push(current);
|
|
2420
|
+
if (current === stopAt || current.length <= stopAt.length) break;
|
|
2421
|
+
const parent = path4.posix.dirname(current);
|
|
2422
|
+
if (parent === current) break;
|
|
2423
|
+
current = parent;
|
|
2424
|
+
}
|
|
2425
|
+
return out;
|
|
2426
|
+
}
|
|
2427
|
+
var MARKER_PROBES = [
|
|
2428
|
+
{
|
|
2429
|
+
kind: "npm",
|
|
2430
|
+
file: "package.json",
|
|
2431
|
+
build: (dir, source) => {
|
|
2432
|
+
const name = parsePackageJsonName(source) ?? path4.posix.basename(dir);
|
|
2433
|
+
return { name, importPath: name, sourceRoots: [dir] };
|
|
2434
|
+
}
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
kind: "cargo",
|
|
2438
|
+
file: "Cargo.toml",
|
|
2439
|
+
build: (dir, source) => {
|
|
2440
|
+
const name = parseTomlTableName(source, ["package"]);
|
|
2441
|
+
if (!name) return void 0;
|
|
2442
|
+
return {
|
|
2443
|
+
name: `crate:${name}`,
|
|
2444
|
+
// Rust paths use underscores where crate names often use dashes.
|
|
2445
|
+
importPath: name.replace(/-/g, "_"),
|
|
2446
|
+
sourceRoots: [path4.posix.join(dir, "src")]
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
kind: "go",
|
|
2452
|
+
file: "go.mod",
|
|
2453
|
+
build: (dir, source) => {
|
|
2454
|
+
const modulePath = parseGoModulePath(source);
|
|
2455
|
+
if (!modulePath) return void 0;
|
|
2456
|
+
return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
|
|
2457
|
+
}
|
|
2458
|
+
},
|
|
2459
|
+
{
|
|
2460
|
+
kind: "python",
|
|
2461
|
+
file: "pyproject.toml",
|
|
2462
|
+
build: (dir, source) => {
|
|
2463
|
+
const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path4.posix.basename(dir);
|
|
2464
|
+
return {
|
|
2465
|
+
name: `py:${name}`,
|
|
2466
|
+
importPath: void 0,
|
|
2467
|
+
// `src/` layout is the packaging-guide default; the root itself covers
|
|
2468
|
+
// the flat layout. Both are probed, missing ones simply never match.
|
|
2469
|
+
sourceRoots: [path4.posix.join(dir, "src"), dir]
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
kind: "python",
|
|
2475
|
+
file: "setup.py",
|
|
2476
|
+
build: (dir) => ({
|
|
2477
|
+
name: `py:${path4.posix.basename(dir)}`,
|
|
2478
|
+
importPath: void 0,
|
|
2479
|
+
sourceRoots: [path4.posix.join(dir, "src"), dir]
|
|
2480
|
+
})
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
kind: "maven",
|
|
2484
|
+
file: "pom.xml",
|
|
2485
|
+
build: (dir, source) => {
|
|
2486
|
+
const artifactId = parsePomArtifactId(source) ?? path4.posix.basename(dir);
|
|
2487
|
+
return {
|
|
2488
|
+
name: `mvn:${artifactId}`,
|
|
2489
|
+
importPath: void 0,
|
|
2490
|
+
sourceRoots: [
|
|
2491
|
+
path4.posix.join(dir, "src/main/java"),
|
|
2492
|
+
path4.posix.join(dir, "src/main/kotlin"),
|
|
2493
|
+
path4.posix.join(dir, "src/main/scala"),
|
|
2494
|
+
path4.posix.join(dir, "src/test/java")
|
|
2495
|
+
]
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
kind: "gradle",
|
|
2501
|
+
file: "build.gradle",
|
|
2502
|
+
build: (dir) => buildGradleRoot(dir)
|
|
2503
|
+
},
|
|
2504
|
+
{
|
|
2505
|
+
kind: "gradle",
|
|
2506
|
+
file: "build.gradle.kts",
|
|
2507
|
+
build: (dir) => buildGradleRoot(dir)
|
|
2508
|
+
}
|
|
2509
|
+
];
|
|
2510
|
+
function buildGradleRoot(dir) {
|
|
2511
|
+
return {
|
|
2512
|
+
name: `gradle:${path4.posix.basename(dir)}`,
|
|
2513
|
+
importPath: void 0,
|
|
2514
|
+
sourceRoots: [
|
|
2515
|
+
path4.posix.join(dir, "src/main/java"),
|
|
2516
|
+
path4.posix.join(dir, "src/main/kotlin"),
|
|
2517
|
+
path4.posix.join(dir, "src/main/scala")
|
|
2518
|
+
]
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
async function probeDotnetRoot(dir) {
|
|
2522
|
+
let entries;
|
|
2523
|
+
try {
|
|
2524
|
+
entries = await fs2.readdir(dir);
|
|
2525
|
+
} catch {
|
|
2526
|
+
return void 0;
|
|
2527
|
+
}
|
|
2528
|
+
const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
|
|
2529
|
+
if (!project) return void 0;
|
|
2530
|
+
const name = project.slice(0, -".csproj".length);
|
|
2531
|
+
return {
|
|
2532
|
+
dir,
|
|
2533
|
+
kind: "dotnet",
|
|
2534
|
+
name: `csproj:${name}`,
|
|
2535
|
+
importPath: void 0,
|
|
2536
|
+
sourceRoots: [dir]
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
async function detectModuleRoots(projectRoot, files) {
|
|
2540
|
+
const root = toPortablePath(projectRoot).replace(/\/+$/, "");
|
|
2541
|
+
const langsByDir = /* @__PURE__ */ new Map();
|
|
2542
|
+
for (const file of files) {
|
|
2543
|
+
const portable = toPortablePath(file);
|
|
2544
|
+
const lang = detectLang(portable);
|
|
2545
|
+
if (!lang) continue;
|
|
2546
|
+
const dir = path4.posix.dirname(portable);
|
|
2547
|
+
let langs = langsByDir.get(dir);
|
|
2548
|
+
if (!langs) {
|
|
2549
|
+
langs = /* @__PURE__ */ new Set();
|
|
2550
|
+
langsByDir.set(dir, langs);
|
|
2551
|
+
}
|
|
2552
|
+
langs.add(lang);
|
|
2553
|
+
}
|
|
2554
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2555
|
+
for (const [dir, langs] of langsByDir) {
|
|
2556
|
+
for (const ancestor of ancestorsOf(dir, root)) {
|
|
2557
|
+
let merged = candidates.get(ancestor);
|
|
2558
|
+
if (!merged) {
|
|
2559
|
+
merged = /* @__PURE__ */ new Set();
|
|
2560
|
+
candidates.set(ancestor, merged);
|
|
2561
|
+
}
|
|
2562
|
+
for (const lang of langs) merged.add(lang);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
const roots = [];
|
|
2566
|
+
await Promise.all(
|
|
2567
|
+
[...candidates].map(async ([dir, langs]) => {
|
|
2568
|
+
for (const probe of MARKER_PROBES) {
|
|
2569
|
+
if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
|
|
2570
|
+
const source = await readTextIfPresent(path4.posix.join(dir, probe.file));
|
|
2571
|
+
if (source === void 0) continue;
|
|
2572
|
+
const built = probe.build(dir, source);
|
|
2573
|
+
if (built) roots.push({ dir, kind: probe.kind, ...built });
|
|
2574
|
+
}
|
|
2575
|
+
if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
|
|
2576
|
+
const dotnet = await probeDotnetRoot(dir);
|
|
2577
|
+
if (dotnet) roots.push(dotnet);
|
|
2578
|
+
}
|
|
2579
|
+
})
|
|
2580
|
+
);
|
|
2581
|
+
roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
|
|
2582
|
+
return { projectRoot: root, roots };
|
|
2583
|
+
}
|
|
2584
|
+
function findOwningRoot(structure, file, kinds) {
|
|
2585
|
+
const portable = toPortablePath(file);
|
|
2586
|
+
for (const root of structure.roots) {
|
|
2587
|
+
if (kinds && !kinds.includes(root.kind)) continue;
|
|
2588
|
+
if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
|
|
2589
|
+
}
|
|
2590
|
+
return void 0;
|
|
2591
|
+
}
|
|
2592
|
+
function derivePackageFromLayout(filePath) {
|
|
2593
|
+
const portable = toPortablePath(filePath);
|
|
2594
|
+
const packagesIdx = portable.indexOf("/packages/");
|
|
2595
|
+
if (packagesIdx !== -1) {
|
|
2596
|
+
const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
|
|
2597
|
+
if (segment) return `@wrongstack/${segment}`;
|
|
2598
|
+
}
|
|
2599
|
+
const appsIdx = portable.indexOf("/apps/");
|
|
2600
|
+
if (appsIdx !== -1) {
|
|
2601
|
+
const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
|
|
2602
|
+
if (segment) return `app:${segment}`;
|
|
2603
|
+
}
|
|
2604
|
+
return void 0;
|
|
2605
|
+
}
|
|
2606
|
+
function pythonPackageLabel(structure, file, initDirs) {
|
|
2607
|
+
const portable = toPortablePath(file);
|
|
2608
|
+
const dir = path4.posix.dirname(portable);
|
|
2609
|
+
if (!initDirs.has(dir)) return void 0;
|
|
2610
|
+
const segments = [];
|
|
2611
|
+
let current = dir;
|
|
2612
|
+
while (initDirs.has(current) && current.length > structure.projectRoot.length) {
|
|
2613
|
+
segments.unshift(path4.posix.basename(current));
|
|
2614
|
+
current = path4.posix.dirname(current);
|
|
2615
|
+
}
|
|
2616
|
+
return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
|
|
2617
|
+
}
|
|
2618
|
+
function assignPackageLabels(structure, files) {
|
|
2619
|
+
const initDirs = /* @__PURE__ */ new Set();
|
|
2620
|
+
for (const file of files) {
|
|
2621
|
+
const portable = toPortablePath(file);
|
|
2622
|
+
if (path4.posix.basename(portable) === "__init__.py") {
|
|
2623
|
+
initDirs.add(path4.posix.dirname(portable));
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
const labels = /* @__PURE__ */ new Map();
|
|
2627
|
+
for (const file of files) {
|
|
2628
|
+
const portable = toPortablePath(file);
|
|
2629
|
+
const lang = detectLang(portable);
|
|
2630
|
+
if (lang === "go") {
|
|
2631
|
+
const owner2 = findOwningRoot(structure, portable, ["go"]);
|
|
2632
|
+
const dir = path4.posix.dirname(portable);
|
|
2633
|
+
if (owner2?.importPath) {
|
|
2634
|
+
const relative3 = path4.posix.relative(owner2.dir, dir);
|
|
2635
|
+
labels.set(file, relative3 ? `${owner2.importPath}/${relative3}` : owner2.importPath);
|
|
2636
|
+
} else {
|
|
2637
|
+
labels.set(file, `go:${path4.posix.relative(structure.projectRoot, dir) || "."}`);
|
|
2638
|
+
}
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
if (lang === "py") {
|
|
2642
|
+
const dotted = pythonPackageLabel(structure, portable, initDirs);
|
|
2643
|
+
if (dotted) {
|
|
2644
|
+
labels.set(file, dotted);
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
const owner = findOwningRoot(structure, portable);
|
|
2649
|
+
const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
|
|
2650
|
+
labels.set(file, label);
|
|
2651
|
+
}
|
|
2652
|
+
return labels;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/codebase-index/module-resolver.ts
|
|
2656
|
+
var EXTENSIONS = {
|
|
2657
|
+
js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
|
|
2658
|
+
py: [".py", ".pyi"],
|
|
2659
|
+
rs: [".rs"],
|
|
2660
|
+
jvm: [".java", ".kt", ".scala"],
|
|
2661
|
+
c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
|
|
2662
|
+
ruby: [".rb"],
|
|
2663
|
+
go: [".go"]
|
|
2664
|
+
};
|
|
2665
|
+
var DIRECTORY_ENTRIES = {
|
|
2666
|
+
js: ["index"],
|
|
2667
|
+
py: ["__init__"],
|
|
2668
|
+
rs: ["mod"],
|
|
2669
|
+
ruby: ["index"]
|
|
2670
|
+
};
|
|
2671
|
+
function normalizeNamespace(value) {
|
|
2672
|
+
return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
|
|
2673
|
+
}
|
|
2674
|
+
var ModuleResolver = class {
|
|
2675
|
+
structure;
|
|
2676
|
+
/** Lowercased portable path → the path as indexed (case is preserved). */
|
|
2677
|
+
byPath;
|
|
2678
|
+
/** Lowercased portable directory → files directly inside it, as indexed. */
|
|
2679
|
+
byDir;
|
|
2680
|
+
/** Normalized namespace → the file declaring it (first by path, stable). */
|
|
2681
|
+
byNamespace;
|
|
2682
|
+
constructor(structure, files, namespaces = []) {
|
|
2683
|
+
this.structure = structure;
|
|
2684
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
2685
|
+
this.byDir = /* @__PURE__ */ new Map();
|
|
2686
|
+
this.byNamespace = /* @__PURE__ */ new Map();
|
|
2687
|
+
const dirsByKey = /* @__PURE__ */ new Map();
|
|
2688
|
+
for (const file of files) {
|
|
2689
|
+
const portable = toPortablePath(file);
|
|
2690
|
+
const pathKey = portable.toLowerCase();
|
|
2691
|
+
const priorPath = this.byPath.get(pathKey);
|
|
2692
|
+
if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
|
|
2693
|
+
else this.byPath.set(pathKey, file);
|
|
2694
|
+
const dir = path5.posix.dirname(portable);
|
|
2695
|
+
const dirKey = dir.toLowerCase();
|
|
2696
|
+
const knownDir = dirsByKey.get(dirKey);
|
|
2697
|
+
if (knownDir === void 0) {
|
|
2698
|
+
dirsByKey.set(dirKey, dir);
|
|
2699
|
+
this.byDir.set(dirKey, [file]);
|
|
2700
|
+
} else if (knownDir === dir) {
|
|
2701
|
+
this.byDir.get(dirKey)?.push(file);
|
|
2702
|
+
} else {
|
|
2703
|
+
dirsByKey.delete(dirKey);
|
|
2704
|
+
this.byDir.delete(dirKey);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
for (const { name, file } of namespaces) {
|
|
2708
|
+
const lang = detectLang(file);
|
|
2709
|
+
if (!lang) continue;
|
|
2710
|
+
const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
|
|
2711
|
+
if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
|
|
2712
|
+
this.byNamespace.set(key, file);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Resolve `specifier` as written in `fromFile`.
|
|
2718
|
+
* Returns the indexed target path, or `undefined` when it is external or
|
|
2719
|
+
* cannot be located.
|
|
2720
|
+
*/
|
|
2721
|
+
resolve(fromFile, lang, specifier) {
|
|
2722
|
+
const spec = specifier.trim().replace(/\\/g, "/");
|
|
2723
|
+
if (!spec) return void 0;
|
|
2724
|
+
const from = toPortablePath(fromFile);
|
|
2725
|
+
switch (languageFamily(lang)) {
|
|
2726
|
+
case "js":
|
|
2727
|
+
return this.resolveJs(from, spec);
|
|
2728
|
+
case "go":
|
|
2729
|
+
return this.resolveGo(spec);
|
|
2730
|
+
case "py":
|
|
2731
|
+
return this.resolvePython(from, spec);
|
|
2732
|
+
case "rs":
|
|
2733
|
+
return this.resolveRust(from, spec);
|
|
2734
|
+
case "jvm":
|
|
2735
|
+
return this.resolveJvm(spec);
|
|
2736
|
+
case "c":
|
|
2737
|
+
return this.resolveInclude(from, spec);
|
|
2738
|
+
case "ruby":
|
|
2739
|
+
return this.resolveRuby(from, spec);
|
|
2740
|
+
case "dotnet":
|
|
2741
|
+
case "php":
|
|
2742
|
+
case "elixir":
|
|
2743
|
+
case "haskell":
|
|
2744
|
+
return this.resolveNamespace(lang, spec);
|
|
2745
|
+
default:
|
|
2746
|
+
return void 0;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Resolve a namespace specifier to the file declaring it.
|
|
2751
|
+
*
|
|
2752
|
+
* Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
|
|
2753
|
+
* names a namespace outright, while PHP's `use App\Models\User` names a
|
|
2754
|
+
* *class* inside `App\Models`, so the prefix is what was declared.
|
|
2755
|
+
*/
|
|
2756
|
+
resolveNamespace(lang, spec) {
|
|
2757
|
+
const family = languageFamily(lang);
|
|
2758
|
+
const normalized = normalizeNamespace(spec);
|
|
2759
|
+
const exact = this.byNamespace.get(`${family}:${normalized}`);
|
|
2760
|
+
if (exact) return exact;
|
|
2761
|
+
const segments = normalized.split(".").filter(Boolean);
|
|
2762
|
+
if (segments.length < 2) return void 0;
|
|
2763
|
+
return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
|
|
2764
|
+
}
|
|
2765
|
+
// ─── Lookup primitives ──────────────────────────────────────────────────────
|
|
2766
|
+
lookup(candidate) {
|
|
2767
|
+
return this.byPath.get(path5.posix.normalize(candidate).toLowerCase());
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Try `base` verbatim, then `base` + each extension, then each directory
|
|
2771
|
+
* entry point inside `base`.
|
|
2772
|
+
*/
|
|
2773
|
+
lookupWithExtensions(base, family) {
|
|
2774
|
+
const direct = this.lookup(base);
|
|
2775
|
+
if (direct) return direct;
|
|
2776
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
2777
|
+
const suffix = path5.posix.extname(base);
|
|
2778
|
+
const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
|
|
2779
|
+
for (const ext of extensions) {
|
|
2780
|
+
const hit = this.lookup(`${stem}${ext}`);
|
|
2781
|
+
if (hit) return hit;
|
|
2782
|
+
}
|
|
2783
|
+
for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
|
|
2784
|
+
for (const ext of extensions) {
|
|
2785
|
+
const hit = this.lookup(path5.posix.join(base, `${entry}${ext}`));
|
|
2786
|
+
if (hit) return hit;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
return void 0;
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* A representative indexed file inside `dir`, for ecosystems whose import
|
|
2793
|
+
* unit is a directory rather than a file (Go packages, JVM wildcard imports).
|
|
2794
|
+
*
|
|
2795
|
+
* The choice is deterministic — a file named after the directory, else the
|
|
2796
|
+
* first by name — so the same import always produces the same edge. Package
|
|
2797
|
+
* grouping is unaffected either way: every file in the directory carries the
|
|
2798
|
+
* same package label, so the package-level edge is exact regardless of which
|
|
2799
|
+
* member represents it.
|
|
2800
|
+
*/
|
|
2801
|
+
representativeIn(dir, family) {
|
|
2802
|
+
const members = this.byDir.get(path5.posix.normalize(dir).toLowerCase());
|
|
2803
|
+
if (!members?.length) return void 0;
|
|
2804
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
2805
|
+
const eligible = members.filter((file) => extensions.includes(path5.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
|
|
2806
|
+
if (eligible.length === 0) return void 0;
|
|
2807
|
+
const base = path5.posix.basename(path5.posix.normalize(dir)).toLowerCase();
|
|
2808
|
+
const named = eligible.find(
|
|
2809
|
+
(file) => path5.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
|
|
2810
|
+
);
|
|
2811
|
+
return named ?? eligible[0];
|
|
2812
|
+
}
|
|
2813
|
+
// ─── Per-family resolution ──────────────────────────────────────────────────
|
|
2814
|
+
/** Relative specifiers, then workspace package names and their subpaths. */
|
|
2815
|
+
resolveJs(fromFile, spec) {
|
|
2816
|
+
if (spec.startsWith(".")) {
|
|
2817
|
+
const absolute = path5.posix.join(path5.posix.dirname(fromFile), spec);
|
|
2818
|
+
return this.lookupWithExtensions(absolute, "js");
|
|
2819
|
+
}
|
|
2820
|
+
const owner = this.structure.roots.find(
|
|
2821
|
+
(root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
2822
|
+
);
|
|
2823
|
+
if (!owner?.importPath) return void 0;
|
|
2824
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
2825
|
+
if (!subpath) {
|
|
2826
|
+
return this.lookupWithExtensions(path5.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path5.posix.join(owner.dir, "index"), "js");
|
|
2827
|
+
}
|
|
2828
|
+
return this.lookupWithExtensions(path5.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path5.posix.join(owner.dir, "src", subpath), "js");
|
|
2829
|
+
}
|
|
2830
|
+
/** Go import paths are absolute module paths; a package is a directory. */
|
|
2831
|
+
resolveGo(spec) {
|
|
2832
|
+
const owner = this.structure.roots.find(
|
|
2833
|
+
(root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
2834
|
+
);
|
|
2835
|
+
if (!owner?.importPath) return void 0;
|
|
2836
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
2837
|
+
return this.representativeIn(path5.posix.join(owner.dir, subpath), "go");
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
|
|
2841
|
+
* leading dots the extractor preserves (`.sibling`, `..parent.mod`).
|
|
2842
|
+
*/
|
|
2843
|
+
resolvePython(fromFile, spec) {
|
|
2844
|
+
const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
|
|
2845
|
+
if (leadingDots > 0) {
|
|
2846
|
+
let base = path5.posix.dirname(fromFile);
|
|
2847
|
+
for (let i = 1; i < leadingDots; i++) base = path5.posix.dirname(base);
|
|
2848
|
+
const rest = spec.slice(leadingDots).split(".").filter(Boolean);
|
|
2849
|
+
return this.lookupWithExtensions(path5.posix.join(base, ...rest), "py");
|
|
2850
|
+
}
|
|
2851
|
+
const segments = spec.split(".").filter(Boolean);
|
|
2852
|
+
if (segments.length === 0) return void 0;
|
|
2853
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
|
|
2854
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
2855
|
+
const hit = this.lookupWithExtensions(path5.posix.join(base, ...segments), "py");
|
|
2856
|
+
if (hit) return hit;
|
|
2857
|
+
if (segments.length > 1) {
|
|
2858
|
+
const parent = this.lookupWithExtensions(
|
|
2859
|
+
path5.posix.join(base, ...segments.slice(0, -1)),
|
|
2860
|
+
"py"
|
|
2861
|
+
);
|
|
2862
|
+
if (parent) return parent;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return void 0;
|
|
2866
|
+
}
|
|
2867
|
+
/** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
|
|
2868
|
+
resolveRust(fromFile, spec) {
|
|
2869
|
+
const segments = spec.split("::").filter(Boolean);
|
|
2870
|
+
if (segments.length === 0) return void 0;
|
|
2871
|
+
const head = segments[0];
|
|
2872
|
+
if (head === "self" || head === "super") {
|
|
2873
|
+
let base = path5.posix.dirname(fromFile);
|
|
2874
|
+
for (const segment of segments) {
|
|
2875
|
+
if (segment === "super") base = path5.posix.dirname(base);
|
|
2876
|
+
else if (segment !== "self") break;
|
|
2877
|
+
}
|
|
2878
|
+
const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
|
|
2879
|
+
return this.lookupWithExtensions(path5.posix.join(base, ...rest2), "rs");
|
|
2880
|
+
}
|
|
2881
|
+
const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
|
|
2882
|
+
const crate = head === "crate" ? owningCrate : this.structure.roots.find(
|
|
2883
|
+
(root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
|
|
2884
|
+
);
|
|
2885
|
+
if (!crate) {
|
|
2886
|
+
return this.lookupWithExtensions(
|
|
2887
|
+
path5.posix.join(path5.posix.dirname(fromFile), ...segments),
|
|
2888
|
+
"rs"
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
const rest = segments.slice(1);
|
|
2892
|
+
for (const base of crate.sourceRoots) {
|
|
2893
|
+
const parent = rest.length > 1 ? this.lookupWithExtensions(path5.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
|
|
2894
|
+
const exact = this.lookupWithExtensions(path5.posix.join(base, ...rest), "rs");
|
|
2895
|
+
const hit = exact ?? parent ?? this.lookupWithExtensions(path5.posix.join(base, "lib"), "rs");
|
|
2896
|
+
if (hit) return hit;
|
|
2897
|
+
}
|
|
2898
|
+
return void 0;
|
|
2899
|
+
}
|
|
2900
|
+
/** `com.example.Thing` and `com.example.*` against JVM source roots. */
|
|
2901
|
+
resolveJvm(spec) {
|
|
2902
|
+
const segments = spec.split(".").filter(Boolean);
|
|
2903
|
+
if (segments.length === 0) return void 0;
|
|
2904
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
|
|
2905
|
+
const wildcard = segments[segments.length - 1] === "*";
|
|
2906
|
+
const parts = wildcard ? segments.slice(0, -1) : segments;
|
|
2907
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
2908
|
+
const target = path5.posix.join(base, ...parts);
|
|
2909
|
+
const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
|
|
2910
|
+
if (hit) return hit;
|
|
2911
|
+
}
|
|
2912
|
+
return void 0;
|
|
2913
|
+
}
|
|
2914
|
+
/** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
|
|
2915
|
+
resolveInclude(fromFile, spec) {
|
|
2916
|
+
const relative3 = this.lookupWithExtensions(
|
|
2917
|
+
path5.posix.join(path5.posix.dirname(fromFile), spec),
|
|
2918
|
+
"c"
|
|
2919
|
+
);
|
|
2920
|
+
if (relative3) return relative3;
|
|
2921
|
+
for (const base of [
|
|
2922
|
+
path5.posix.join(this.structure.projectRoot, "include"),
|
|
2923
|
+
this.structure.projectRoot
|
|
2924
|
+
]) {
|
|
2925
|
+
const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "c");
|
|
2926
|
+
if (hit) return hit;
|
|
2927
|
+
}
|
|
2928
|
+
return void 0;
|
|
2929
|
+
}
|
|
2930
|
+
/** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
|
|
2931
|
+
resolveRuby(fromFile, spec) {
|
|
2932
|
+
const relative3 = this.lookupWithExtensions(
|
|
2933
|
+
path5.posix.join(path5.posix.dirname(fromFile), spec),
|
|
2934
|
+
"ruby"
|
|
2935
|
+
);
|
|
2936
|
+
if (relative3) return relative3;
|
|
2937
|
+
for (const base of [
|
|
2938
|
+
path5.posix.join(this.structure.projectRoot, "lib"),
|
|
2939
|
+
this.structure.projectRoot
|
|
2940
|
+
]) {
|
|
2941
|
+
const hit = this.lookupWithExtensions(path5.posix.join(base, spec), "ruby");
|
|
2942
|
+
if (hit) return hit;
|
|
2943
|
+
}
|
|
2944
|
+
return void 0;
|
|
2945
|
+
}
|
|
2946
|
+
};
|
|
2947
|
+
|
|
2948
|
+
// src/codebase-index/import-extractor.ts
|
|
2949
|
+
var IMPORT_MAX_FILE_CHARS = 512 * 1024;
|
|
2950
|
+
var IMPORT_MAX_PER_FILE = 400;
|
|
2951
|
+
var DOTTED_IMPORT = [
|
|
2952
|
+
{ re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
|
|
2953
|
+
];
|
|
2954
|
+
var LANG_IMPORTS = {
|
|
2955
|
+
// Go and Python have real AST extractors; these patterns are the fallback for
|
|
2956
|
+
// machines with no Go toolchain or Python interpreter installed, where the
|
|
2957
|
+
// parser degrades to regex symbols and would otherwise contribute no edges.
|
|
2958
|
+
go: [
|
|
2959
|
+
{ re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
|
|
2960
|
+
// Grouped form: inside `import ( … )` each line is an optional alias plus a
|
|
2961
|
+
// quoted path. A stray match elsewhere resolves to no file and is dropped.
|
|
2962
|
+
{ re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
|
|
2963
|
+
],
|
|
2964
|
+
py: [
|
|
2965
|
+
{ re: /^[ \t]*import\s+([\w.]+)/gm },
|
|
2966
|
+
{ re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
|
|
2967
|
+
],
|
|
2968
|
+
rs: [
|
|
2969
|
+
// use a::b::C; | use a::b::{C, D}; → the path before any brace
|
|
2970
|
+
{ re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
|
|
2971
|
+
// mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
|
|
2972
|
+
{ re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
|
|
2973
|
+
],
|
|
2974
|
+
java: DOTTED_IMPORT,
|
|
2975
|
+
kotlin: DOTTED_IMPORT,
|
|
2976
|
+
scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
|
|
2977
|
+
csharp: [
|
|
2978
|
+
// using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
|
|
2979
|
+
{ re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
|
|
2980
|
+
{ re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
|
|
2981
|
+
],
|
|
2982
|
+
// Quoted includes only: <stdio.h> is a system header with no indexed file.
|
|
2983
|
+
c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
|
|
2984
|
+
cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
|
|
2985
|
+
ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
|
|
2986
|
+
php: [
|
|
2987
|
+
// `use A\B\C` imports the class C, which is what the index has a symbol
|
|
2988
|
+
// for — the namespace symbol only covers the `A\B` prefix.
|
|
2989
|
+
{ re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
|
|
2990
|
+
{ re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
|
|
2991
|
+
],
|
|
2992
|
+
swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
|
|
2993
|
+
dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
|
|
2994
|
+
lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
|
|
2995
|
+
elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
|
|
2996
|
+
haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
|
|
2997
|
+
zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
|
|
2998
|
+
proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
|
|
2999
|
+
// `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
|
|
3000
|
+
css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
|
|
3001
|
+
// A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
|
|
3002
|
+
vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
|
|
3003
|
+
svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
|
|
3004
|
+
html: [
|
|
3005
|
+
{ re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
|
|
3006
|
+
{ re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
|
|
3007
|
+
],
|
|
3008
|
+
shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
|
|
3009
|
+
r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
|
|
3010
|
+
};
|
|
3011
|
+
function lastSegment(specifier) {
|
|
3012
|
+
const pathLike = /[/\\]|::/.test(specifier);
|
|
3013
|
+
const segments = specifier.split(/[/\\]|::/).filter(Boolean);
|
|
3014
|
+
let last = segments[segments.length - 1] ?? specifier;
|
|
3015
|
+
if (last === "*" || last === "_") {
|
|
3016
|
+
last = segments[segments.length - 2] ?? specifier;
|
|
3017
|
+
}
|
|
3018
|
+
if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
|
|
3019
|
+
const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
|
|
3020
|
+
return dotted[dotted.length - 1] ?? last;
|
|
3021
|
+
}
|
|
3022
|
+
function newlineOffsets(content) {
|
|
3023
|
+
const offsets = [];
|
|
3024
|
+
for (let i = 0; i < content.length; i++) {
|
|
3025
|
+
if (content.charCodeAt(i) === 10) offsets.push(i);
|
|
3026
|
+
}
|
|
3027
|
+
return offsets;
|
|
3028
|
+
}
|
|
3029
|
+
function lineAt(offsets, index) {
|
|
3030
|
+
let low = 0;
|
|
3031
|
+
let high = offsets.length;
|
|
3032
|
+
while (low < high) {
|
|
3033
|
+
const mid = low + high >>> 1;
|
|
3034
|
+
if ((offsets[mid] ?? 0) < index) low = mid + 1;
|
|
3035
|
+
else high = mid;
|
|
3036
|
+
}
|
|
3037
|
+
return low + 1;
|
|
3038
|
+
}
|
|
3039
|
+
function hasImportPatterns(lang) {
|
|
3040
|
+
return LANG_IMPORTS[lang] !== void 0;
|
|
3041
|
+
}
|
|
3042
|
+
function extractImports(opts) {
|
|
3043
|
+
const patterns = LANG_IMPORTS[opts.lang];
|
|
3044
|
+
if (!patterns || !opts.content) return [];
|
|
3045
|
+
const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
|
|
3046
|
+
const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
|
|
3047
|
+
const refs = [];
|
|
3048
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3049
|
+
const offsets = newlineOffsets(content);
|
|
3050
|
+
for (const pattern of patterns) {
|
|
3051
|
+
const re = new RegExp(pattern.re.source, pattern.re.flags);
|
|
3052
|
+
for (const match of content.matchAll(re)) {
|
|
3053
|
+
if (refs.length >= limit) return refs;
|
|
3054
|
+
const specifier = match[1]?.trim();
|
|
3055
|
+
if (!specifier) continue;
|
|
3056
|
+
const module = specifier;
|
|
3057
|
+
const toName = pattern.name === "full" ? module : lastSegment(module);
|
|
3058
|
+
if (!toName) continue;
|
|
3059
|
+
const key = `${module}\0${toName}`;
|
|
3060
|
+
if (seen.has(key)) continue;
|
|
3061
|
+
seen.add(key);
|
|
3062
|
+
refs.push({
|
|
3063
|
+
fromId: 0,
|
|
3064
|
+
toName,
|
|
3065
|
+
callType: "import",
|
|
3066
|
+
line: lineAt(offsets, match.index ?? 0),
|
|
3067
|
+
lang: opts.lang,
|
|
3068
|
+
module
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return refs;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
2197
3075
|
// src/codebase-index/parser-dispatch.ts
|
|
2198
3076
|
async function parseFileContent(file, content, lang) {
|
|
3077
|
+
const parsed = await dispatch(file, content, lang);
|
|
3078
|
+
return withRelations(parsed, content, lang);
|
|
3079
|
+
}
|
|
3080
|
+
async function dispatch(file, content, lang) {
|
|
2199
3081
|
switch (lang) {
|
|
2200
3082
|
case "ts":
|
|
2201
3083
|
case "tsx":
|
|
@@ -2230,6 +3112,13 @@ async function parseFileContent(file, content, lang) {
|
|
|
2230
3112
|
}
|
|
2231
3113
|
}
|
|
2232
3114
|
}
|
|
3115
|
+
function withRelations(parsed, content, lang) {
|
|
3116
|
+
let refs = parsed.refs ?? [];
|
|
3117
|
+
if (refs.length === 0 && hasImportPatterns(lang)) {
|
|
3118
|
+
refs = extractImports({ content, lang });
|
|
3119
|
+
}
|
|
3120
|
+
return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
|
|
3121
|
+
}
|
|
2233
3122
|
|
|
2234
3123
|
// src/codebase-index/writer.ts
|
|
2235
3124
|
import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
|
|
@@ -2325,6 +3214,9 @@ var Bm25Index = class {
|
|
|
2325
3214
|
}
|
|
2326
3215
|
};
|
|
2327
3216
|
|
|
3217
|
+
// src/codebase-index/writer.ts
|
|
3218
|
+
init_languages();
|
|
3219
|
+
|
|
2328
3220
|
// src/codebase-index/lsp-kind.ts
|
|
2329
3221
|
function lspKindToInternalKind(k) {
|
|
2330
3222
|
switch (k) {
|
|
@@ -2359,7 +3251,7 @@ function lspKindToInternalKind(k) {
|
|
|
2359
3251
|
}
|
|
2360
3252
|
|
|
2361
3253
|
// src/codebase-index/schema.ts
|
|
2362
|
-
var SCHEMA_VERSION =
|
|
3254
|
+
var SCHEMA_VERSION = 4;
|
|
2363
3255
|
|
|
2364
3256
|
// src/codebase-index/sqlite-runtime.ts
|
|
2365
3257
|
import { createRequire } from "node:module";
|
|
@@ -2430,7 +3322,7 @@ function runSqliteWithRetry(fn) {
|
|
|
2430
3322
|
|
|
2431
3323
|
// src/codebase-index/writer-admin.ts
|
|
2432
3324
|
import * as fs6 from "node:fs";
|
|
2433
|
-
import * as
|
|
3325
|
+
import * as path10 from "node:path";
|
|
2434
3326
|
var DB_FILE = "index.db";
|
|
2435
3327
|
function getAllIndexableWithStatement(stmt) {
|
|
2436
3328
|
return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
|
|
@@ -2489,7 +3381,7 @@ function getAllFileMetasWithStatement(stmt) {
|
|
|
2489
3381
|
}
|
|
2490
3382
|
function getIndexDbSizeBytes(indexDir) {
|
|
2491
3383
|
try {
|
|
2492
|
-
return fs6.statSync(
|
|
3384
|
+
return fs6.statSync(path10.join(indexDir, DB_FILE)).size;
|
|
2493
3385
|
} catch {
|
|
2494
3386
|
return 0;
|
|
2495
3387
|
}
|
|
@@ -2540,49 +3432,43 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
|
|
|
2540
3432
|
}
|
|
2541
3433
|
function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
|
|
2542
3434
|
if (refs.length === 0) return;
|
|
2543
|
-
const chunkSize = Math.max(1, Math.floor(maxSqlVars /
|
|
3435
|
+
const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
|
|
2544
3436
|
for (let i = 0; i < refs.length; i += chunkSize) {
|
|
2545
3437
|
const chunk = refs.slice(i, i + chunkSize);
|
|
2546
|
-
const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
|
|
3438
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
|
|
2547
3439
|
const insert = stmt(
|
|
2548
|
-
`INSERT INTO refs(from_id, to_name, to_id, call_type, line
|
|
3440
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
|
|
3441
|
+
VALUES ${placeholders}`
|
|
2549
3442
|
);
|
|
2550
3443
|
const binds = [];
|
|
2551
3444
|
for (const ref of chunk) {
|
|
2552
|
-
binds.push(
|
|
3445
|
+
binds.push(
|
|
3446
|
+
ref.fromId,
|
|
3447
|
+
ref.toName,
|
|
3448
|
+
ref.toId ?? null,
|
|
3449
|
+
ref.callType,
|
|
3450
|
+
ref.line,
|
|
3451
|
+
ref.lang ?? "",
|
|
3452
|
+
ref.module ?? null,
|
|
3453
|
+
ref.toFile ?? null
|
|
3454
|
+
);
|
|
2553
3455
|
}
|
|
2554
3456
|
insert.run(...binds);
|
|
2555
3457
|
}
|
|
2556
3458
|
}
|
|
2557
3459
|
|
|
3460
|
+
// src/codebase-index/writer-graph-reader.ts
|
|
3461
|
+
init_languages();
|
|
3462
|
+
|
|
2558
3463
|
// src/codebase-index/writer-graph-helpers.ts
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
const f = filePath.replace(/\\/g, "/");
|
|
2562
|
-
const pkgsIdx = f.indexOf("/packages/");
|
|
2563
|
-
if (pkgsIdx !== -1) {
|
|
2564
|
-
const rest = f.slice(pkgsIdx + "/packages/".length);
|
|
2565
|
-
const seg = rest.split("/")[0];
|
|
2566
|
-
return seg ? `@wrongstack/${seg}` : void 0;
|
|
2567
|
-
}
|
|
2568
|
-
const appsIdx = f.indexOf("/apps/");
|
|
2569
|
-
if (appsIdx !== -1) {
|
|
2570
|
-
const rest = f.slice(appsIdx + "/apps/".length);
|
|
2571
|
-
const seg = rest.split("/")[0];
|
|
2572
|
-
return seg ? `app:${seg}` : void 0;
|
|
2573
|
-
}
|
|
2574
|
-
return void 0;
|
|
2575
|
-
}
|
|
2576
|
-
function packageFromImport(moduleName) {
|
|
2577
|
-
if (!moduleName.startsWith("@wrongstack/")) return void 0;
|
|
2578
|
-
const parts = moduleName.split("/");
|
|
2579
|
-
return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
|
|
3464
|
+
function createPackageLabeller(stored) {
|
|
3465
|
+
return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
|
|
2580
3466
|
}
|
|
2581
|
-
function buildPackageGraphNodes(fileCounts, files) {
|
|
3467
|
+
function buildPackageGraphNodes(fileCounts, files, packageOf) {
|
|
2582
3468
|
const pkgNodes = /* @__PURE__ */ new Map();
|
|
2583
3469
|
const fileToPkg = /* @__PURE__ */ new Map();
|
|
2584
3470
|
for (const { file, n } of fileCounts) {
|
|
2585
|
-
const pkg =
|
|
3471
|
+
const pkg = packageOf(file);
|
|
2586
3472
|
fileToPkg.set(file, pkg);
|
|
2587
3473
|
const node = pkgNodes.get(pkg);
|
|
2588
3474
|
if (node) {
|
|
@@ -2599,7 +3485,7 @@ function buildPackageGraphNodes(fileCounts, files) {
|
|
|
2599
3485
|
}
|
|
2600
3486
|
}
|
|
2601
3487
|
for (const { file } of files) {
|
|
2602
|
-
const pkg =
|
|
3488
|
+
const pkg = packageOf(file);
|
|
2603
3489
|
fileToPkg.set(file, pkg);
|
|
2604
3490
|
const node = pkgNodes.get(pkg);
|
|
2605
3491
|
if (node) {
|
|
@@ -2617,7 +3503,7 @@ function buildPackageGraphNodes(fileCounts, files) {
|
|
|
2617
3503
|
}
|
|
2618
3504
|
return { pkgNodes, fileToPkg };
|
|
2619
3505
|
}
|
|
2620
|
-
function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
3506
|
+
function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
|
|
2621
3507
|
const fileNodes = /* @__PURE__ */ new Map();
|
|
2622
3508
|
const symToFile = /* @__PURE__ */ new Map();
|
|
2623
3509
|
const fileStats = /* @__PURE__ */ new Map();
|
|
@@ -2636,7 +3522,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
|
2636
3522
|
id: `file:${file}`,
|
|
2637
3523
|
label: file.replace(/\\/g, "/").split("/").pop() ?? file,
|
|
2638
3524
|
kind: "file",
|
|
2639
|
-
package:
|
|
3525
|
+
package: packageOf(file),
|
|
2640
3526
|
file,
|
|
2641
3527
|
symbolCount: stats?.count ?? 0,
|
|
2642
3528
|
lang: stats?.lang,
|
|
@@ -2648,7 +3534,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
|
2648
3534
|
}
|
|
2649
3535
|
return { fileNodes, symToFile, fileStats, ensureFileNode };
|
|
2650
3536
|
}
|
|
2651
|
-
function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
3537
|
+
function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
|
|
2652
3538
|
return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
2653
3539
|
const aExternal = a.file === fileFilter ? 0 : 1;
|
|
2654
3540
|
const bExternal = b.file === fileFilter ? 0 : 1;
|
|
@@ -2660,7 +3546,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
|
2660
3546
|
symbolId: s.id,
|
|
2661
3547
|
symbolKind: s.kind,
|
|
2662
3548
|
file: s.file,
|
|
2663
|
-
package:
|
|
3549
|
+
package: packageOf(s.file),
|
|
2664
3550
|
lang: s.lang,
|
|
2665
3551
|
line: s.line,
|
|
2666
3552
|
signature: s.signature,
|
|
@@ -2668,29 +3554,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
|
2668
3554
|
external: s.file !== fileFilter
|
|
2669
3555
|
}));
|
|
2670
3556
|
}
|
|
2671
|
-
function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
|
|
2672
|
-
if (!moduleName.startsWith(".")) return void 0;
|
|
2673
|
-
const normalizedFrom = fromFile.replace(/\\/g, "/");
|
|
2674
|
-
const absolute = path10.posix.normalize(
|
|
2675
|
-
path10.posix.join(path10.posix.dirname(normalizedFrom), moduleName)
|
|
2676
|
-
);
|
|
2677
|
-
const extension = path10.posix.extname(absolute);
|
|
2678
|
-
const base = extension ? absolute.slice(0, -extension.length) : absolute;
|
|
2679
|
-
const candidates = [
|
|
2680
|
-
absolute,
|
|
2681
|
-
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
|
|
2682
|
-
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path10.posix.join(absolute, `index${ext}`)),
|
|
2683
|
-
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path10.posix.join(base, `index${ext}`))
|
|
2684
|
-
];
|
|
2685
|
-
const indexedByPortablePath = new Map(
|
|
2686
|
-
[...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
|
|
2687
|
-
);
|
|
2688
|
-
for (const candidate of candidates) {
|
|
2689
|
-
const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
|
|
2690
|
-
if (indexed) return indexed;
|
|
2691
|
-
}
|
|
2692
|
-
return void 0;
|
|
2693
|
-
}
|
|
2694
3557
|
function addWeightedEdge(edgeMap, source, target, callType, weight) {
|
|
2695
3558
|
const key = `${source}\0${target}`;
|
|
2696
3559
|
let edge = edgeMap.get(key);
|
|
@@ -2731,11 +3594,146 @@ function mapWriterRefRow(row) {
|
|
|
2731
3594
|
toName: row.to_name,
|
|
2732
3595
|
toId: row.to_id ?? void 0,
|
|
2733
3596
|
callType: row.call_type,
|
|
2734
|
-
line: row.line
|
|
3597
|
+
line: row.line,
|
|
3598
|
+
// `lang`/`module`/`to_file` are absent from the narrower column lists some
|
|
3599
|
+
// queries select; `undefined` keeps those rows valid Refs.
|
|
3600
|
+
lang: row.lang || void 0,
|
|
3601
|
+
module: row.module ?? void 0,
|
|
3602
|
+
toFile: row.to_file ?? void 0
|
|
2735
3603
|
};
|
|
2736
3604
|
}
|
|
2737
3605
|
|
|
2738
3606
|
// src/codebase-index/writer-graph-reader.ts
|
|
3607
|
+
var MAX_SQL_VARS = 900;
|
|
3608
|
+
function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
|
|
3609
|
+
const results = [];
|
|
3610
|
+
for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
|
|
3611
|
+
const chunk = ids.slice(start, start + MAX_SQL_VARS);
|
|
3612
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
3613
|
+
const sql = buildSql(placeholders);
|
|
3614
|
+
results.push(...stmt(sql).all(...chunk, ...extraArgs));
|
|
3615
|
+
}
|
|
3616
|
+
return results;
|
|
3617
|
+
}
|
|
3618
|
+
function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
|
|
3619
|
+
let total = 0;
|
|
3620
|
+
for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
|
|
3621
|
+
const chunk = ids.slice(start, start + MAX_SQL_VARS);
|
|
3622
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
3623
|
+
const sql = buildSql(placeholders);
|
|
3624
|
+
const rows = stmt(sql).all(...chunk, ...extraArgs);
|
|
3625
|
+
total += rows[0]?.n ?? 0;
|
|
3626
|
+
}
|
|
3627
|
+
return total;
|
|
3628
|
+
}
|
|
3629
|
+
function mapCallSiteRow(row) {
|
|
3630
|
+
return {
|
|
3631
|
+
symbol: {
|
|
3632
|
+
id: row.sym_id,
|
|
3633
|
+
name: row.sym_name,
|
|
3634
|
+
kind: row.sym_kind,
|
|
3635
|
+
lang: row.sym_lang,
|
|
3636
|
+
file: row.sym_file,
|
|
3637
|
+
line: row.sym_line,
|
|
3638
|
+
signature: row.sym_signature
|
|
3639
|
+
},
|
|
3640
|
+
callType: row.call_type,
|
|
3641
|
+
line: row.ref_line
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
function resolveSymbolIds(stmt, symbolName, file) {
|
|
3645
|
+
const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
|
|
3646
|
+
const args = file ? [symbolName, file] : [symbolName];
|
|
3647
|
+
const rows = stmt(baseSql).all(...args);
|
|
3648
|
+
return rows.map((r) => r.id);
|
|
3649
|
+
}
|
|
3650
|
+
function findIncomingCallsByName(stmt, symbolName, file, limit) {
|
|
3651
|
+
const targetIds = resolveSymbolIds(stmt, symbolName, file);
|
|
3652
|
+
if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
|
|
3653
|
+
let matchIds = targetIds;
|
|
3654
|
+
let ambiguous = false;
|
|
3655
|
+
if (file !== void 0) {
|
|
3656
|
+
const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
|
|
3657
|
+
if (allNamedIds.length > targetIds.length) {
|
|
3658
|
+
matchIds = allNamedIds;
|
|
3659
|
+
ambiguous = true;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
const useFallback = !file;
|
|
3663
|
+
const rows = chunkedIdQuery(
|
|
3664
|
+
stmt,
|
|
3665
|
+
matchIds,
|
|
3666
|
+
(ph) => `SELECT
|
|
3667
|
+
s.id AS sym_id,
|
|
3668
|
+
s.name AS sym_name,
|
|
3669
|
+
s.kind AS sym_kind,
|
|
3670
|
+
s.lang AS sym_lang,
|
|
3671
|
+
s.file AS sym_file,
|
|
3672
|
+
s.line AS sym_line,
|
|
3673
|
+
s.signature AS sym_signature,
|
|
3674
|
+
r.call_type,
|
|
3675
|
+
r.line AS ref_line
|
|
3676
|
+
FROM refs r
|
|
3677
|
+
JOIN symbols s ON s.id = r.from_id
|
|
3678
|
+
WHERE r.to_id IN (${ph})
|
|
3679
|
+
ORDER BY r.line, r.id`,
|
|
3680
|
+
[]
|
|
3681
|
+
);
|
|
3682
|
+
if (useFallback) {
|
|
3683
|
+
const fallbackRows = stmt(
|
|
3684
|
+
`SELECT
|
|
3685
|
+
s.id AS sym_id,
|
|
3686
|
+
s.name AS sym_name,
|
|
3687
|
+
s.kind AS sym_kind,
|
|
3688
|
+
s.lang AS sym_lang,
|
|
3689
|
+
s.file AS sym_file,
|
|
3690
|
+
s.line AS sym_line,
|
|
3691
|
+
s.signature AS sym_signature,
|
|
3692
|
+
r.call_type,
|
|
3693
|
+
r.line AS ref_line
|
|
3694
|
+
FROM refs r
|
|
3695
|
+
JOIN symbols s ON s.id = r.from_id
|
|
3696
|
+
WHERE r.to_id IS NULL AND r.to_name = ?
|
|
3697
|
+
ORDER BY r.line, r.id`
|
|
3698
|
+
).all(symbolName);
|
|
3699
|
+
rows.push(...fallbackRows);
|
|
3700
|
+
}
|
|
3701
|
+
rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
|
|
3702
|
+
const allCalls = rows.map(mapCallSiteRow);
|
|
3703
|
+
return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
|
|
3704
|
+
}
|
|
3705
|
+
function findOutgoingCallsByName(stmt, symbolName, file, limit) {
|
|
3706
|
+
const sourceIds = resolveSymbolIds(stmt, symbolName, file);
|
|
3707
|
+
if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
|
|
3708
|
+
const unresolvedCount = chunkedIdScalar(
|
|
3709
|
+
stmt,
|
|
3710
|
+
sourceIds,
|
|
3711
|
+
(ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
|
|
3712
|
+
);
|
|
3713
|
+
const rows = chunkedIdQuery(
|
|
3714
|
+
stmt,
|
|
3715
|
+
sourceIds,
|
|
3716
|
+
(ph) => `SELECT
|
|
3717
|
+
s.id AS sym_id,
|
|
3718
|
+
s.name AS sym_name,
|
|
3719
|
+
s.kind AS sym_kind,
|
|
3720
|
+
s.lang AS sym_lang,
|
|
3721
|
+
s.file AS sym_file,
|
|
3722
|
+
s.line AS sym_line,
|
|
3723
|
+
s.signature AS sym_signature,
|
|
3724
|
+
r.call_type,
|
|
3725
|
+
r.line AS ref_line
|
|
3726
|
+
FROM refs r
|
|
3727
|
+
JOIN symbols s ON s.id = r.to_id
|
|
3728
|
+
WHERE r.from_id IN (${ph})
|
|
3729
|
+
AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
|
|
3730
|
+
ORDER BY r.line, r.id`,
|
|
3731
|
+
[]
|
|
3732
|
+
);
|
|
3733
|
+
rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
|
|
3734
|
+
const calls = rows.map(mapCallSiteRow).slice(0, limit);
|
|
3735
|
+
return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
|
|
3736
|
+
}
|
|
2739
3737
|
function findRefsToWithStatement(stmt, symbolId) {
|
|
2740
3738
|
return stmt(
|
|
2741
3739
|
"SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
|
|
@@ -2749,7 +3747,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
|
|
|
2749
3747
|
function getPackageGraphWithStatement(stmt) {
|
|
2750
3748
|
const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
|
|
2751
3749
|
const files = stmt("SELECT DISTINCT file FROM files").all();
|
|
2752
|
-
const
|
|
3750
|
+
const packageOf = readPackageLabeller(stmt);
|
|
3751
|
+
const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
|
|
2753
3752
|
const refRows = stmt(
|
|
2754
3753
|
`SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
|
|
2755
3754
|
FROM refs r
|
|
@@ -2760,32 +3759,42 @@ function getPackageGraphWithStatement(stmt) {
|
|
|
2760
3759
|
).all();
|
|
2761
3760
|
const edgeMap = /* @__PURE__ */ new Map();
|
|
2762
3761
|
for (const r of refRows) {
|
|
2763
|
-
const fromPkg = fileToPkg.get(r.from_file) ??
|
|
2764
|
-
const toPkg = fileToPkg.get(r.to_file) ??
|
|
3762
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
3763
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
2765
3764
|
if (fromPkg === toPkg) continue;
|
|
2766
3765
|
const n = Number(r.n) || 0;
|
|
2767
3766
|
addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
|
|
2768
3767
|
}
|
|
2769
3768
|
const importRows = stmt(
|
|
2770
|
-
`SELECT
|
|
3769
|
+
`SELECT s.file AS from_file,
|
|
3770
|
+
COALESCE(r.to_file, st.file) AS to_file,
|
|
3771
|
+
COUNT(*) AS n
|
|
2771
3772
|
FROM refs r
|
|
2772
3773
|
JOIN symbols s ON s.id = r.from_id
|
|
3774
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
2773
3775
|
WHERE r.call_type = 'import'
|
|
2774
|
-
|
|
3776
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
3777
|
+
GROUP BY s.file, COALESCE(r.to_file, st.file)`
|
|
2775
3778
|
).all();
|
|
2776
3779
|
for (const r of importRows) {
|
|
2777
|
-
const fromPkg = fileToPkg.get(r.from_file) ??
|
|
2778
|
-
const toPkg =
|
|
2779
|
-
if (
|
|
3780
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
3781
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
3782
|
+
if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
|
|
2780
3783
|
const n = Number(r.n) || 0;
|
|
2781
3784
|
addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
|
|
2782
3785
|
}
|
|
2783
3786
|
const edges = materializeWeightedEdges(edgeMap, "pkg");
|
|
2784
3787
|
return { nodes: [...pkgNodes.values()], edges };
|
|
2785
3788
|
}
|
|
3789
|
+
function readPackageLabeller(stmt) {
|
|
3790
|
+
const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
3791
|
+
return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
|
|
3792
|
+
}
|
|
2786
3793
|
function getFileGraphWithStatement(stmt, packageFilter) {
|
|
2787
3794
|
const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
|
|
2788
|
-
const
|
|
3795
|
+
const packageOf = readPackageLabeller(stmt);
|
|
3796
|
+
const langOf = (file) => detectLang(file) ?? "other";
|
|
3797
|
+
const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
|
|
2789
3798
|
const localFiles = new Set(pkgFilePaths);
|
|
2790
3799
|
if (localFiles.size === 0) return { nodes: [], edges: [] };
|
|
2791
3800
|
const filePlaceholders = [...localFiles].map(() => "?").join(",");
|
|
@@ -2794,9 +3803,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2794
3803
|
).all(...pkgFilePaths);
|
|
2795
3804
|
const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
|
|
2796
3805
|
pkgSyms,
|
|
2797
|
-
localFiles
|
|
3806
|
+
localFiles,
|
|
3807
|
+
packageOf
|
|
2798
3808
|
);
|
|
2799
|
-
const indexedFiles = new Set(allFiles.map((f) => f.file));
|
|
2800
3809
|
const refRows = stmt(
|
|
2801
3810
|
`SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
|
|
2802
3811
|
FROM refs r
|
|
@@ -2819,7 +3828,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2819
3828
|
for (const x of extras) {
|
|
2820
3829
|
symToFile.set(x.id, x.file);
|
|
2821
3830
|
if (!fileStats.has(x.file)) {
|
|
2822
|
-
fileStats.set(x.file, { count: 0, lang:
|
|
3831
|
+
fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
|
|
2823
3832
|
}
|
|
2824
3833
|
}
|
|
2825
3834
|
}
|
|
@@ -2836,17 +3845,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2836
3845
|
addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
|
|
2837
3846
|
}
|
|
2838
3847
|
const importRows = stmt(
|
|
2839
|
-
`SELECT r.from_id, r.
|
|
3848
|
+
`SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
|
|
2840
3849
|
FROM refs r
|
|
3850
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
2841
3851
|
WHERE r.call_type = 'import'
|
|
3852
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
2842
3853
|
AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
|
|
2843
|
-
GROUP BY r.from_id, r.
|
|
3854
|
+
GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
|
|
2844
3855
|
).all(...pkgFilePaths);
|
|
2845
3856
|
for (const r of importRows) {
|
|
2846
3857
|
const fromFile = symToFile.get(r.from_id);
|
|
2847
3858
|
if (!fromFile || !localFiles.has(fromFile)) continue;
|
|
2848
|
-
const toFile =
|
|
3859
|
+
const toFile = r.to_file;
|
|
2849
3860
|
if (!toFile || fromFile === toFile) continue;
|
|
3861
|
+
if (!fileStats.has(toFile)) {
|
|
3862
|
+
fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
|
|
3863
|
+
}
|
|
2850
3864
|
ensureFileNode(fromFile);
|
|
2851
3865
|
ensureFileNode(toFile);
|
|
2852
3866
|
const n = Number(r.n) || 0;
|
|
@@ -2896,7 +3910,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
|
|
|
2896
3910
|
).all(...missingIds);
|
|
2897
3911
|
for (const s of extras) symById.set(s.id, s);
|
|
2898
3912
|
}
|
|
2899
|
-
const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
|
|
3913
|
+
const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
|
|
2900
3914
|
return { nodes, edges };
|
|
2901
3915
|
}
|
|
2902
3916
|
|
|
@@ -2918,7 +3932,7 @@ function assignRefsToSymbols(refs, symbols) {
|
|
|
2918
3932
|
}
|
|
2919
3933
|
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
2920
3934
|
if (!owner || owner.id <= 0) continue;
|
|
2921
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
3935
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
2922
3936
|
if (seen.has(key)) continue;
|
|
2923
3937
|
seen.add(key);
|
|
2924
3938
|
assigned.push({ ...ref, fromId: owner.id });
|
|
@@ -2964,7 +3978,11 @@ var CORE_TABLES_SQL = `
|
|
|
2964
3978
|
lang TEXT NOT NULL,
|
|
2965
3979
|
mtime_ms INTEGER NOT NULL,
|
|
2966
3980
|
symbol_count INTEGER NOT NULL DEFAULT 0,
|
|
2967
|
-
last_indexed INTEGER NOT NULL
|
|
3981
|
+
last_indexed INTEGER NOT NULL,
|
|
3982
|
+
-- Code Atlas grouping label, computed at index time from the ecosystem's
|
|
3983
|
+
-- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
|
|
3984
|
+
-- re-derived per query because the evidence lives on disk, not in the DB.
|
|
3985
|
+
package TEXT NOT NULL DEFAULT ''
|
|
2968
3986
|
);
|
|
2969
3987
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
2970
3988
|
id INTEGER PRIMARY KEY,
|
|
@@ -2981,6 +3999,9 @@ var CORE_TABLES_SQL = `
|
|
|
2981
3999
|
file_fk TEXT NOT NULL
|
|
2982
4000
|
);
|
|
2983
4001
|
`;
|
|
4002
|
+
var FILE_INDEX_SQL = [
|
|
4003
|
+
"CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
|
|
4004
|
+
];
|
|
2984
4005
|
var SYMBOL_INDEX_SQL = [
|
|
2985
4006
|
"CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
|
|
2986
4007
|
"CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
|
|
@@ -2997,15 +4018,32 @@ var REFS_TABLE_SQL = `
|
|
|
2997
4018
|
to_name TEXT NOT NULL,
|
|
2998
4019
|
to_id INTEGER,
|
|
2999
4020
|
call_type TEXT NOT NULL,
|
|
3000
|
-
line INTEGER NOT NULL
|
|
4021
|
+
line INTEGER NOT NULL,
|
|
4022
|
+
lang TEXT NOT NULL DEFAULT '',
|
|
4023
|
+
module TEXT,
|
|
4024
|
+
to_file TEXT
|
|
3001
4025
|
);
|
|
3002
4026
|
`;
|
|
3003
4027
|
var REFS_INDEX_SQL = [
|
|
3004
4028
|
"CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
|
|
3005
4029
|
"CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
|
|
3006
4030
|
"CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
|
|
3007
|
-
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
|
|
4031
|
+
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
|
|
4032
|
+
// Name resolution matches (to_name, lang) pairs; the composite keeps the
|
|
4033
|
+
// language-scoped UPDATE from degrading into a scan of every same-named row.
|
|
4034
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
|
|
4035
|
+
// The post-index module resolution pass groups unresolved import refs by
|
|
4036
|
+
// (module, lang); graph readers then read to_file back.
|
|
4037
|
+
"CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
|
|
4038
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
|
|
3008
4039
|
];
|
|
4040
|
+
var LANG_FAMILY_TABLE_SQL = `
|
|
4041
|
+
CREATE TABLE IF NOT EXISTS lang_family (
|
|
4042
|
+
lang TEXT PRIMARY KEY,
|
|
4043
|
+
family TEXT NOT NULL
|
|
4044
|
+
);
|
|
4045
|
+
`;
|
|
4046
|
+
var LANG_FAMILY_WILDCARD = "*";
|
|
3009
4047
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
3010
4048
|
|
|
3011
4049
|
// src/codebase-index/writer-search-helpers.ts
|
|
@@ -3226,6 +4264,60 @@ var IndexStore = class _IndexStore {
|
|
|
3226
4264
|
runWithRetry(fn) {
|
|
3227
4265
|
return runSqliteWithRetry(fn);
|
|
3228
4266
|
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Mirror the in-process language→family map into SQLite.
|
|
4269
|
+
*
|
|
4270
|
+
* Rewritten on every open rather than only on schema bumps: the mapping is
|
|
4271
|
+
* static lookup data, so a code-side change (a new language, a language
|
|
4272
|
+
* moving families) must take effect without forcing a full reindex.
|
|
4273
|
+
*/
|
|
4274
|
+
seedLangFamilies() {
|
|
4275
|
+
const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
|
|
4276
|
+
for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
|
|
4277
|
+
insert.run("", LANG_FAMILY_WILDCARD);
|
|
4278
|
+
}
|
|
4279
|
+
/**
|
|
4280
|
+
* Add any column the current schema expects but the on-disk table lacks.
|
|
4281
|
+
*
|
|
4282
|
+
* `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
|
|
4283
|
+
* and the version check above only rebuilds on a version *mismatch*. That
|
|
4284
|
+
* leaves a real gap: several wstack processes share this database, and while
|
|
4285
|
+
* a version upgrade is rolling out one of them may still be running the
|
|
4286
|
+
* previous build. That older process sees the newer version number, drops the
|
|
4287
|
+
* tables, and recreates them from *its* DDL — without the newer columns —
|
|
4288
|
+
* while the metadata row still reads the new version. Every later query for
|
|
4289
|
+
* one of those columns then fails with `no such column`, and no amount of
|
|
4290
|
+
* reindexing fixes it, because the version numbers already agree.
|
|
4291
|
+
*
|
|
4292
|
+
* Repairing column-by-column makes the schema self-healing from any of those
|
|
4293
|
+
* states. Table and column names are compile-time literals from this module,
|
|
4294
|
+
* never user input.
|
|
4295
|
+
*/
|
|
4296
|
+
repairMissingColumns() {
|
|
4297
|
+
const expected = [
|
|
4298
|
+
{ table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
|
|
4299
|
+
{
|
|
4300
|
+
table: "refs",
|
|
4301
|
+
columns: [
|
|
4302
|
+
["lang", "TEXT NOT NULL DEFAULT ''"],
|
|
4303
|
+
["module", "TEXT"],
|
|
4304
|
+
["to_file", "TEXT"]
|
|
4305
|
+
]
|
|
4306
|
+
}
|
|
4307
|
+
];
|
|
4308
|
+
for (const { table, columns } of expected) {
|
|
4309
|
+
const present = new Set(
|
|
4310
|
+
this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
|
|
4311
|
+
(row) => typeof row.name === "string" ? [row.name] : []
|
|
4312
|
+
)
|
|
4313
|
+
);
|
|
4314
|
+
if (present.size === 0) continue;
|
|
4315
|
+
for (const [name, type] of columns) {
|
|
4316
|
+
if (present.has(name)) continue;
|
|
4317
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
3229
4321
|
initSchema() {
|
|
3230
4322
|
this.db.exec(METADATA_TABLE_SQL);
|
|
3231
4323
|
const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
@@ -3248,9 +4340,13 @@ var IndexStore = class _IndexStore {
|
|
|
3248
4340
|
);
|
|
3249
4341
|
}
|
|
3250
4342
|
this.db.exec(CORE_TABLES_SQL);
|
|
3251
|
-
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
3252
4343
|
this.db.exec(REFS_TABLE_SQL);
|
|
4344
|
+
this.repairMissingColumns();
|
|
4345
|
+
for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
|
|
4346
|
+
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
3253
4347
|
for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
|
|
4348
|
+
this.db.exec(LANG_FAMILY_TABLE_SQL);
|
|
4349
|
+
this.seedLangFamilies();
|
|
3254
4350
|
try {
|
|
3255
4351
|
this.db.exec(SYMBOLS_FTS_SQL);
|
|
3256
4352
|
this.ftsAvailable = true;
|
|
@@ -3285,6 +4381,18 @@ var IndexStore = class _IndexStore {
|
|
|
3285
4381
|
static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
|
|
3286
4382
|
/** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
|
|
3287
4383
|
static MAX_SQL_VARS = 900;
|
|
4384
|
+
/**
|
|
4385
|
+
* Correlated predicate: the ref in `refs` and the candidate symbol aliased
|
|
4386
|
+
* `sym` belong to the same language family — or the ref carries no language,
|
|
4387
|
+
* in which case the wildcard bind matches everything.
|
|
4388
|
+
*
|
|
4389
|
+
* Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
|
|
4390
|
+
*/
|
|
4391
|
+
static FAMILY_MATCH_SQL = `(
|
|
4392
|
+
(SELECT family FROM lang_family WHERE lang = refs.lang) = ?
|
|
4393
|
+
OR (SELECT family FROM lang_family WHERE lang = sym.lang)
|
|
4394
|
+
= (SELECT family FROM lang_family WHERE lang = refs.lang)
|
|
4395
|
+
)`;
|
|
3288
4396
|
/**
|
|
3289
4397
|
* Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
|
|
3290
4398
|
* transaction on open; the first concurrent writer under BEGIN IMMEDIATE
|
|
@@ -3348,9 +4456,12 @@ var IndexStore = class _IndexStore {
|
|
|
3348
4456
|
const placeholders = chunk.map(() => "?").join(",");
|
|
3349
4457
|
const result = this.stmt(
|
|
3350
4458
|
`UPDATE refs
|
|
3351
|
-
SET to_id = (
|
|
4459
|
+
SET to_id = (
|
|
4460
|
+
SELECT MIN(sym.id) FROM symbols sym
|
|
4461
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
4462
|
+
)
|
|
3352
4463
|
WHERE to_name IN (${placeholders})`
|
|
3353
|
-
).run(...chunk);
|
|
4464
|
+
).run(LANG_FAMILY_WILDCARD, ...chunk);
|
|
3354
4465
|
changes += result.changes ?? 0;
|
|
3355
4466
|
}
|
|
3356
4467
|
return changes;
|
|
@@ -3477,6 +4588,115 @@ var IndexStore = class _IndexStore {
|
|
|
3477
4588
|
getAllFileMetas() {
|
|
3478
4589
|
return getAllFileMetasWithStatement((sql) => this.stmt(sql));
|
|
3479
4590
|
}
|
|
4591
|
+
// ─── Project structure & module resolution ──────────────────────────────────
|
|
4592
|
+
/** Store the Code Atlas grouping label for each indexed file. */
|
|
4593
|
+
setFilePackages(entries) {
|
|
4594
|
+
if (entries.size === 0) return;
|
|
4595
|
+
this.runWithRetry(() => {
|
|
4596
|
+
const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
|
|
4597
|
+
for (const [file, label] of entries) update.run(label, file);
|
|
4598
|
+
});
|
|
4599
|
+
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Every indexed `namespace`/`module` declaration, for ecosystems whose import
|
|
4602
|
+
* specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
|
|
4603
|
+
* Ordered so the resolver's choice among duplicate declarations is stable.
|
|
4604
|
+
*/
|
|
4605
|
+
getNamespaceDeclarations() {
|
|
4606
|
+
return this.stmt(
|
|
4607
|
+
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
4608
|
+
).all();
|
|
4609
|
+
}
|
|
4610
|
+
/** `file → package` for every indexed file that has a label. */
|
|
4611
|
+
getFilePackages() {
|
|
4612
|
+
const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
4613
|
+
return new Map(rows.map((row) => [row.file, row.package]));
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Distinct `(fromFile, lang, module)` triples needing module resolution.
|
|
4617
|
+
*
|
|
4618
|
+
* Distinct rather than per-ref because resolution depends only on these three
|
|
4619
|
+
* values: a file importing the same module twenty times resolves it once.
|
|
4620
|
+
*/
|
|
4621
|
+
getUnresolvedImports(onlyFiles) {
|
|
4622
|
+
const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
|
|
4623
|
+
FROM refs r
|
|
4624
|
+
JOIN symbols s ON s.id = r.from_id
|
|
4625
|
+
WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
|
|
4626
|
+
if (!onlyFiles?.length) {
|
|
4627
|
+
return this.stmt(base).all();
|
|
4628
|
+
}
|
|
4629
|
+
const out = [];
|
|
4630
|
+
for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
|
|
4631
|
+
const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
|
|
4632
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
4633
|
+
out.push(
|
|
4634
|
+
...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
|
|
4635
|
+
);
|
|
4636
|
+
}
|
|
4637
|
+
return out;
|
|
4638
|
+
}
|
|
4639
|
+
/**
|
|
4640
|
+
* Write resolved import targets back onto `refs.to_file`.
|
|
4641
|
+
*
|
|
4642
|
+
* Applied through a temp table and a single UPDATE: one statement per
|
|
4643
|
+
* resolution would mean thousands of round-trips on a first index.
|
|
4644
|
+
*/
|
|
4645
|
+
applyImportResolutions(resolutions) {
|
|
4646
|
+
if (resolutions.length === 0) return 0;
|
|
4647
|
+
return this.runWithRetry(() => {
|
|
4648
|
+
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
4649
|
+
this.db.exec(
|
|
4650
|
+
`CREATE TEMP TABLE import_resolution (
|
|
4651
|
+
from_file TEXT NOT NULL,
|
|
4652
|
+
lang TEXT NOT NULL,
|
|
4653
|
+
module TEXT NOT NULL,
|
|
4654
|
+
to_file TEXT NOT NULL
|
|
4655
|
+
)`
|
|
4656
|
+
);
|
|
4657
|
+
const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
|
|
4658
|
+
for (let i = 0; i < resolutions.length; i += chunkSize) {
|
|
4659
|
+
const chunk = resolutions.slice(i, i + chunkSize);
|
|
4660
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
4661
|
+
const binds = [];
|
|
4662
|
+
for (const entry of chunk) {
|
|
4663
|
+
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
4664
|
+
}
|
|
4665
|
+
this.stmt(
|
|
4666
|
+
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
4667
|
+
VALUES ${placeholders}`
|
|
4668
|
+
).run(...binds);
|
|
4669
|
+
}
|
|
4670
|
+
this.db.exec(
|
|
4671
|
+
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
4672
|
+
ON import_resolution(module, lang, from_file)`
|
|
4673
|
+
);
|
|
4674
|
+
const result = this.stmt(
|
|
4675
|
+
`UPDATE refs
|
|
4676
|
+
SET to_file = (
|
|
4677
|
+
SELECT ir.to_file
|
|
4678
|
+
FROM temp.import_resolution ir
|
|
4679
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
4680
|
+
WHERE ir.module = refs.module
|
|
4681
|
+
AND ir.lang = refs.lang
|
|
4682
|
+
AND ir.from_file = s.file
|
|
4683
|
+
LIMIT 1
|
|
4684
|
+
)
|
|
4685
|
+
WHERE refs.call_type = 'import'
|
|
4686
|
+
AND refs.module IS NOT NULL
|
|
4687
|
+
AND EXISTS (
|
|
4688
|
+
SELECT 1
|
|
4689
|
+
FROM temp.import_resolution ir
|
|
4690
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
4691
|
+
WHERE ir.module = refs.module
|
|
4692
|
+
AND ir.lang = refs.lang
|
|
4693
|
+
AND ir.from_file = s.file
|
|
4694
|
+
)`
|
|
4695
|
+
).run();
|
|
4696
|
+
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
4697
|
+
return result.changes ?? 0;
|
|
4698
|
+
});
|
|
4699
|
+
}
|
|
3480
4700
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
3481
4701
|
search(query, filter, opts) {
|
|
3482
4702
|
const built = this.buildSearchWhere(query, filter);
|
|
@@ -3863,9 +5083,12 @@ var IndexStore = class _IndexStore {
|
|
|
3863
5083
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
3864
5084
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
3865
5085
|
*
|
|
3866
|
-
*
|
|
3867
|
-
* the
|
|
3868
|
-
*
|
|
5086
|
+
* A match additionally requires the referencing ref and the target symbol to
|
|
5087
|
+
* be in the same {@link LangFamily}. Without that guard a name match is a
|
|
5088
|
+
* cross-language accident waiting to happen — `main`, `New`, `Parse` and
|
|
5089
|
+
* `Config` are declared in most languages at once, and each collision draws a
|
|
5090
|
+
* Code Atlas edge between files that never reference each other. Refs stored
|
|
5091
|
+
* without a language keep the old global behaviour via the `'*'` wildcard row.
|
|
3869
5092
|
*/
|
|
3870
5093
|
resolveRefs() {
|
|
3871
5094
|
return this.runWithRetry(() => {
|
|
@@ -3874,20 +5097,35 @@ var IndexStore = class _IndexStore {
|
|
|
3874
5097
|
`UPDATE refs
|
|
3875
5098
|
SET to_id = s.id
|
|
3876
5099
|
FROM (
|
|
3877
|
-
|
|
3878
|
-
|
|
5100
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
5101
|
+
FROM symbols sym
|
|
5102
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
5103
|
+
GROUP BY sym.name, lf.family
|
|
5104
|
+
UNION ALL
|
|
5105
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
5106
|
+
FROM symbols sym
|
|
5107
|
+
GROUP BY sym.name
|
|
5108
|
+
) AS s,
|
|
5109
|
+
lang_family AS rf
|
|
3879
5110
|
WHERE refs.to_id IS NULL
|
|
3880
5111
|
AND refs.to_name IS NOT NULL
|
|
3881
|
-
AND
|
|
5112
|
+
AND rf.lang = refs.lang
|
|
5113
|
+
AND s.name = refs.to_name
|
|
5114
|
+
AND s.family = rf.family`
|
|
3882
5115
|
).run();
|
|
3883
5116
|
return result.changes ?? 0;
|
|
3884
5117
|
} catch {
|
|
3885
5118
|
const result = this.stmt(
|
|
3886
5119
|
`UPDATE refs SET to_id = (
|
|
3887
|
-
SELECT id FROM symbols
|
|
5120
|
+
SELECT sym.id FROM symbols sym
|
|
5121
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
5122
|
+
ORDER BY sym.id LIMIT 1
|
|
3888
5123
|
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
3889
|
-
AND
|
|
3890
|
-
|
|
5124
|
+
AND EXISTS (
|
|
5125
|
+
SELECT 1 FROM symbols sym
|
|
5126
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
5127
|
+
)`
|
|
5128
|
+
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
3891
5129
|
return result.changes ?? 0;
|
|
3892
5130
|
}
|
|
3893
5131
|
});
|
|
@@ -3971,6 +5209,20 @@ var IndexStore = class _IndexStore {
|
|
|
3971
5209
|
return false;
|
|
3972
5210
|
}
|
|
3973
5211
|
}
|
|
5212
|
+
/**
|
|
5213
|
+
* Find all symbols that reference the named target symbol (incoming callers).
|
|
5214
|
+
* Accepts a name instead of an id so the agent doesn't need a prior lookup.
|
|
5215
|
+
*/
|
|
5216
|
+
findIncomingCallsByName(symbolName, file, limit = 100) {
|
|
5217
|
+
return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
5218
|
+
}
|
|
5219
|
+
/**
|
|
5220
|
+
* Find all symbols that the named source symbol references (outgoing callees).
|
|
5221
|
+
* Accepts a name instead of an id so the agent doesn't need a prior lookup.
|
|
5222
|
+
*/
|
|
5223
|
+
findOutgoingCallsByName(symbolName, file, limit = 100) {
|
|
5224
|
+
return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
5225
|
+
}
|
|
3974
5226
|
/**
|
|
3975
5227
|
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
3976
5228
|
*/
|
|
@@ -4091,7 +5343,7 @@ function normalizeComparablePath(value) {
|
|
|
4091
5343
|
}
|
|
4092
5344
|
function gitOutput(projectRoot, args) {
|
|
4093
5345
|
return new Promise((resolve4, reject) => {
|
|
4094
|
-
|
|
5346
|
+
execFile(
|
|
4095
5347
|
"git",
|
|
4096
5348
|
["-C", projectRoot, ...args],
|
|
4097
5349
|
{
|
|
@@ -4222,13 +5474,40 @@ function assignRefsToSymbols2(refs, symbols) {
|
|
|
4222
5474
|
}
|
|
4223
5475
|
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
4224
5476
|
if (!owner || owner.id <= 0) continue;
|
|
4225
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
5477
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
4226
5478
|
if (seen.has(key)) continue;
|
|
4227
5479
|
seen.add(key);
|
|
4228
5480
|
assigned.push({ ...ref, fromId: owner.id });
|
|
4229
5481
|
}
|
|
4230
5482
|
return assigned;
|
|
4231
5483
|
}
|
|
5484
|
+
async function resolveProjectRelations(store, projectRoot, opts) {
|
|
5485
|
+
if (opts.signal?.aborted) return;
|
|
5486
|
+
try {
|
|
5487
|
+
const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
|
|
5488
|
+
if (indexedFiles.length === 0) return;
|
|
5489
|
+
const structure = await detectModuleRoots(projectRoot, indexedFiles);
|
|
5490
|
+
if (opts.signal?.aborted) return;
|
|
5491
|
+
store.setFilePackages(assignPackageLabels(structure, indexedFiles));
|
|
5492
|
+
const resolver = new ModuleResolver(
|
|
5493
|
+
structure,
|
|
5494
|
+
indexedFiles,
|
|
5495
|
+
store.getNamespaceDeclarations()
|
|
5496
|
+
);
|
|
5497
|
+
const pending2 = store.getUnresolvedImports(opts.onlyFiles);
|
|
5498
|
+
const resolutions = [];
|
|
5499
|
+
for (const entry of pending2) {
|
|
5500
|
+
const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
|
|
5501
|
+
if (toFile && toFile !== entry.fromFile) {
|
|
5502
|
+
resolutions.push({ ...entry, toFile });
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
if (opts.signal?.aborted) return;
|
|
5506
|
+
store.applyImportResolutions(resolutions);
|
|
5507
|
+
} catch (err) {
|
|
5508
|
+
opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
4232
5511
|
async function runIndexerWithStore(store, opts) {
|
|
4233
5512
|
const { projectRoot, langs, ignore = [], signal } = opts;
|
|
4234
5513
|
const relationGraphVersion = "2";
|
|
@@ -4473,6 +5752,14 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4473
5752
|
}
|
|
4474
5753
|
}
|
|
4475
5754
|
if (needsFullRefResolution) store.resolveRefs();
|
|
5755
|
+
await resolveProjectRelations(store, projectRoot, {
|
|
5756
|
+
// A watcher run re-resolves only what it touched; a full run (or a contract
|
|
5757
|
+
// bump) re-resolves everything, because a newly indexed file can be the
|
|
5758
|
+
// target of imports written long before it.
|
|
5759
|
+
onlyFiles: needsFullRefResolution ? void 0 : opts.files,
|
|
5760
|
+
errors,
|
|
5761
|
+
signal
|
|
5762
|
+
});
|
|
4476
5763
|
store.setMetadata("ref_resolution_version", refResolutionVersion);
|
|
4477
5764
|
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
4478
5765
|
if (!opts.files || filesIndexed >= 50) store.optimize();
|
|
@@ -4555,9 +5842,25 @@ function symbolGraphService(args) {
|
|
|
4555
5842
|
indexStorePool.release(store);
|
|
4556
5843
|
}
|
|
4557
5844
|
}
|
|
5845
|
+
function incomingCallsService(args) {
|
|
5846
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
5847
|
+
try {
|
|
5848
|
+
return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
|
|
5849
|
+
} finally {
|
|
5850
|
+
indexStorePool.release(store);
|
|
5851
|
+
}
|
|
5852
|
+
}
|
|
5853
|
+
function outgoingCallsService(args) {
|
|
5854
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
5855
|
+
try {
|
|
5856
|
+
return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
|
|
5857
|
+
} finally {
|
|
5858
|
+
indexStorePool.release(store);
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
4558
5861
|
|
|
4559
5862
|
// src/codebase-index/project-server-client.ts
|
|
4560
|
-
import { spawn as
|
|
5863
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
4561
5864
|
import * as fs10 from "node:fs";
|
|
4562
5865
|
import * as net from "node:net";
|
|
4563
5866
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -5173,7 +6476,7 @@ var ProjectServerConnection = class {
|
|
|
5173
6476
|
}
|
|
5174
6477
|
const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
|
|
5175
6478
|
if (this.indexDir) args.push("--index-dir", this.indexDir);
|
|
5176
|
-
const child =
|
|
6479
|
+
const child = spawn3(process.execPath, args, {
|
|
5177
6480
|
detached: true,
|
|
5178
6481
|
stdio: "ignore",
|
|
5179
6482
|
windowsHide: true,
|
|
@@ -5456,6 +6759,10 @@ async function callInline(op, args, opts) {
|
|
|
5456
6759
|
return fileGraphService(args);
|
|
5457
6760
|
case "symbolGraph":
|
|
5458
6761
|
return symbolGraphService(args);
|
|
6762
|
+
case "incomingCalls":
|
|
6763
|
+
return incomingCallsService(args);
|
|
6764
|
+
case "outgoingCalls":
|
|
6765
|
+
return outgoingCallsService(args);
|
|
5459
6766
|
default:
|
|
5460
6767
|
throw new Error(`unknown index op: ${String(op)}`);
|
|
5461
6768
|
}
|