@wrongstack/tools 0.299.0 → 0.301.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 +6 -2
- package/dist/batch-tool-use.js +3 -1
- package/dist/browser/index.js +1 -1
- package/dist/builtin.d.ts +3 -2
- package/dist/builtin.js +1781 -376
- package/dist/codebase-index/bm25.d.ts +7 -1
- package/dist/codebase-index/import-extractor.d.ts +39 -0
- package/dist/codebase-index/index.js +1418 -267
- 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 +1402 -249
- package/dist/codebase-index/rs-parser.d.ts +22 -0
- package/dist/codebase-index/schema.d.ts +24 -1
- package/dist/codebase-index/worker.js +1401 -250
- package/dist/codebase-index/writer-graph-helpers.d.ts +17 -5
- 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 +76 -3
- package/dist/exec.js +35 -2
- package/dist/format.js +6 -2
- package/dist/git.js +2 -5
- package/dist/glob.js +2 -2
- package/dist/grep.js +118 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1925 -415
- package/dist/install.js +6 -2
- package/dist/json.js +132 -2
- package/dist/languages/index.js +6 -2
- package/dist/lint.js +6 -2
- package/dist/logs.js +81 -0
- package/dist/next-steps-tool.d.ts +26 -0
- package/dist/outdated.js +6 -2
- package/dist/pack.js +1781 -376
- package/dist/patch.js +206 -45
- package/dist/process-registry.d.ts +6 -0
- package/dist/process-registry.js +6 -2
- package/dist/ps-slash.js +6 -2
- package/dist/read.js +1410 -257
- package/dist/replace.js +81 -0
- package/dist/skill.js +51 -2
- package/dist/test.js +6 -2
- package/dist/tool-help.js +2 -2
- package/dist/tool-search.js +1 -1
- package/dist/tool-tier.d.ts +1 -1
- package/dist/tool-tier.js +1786 -397
- package/dist/tool-use.js +1 -1
- package/dist/tree.js +13 -3
- package/dist/typecheck.js +6 -2
- package/package.json +3 -3
- package/dist/codebase-index/refs-extractor.d.ts +0 -11
|
@@ -31,7 +31,10 @@ function detectLang(file) {
|
|
|
31
31
|
function isIndexablePath(file) {
|
|
32
32
|
return detectLang(file) !== null;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
function languageFamily(lang) {
|
|
35
|
+
return LANG_FAMILY[lang] ?? "other";
|
|
36
|
+
}
|
|
37
|
+
var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
|
|
35
38
|
var init_languages = __esm({
|
|
36
39
|
"src/codebase-index/languages.ts"() {
|
|
37
40
|
"use strict";
|
|
@@ -122,6 +125,52 @@ var init_languages = __esm({
|
|
|
122
125
|
procfile: "other",
|
|
123
126
|
justfile: "other"
|
|
124
127
|
};
|
|
128
|
+
LANG_FAMILY = {
|
|
129
|
+
// Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
|
|
130
|
+
// imports from — and is imported by — plain .ts files.
|
|
131
|
+
ts: "js",
|
|
132
|
+
tsx: "js",
|
|
133
|
+
js: "js",
|
|
134
|
+
jsx: "js",
|
|
135
|
+
vue: "js",
|
|
136
|
+
svelte: "js",
|
|
137
|
+
go: "go",
|
|
138
|
+
py: "py",
|
|
139
|
+
rs: "rs",
|
|
140
|
+
// The JVM resolves across languages: Kotlin and Scala call Java directly.
|
|
141
|
+
java: "jvm",
|
|
142
|
+
kotlin: "jvm",
|
|
143
|
+
scala: "jvm",
|
|
144
|
+
csharp: "dotnet",
|
|
145
|
+
// A .h header is consumed by both C and C++ translation units.
|
|
146
|
+
c: "c",
|
|
147
|
+
cpp: "c",
|
|
148
|
+
ruby: "ruby",
|
|
149
|
+
php: "php",
|
|
150
|
+
swift: "swift",
|
|
151
|
+
dart: "dart",
|
|
152
|
+
elixir: "elixir",
|
|
153
|
+
haskell: "haskell",
|
|
154
|
+
zig: "zig",
|
|
155
|
+
lua: "lua",
|
|
156
|
+
r: "r",
|
|
157
|
+
shell: "shell",
|
|
158
|
+
sql: "sql",
|
|
159
|
+
json: "data",
|
|
160
|
+
yaml: "data",
|
|
161
|
+
toml: "data",
|
|
162
|
+
html: "web",
|
|
163
|
+
css: "web",
|
|
164
|
+
proto: "proto",
|
|
165
|
+
graphql: "graphql",
|
|
166
|
+
md: "other",
|
|
167
|
+
other: "other"
|
|
168
|
+
};
|
|
169
|
+
LANG_FAMILY_ENTRIES = Object.freeze(
|
|
170
|
+
Object.entries(LANG_FAMILY).map(
|
|
171
|
+
([lang, family]) => Object.freeze([lang, family])
|
|
172
|
+
)
|
|
173
|
+
);
|
|
125
174
|
}
|
|
126
175
|
});
|
|
127
176
|
|
|
@@ -286,7 +335,7 @@ function getTypeName(name) {
|
|
|
286
335
|
function deduplicateRefs(refs) {
|
|
287
336
|
const seen = /* @__PURE__ */ new Set();
|
|
288
337
|
return refs.filter((r) => {
|
|
289
|
-
const key = `${r.toName}:${r.callType}:${r.line}`;
|
|
338
|
+
const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
|
|
290
339
|
if (seen.has(key)) return false;
|
|
291
340
|
seen.add(key);
|
|
292
341
|
return true;
|
|
@@ -296,10 +345,16 @@ function getImportSpecifierName(spec) {
|
|
|
296
345
|
return spec.propertyName?.text ?? spec.name.text;
|
|
297
346
|
}
|
|
298
347
|
function emitImportSpecifierRefs(node, refs, lineNum) {
|
|
348
|
+
const module = moduleSpecifierOf(node.moduleSpecifier);
|
|
299
349
|
const clause = node.importClause;
|
|
300
|
-
if (!clause)
|
|
350
|
+
if (!clause) {
|
|
351
|
+
if (module) {
|
|
352
|
+
refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
301
356
|
if (clause.name) {
|
|
302
|
-
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
|
|
357
|
+
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
|
|
303
358
|
}
|
|
304
359
|
const bindings = clause.namedBindings;
|
|
305
360
|
if (!bindings) return;
|
|
@@ -309,26 +364,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
|
|
|
309
364
|
fromId: 0,
|
|
310
365
|
toName: getImportSpecifierName(element),
|
|
311
366
|
callType: "import",
|
|
312
|
-
line: lineNum
|
|
367
|
+
line: lineNum,
|
|
368
|
+
module
|
|
313
369
|
});
|
|
314
370
|
}
|
|
315
371
|
} else if (ts.isNamespaceImport(bindings)) {
|
|
316
|
-
refs.push({
|
|
372
|
+
refs.push({
|
|
373
|
+
fromId: 0,
|
|
374
|
+
toName: bindings.name.text,
|
|
375
|
+
callType: "import",
|
|
376
|
+
line: lineNum,
|
|
377
|
+
module
|
|
378
|
+
});
|
|
317
379
|
}
|
|
318
380
|
}
|
|
381
|
+
function moduleSpecifierOf(node) {
|
|
382
|
+
return node && ts.isStringLiteral(node) ? node.text : void 0;
|
|
383
|
+
}
|
|
319
384
|
function emitExportSpecifierRefs(node, refs, lineNum) {
|
|
385
|
+
const module = moduleSpecifierOf(node.moduleSpecifier);
|
|
320
386
|
const clause = node.exportClause;
|
|
321
387
|
if (clause && ts.isNamespaceExport(clause)) {
|
|
322
|
-
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
|
|
388
|
+
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
|
|
323
389
|
return;
|
|
324
390
|
}
|
|
325
391
|
if (clause && ts.isNamedExports(clause)) {
|
|
326
392
|
for (const element of clause.elements) {
|
|
327
393
|
const originalName = element.propertyName?.text ?? element.name.text;
|
|
328
|
-
refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
|
|
394
|
+
refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
|
|
329
395
|
}
|
|
330
396
|
return;
|
|
331
397
|
}
|
|
398
|
+
if (module) {
|
|
399
|
+
refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
|
|
400
|
+
}
|
|
332
401
|
}
|
|
333
402
|
var ts, tsLoad, kindMapCache;
|
|
334
403
|
var init_ts_parser = __esm({
|
|
@@ -341,21 +410,21 @@ var init_ts_parser = __esm({
|
|
|
341
410
|
});
|
|
342
411
|
|
|
343
412
|
// src/_win32-resolve.ts
|
|
344
|
-
import * as
|
|
345
|
-
import * as
|
|
413
|
+
import * as fs3 from "node:fs";
|
|
414
|
+
import * as path5 from "node:path";
|
|
346
415
|
function resolveWin32Command(cmd) {
|
|
347
416
|
if (process.platform !== "win32") return cmd;
|
|
348
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
417
|
+
if (cmd.includes("/") || cmd.includes("\\") || path5.extname(cmd.replace(/\//g, "\\"))) {
|
|
349
418
|
return cmd;
|
|
350
419
|
}
|
|
351
420
|
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
352
|
-
const pathDirs = (process.env["PATH"] ?? "").split(
|
|
421
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path5.delimiter);
|
|
353
422
|
for (const dir of pathDirs) {
|
|
354
|
-
const base =
|
|
423
|
+
const base = path5.join(dir, cmd);
|
|
355
424
|
for (const ext of pathext) {
|
|
356
425
|
const full = `${base}${ext}`;
|
|
357
426
|
try {
|
|
358
|
-
|
|
427
|
+
fs3.accessSync(full, fs3.constants.X_OK);
|
|
359
428
|
return full;
|
|
360
429
|
} catch {
|
|
361
430
|
}
|
|
@@ -369,6 +438,82 @@ var init_win32_resolve = __esm({
|
|
|
369
438
|
}
|
|
370
439
|
});
|
|
371
440
|
|
|
441
|
+
// src/codebase-index/parser-output.ts
|
|
442
|
+
function coerceSymbols(value) {
|
|
443
|
+
if (!Array.isArray(value)) return [];
|
|
444
|
+
return value.flatMap((entry) => {
|
|
445
|
+
const candidate = entry;
|
|
446
|
+
if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
|
|
447
|
+
return [
|
|
448
|
+
{
|
|
449
|
+
name: candidate.name,
|
|
450
|
+
kind: candidate.kind,
|
|
451
|
+
line: typeof candidate.line === "number" ? candidate.line : 1,
|
|
452
|
+
col: typeof candidate.col === "number" ? candidate.col : 0,
|
|
453
|
+
signature: typeof candidate.signature === "string" ? candidate.signature : "",
|
|
454
|
+
scope: typeof candidate.scope === "string" ? candidate.scope : ""
|
|
455
|
+
}
|
|
456
|
+
];
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function coerceRefs(value, lang) {
|
|
460
|
+
if (!Array.isArray(value)) return [];
|
|
461
|
+
return value.flatMap((entry) => {
|
|
462
|
+
const candidate = entry;
|
|
463
|
+
if (typeof candidate.toName !== "string" || !candidate.toName) return [];
|
|
464
|
+
if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
|
|
465
|
+
const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
|
|
466
|
+
return [
|
|
467
|
+
{
|
|
468
|
+
fromId: 0,
|
|
469
|
+
toName: candidate.toName,
|
|
470
|
+
callType: candidate.callType,
|
|
471
|
+
line: typeof candidate.line === "number" ? candidate.line : 1,
|
|
472
|
+
lang,
|
|
473
|
+
module
|
|
474
|
+
}
|
|
475
|
+
];
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
function parseParserOutput(stdout, lang) {
|
|
479
|
+
const trimmed = stdout.trim();
|
|
480
|
+
if (!trimmed) return { symbols: [], refs: [] };
|
|
481
|
+
let parsed2;
|
|
482
|
+
try {
|
|
483
|
+
parsed2 = JSON.parse(trimmed);
|
|
484
|
+
} catch {
|
|
485
|
+
return { symbols: [], refs: [] };
|
|
486
|
+
}
|
|
487
|
+
if (Array.isArray(parsed2)) return { symbols: coerceSymbols(parsed2), refs: [] };
|
|
488
|
+
const record = parsed2;
|
|
489
|
+
return {
|
|
490
|
+
symbols: coerceSymbols(record.symbols),
|
|
491
|
+
refs: dedupeRefs(coerceRefs(record.refs, lang))
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function dedupeRefs(refs) {
|
|
495
|
+
const seen = /* @__PURE__ */ new Set();
|
|
496
|
+
return refs.filter((ref) => {
|
|
497
|
+
const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
|
|
498
|
+
if (seen.has(key)) return false;
|
|
499
|
+
seen.add(key);
|
|
500
|
+
return true;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
var CALL_TYPES;
|
|
504
|
+
var init_parser_output = __esm({
|
|
505
|
+
"src/codebase-index/parser-output.ts"() {
|
|
506
|
+
"use strict";
|
|
507
|
+
CALL_TYPES = /* @__PURE__ */ new Set([
|
|
508
|
+
"call",
|
|
509
|
+
"type_ref",
|
|
510
|
+
"inherit",
|
|
511
|
+
"implement",
|
|
512
|
+
"import"
|
|
513
|
+
]);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
372
517
|
// src/codebase-index/spawn-gate.ts
|
|
373
518
|
function withSpawnGate(fn) {
|
|
374
519
|
const run = chain.then(fn, fn);
|
|
@@ -394,8 +539,8 @@ __export(go_parser_exports, {
|
|
|
394
539
|
});
|
|
395
540
|
import { spawn } from "node:child_process";
|
|
396
541
|
import * as os from "node:os";
|
|
397
|
-
import * as
|
|
398
|
-
import * as
|
|
542
|
+
import * as path6 from "node:path";
|
|
543
|
+
import * as fs4 from "node:fs/promises";
|
|
399
544
|
async function parseSymbols2(opts) {
|
|
400
545
|
const { file, content, lang } = opts;
|
|
401
546
|
try {
|
|
@@ -403,7 +548,8 @@ async function parseSymbols2(opts) {
|
|
|
403
548
|
if (parsed2.symbols.length > 0) {
|
|
404
549
|
return parsed2;
|
|
405
550
|
}
|
|
406
|
-
|
|
551
|
+
const fallback = fallbackParse(file, content, lang);
|
|
552
|
+
return parsed2.refs?.length ? { ...fallback, refs: parsed2.refs } : fallback;
|
|
407
553
|
} catch {
|
|
408
554
|
return fallbackParse(file, content, lang);
|
|
409
555
|
}
|
|
@@ -467,9 +613,9 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
467
613
|
try {
|
|
468
614
|
let scriptPath = _cachedGoScriptPath;
|
|
469
615
|
if (!scriptPath) {
|
|
470
|
-
const tmpDir = await
|
|
471
|
-
scriptPath =
|
|
472
|
-
await
|
|
616
|
+
const tmpDir = await fs4.mkdtemp(path6.join(os.tmpdir(), "ws-go-parse-"));
|
|
617
|
+
scriptPath = path6.join(tmpDir, "parse.go");
|
|
618
|
+
await fs4.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
473
619
|
_cachedGoScriptPath = scriptPath;
|
|
474
620
|
}
|
|
475
621
|
const goBinary = resolveWin32Command("go");
|
|
@@ -511,8 +657,8 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
511
657
|
if (code !== 0 || !stdout.trim()) {
|
|
512
658
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
513
659
|
}
|
|
514
|
-
const
|
|
515
|
-
const symbols =
|
|
660
|
+
const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
|
|
661
|
+
const symbols = rawSymbols.map((s) => ({
|
|
516
662
|
id: 0,
|
|
517
663
|
lang,
|
|
518
664
|
kind: s.kind,
|
|
@@ -525,7 +671,7 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
525
671
|
scope: s.scope ?? "",
|
|
526
672
|
text: `${s.name} ${s.signature ?? ""}`.trim()
|
|
527
673
|
}));
|
|
528
|
-
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
674
|
+
return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
|
|
529
675
|
} catch {
|
|
530
676
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
531
677
|
}
|
|
@@ -535,6 +681,7 @@ var init_go_parser = __esm({
|
|
|
535
681
|
"src/codebase-index/go-parser.ts"() {
|
|
536
682
|
"use strict";
|
|
537
683
|
init_win32_resolve();
|
|
684
|
+
init_parser_output();
|
|
538
685
|
init_spawn_gate();
|
|
539
686
|
init_languages();
|
|
540
687
|
GO_PARSE_SCRIPT = `
|
|
@@ -548,6 +695,7 @@ import (
|
|
|
548
695
|
"go/token"
|
|
549
696
|
"io"
|
|
550
697
|
"os"
|
|
698
|
+
"strconv"
|
|
551
699
|
"strings"
|
|
552
700
|
)
|
|
553
701
|
|
|
@@ -560,16 +708,34 @@ type Sym struct {
|
|
|
560
708
|
Scope string \`json:"scope"\`
|
|
561
709
|
}
|
|
562
710
|
|
|
711
|
+
// Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
|
|
712
|
+
// yields both. Module is the import path for CallType "import", else empty.
|
|
713
|
+
type Ref struct {
|
|
714
|
+
ToName string \`json:"toName"\`
|
|
715
|
+
CallType string \`json:"callType"\`
|
|
716
|
+
Line int \`json:"line"\`
|
|
717
|
+
Module string \`json:"module"\`
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
type Result struct {
|
|
721
|
+
Symbols []Sym \`json:"symbols"\`
|
|
722
|
+
Refs []Ref \`json:"refs"\`
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
func emptyResult() string {
|
|
726
|
+
return "{\\"symbols\\":[],\\"refs\\":[]}"
|
|
727
|
+
}
|
|
728
|
+
|
|
563
729
|
func main() {
|
|
564
730
|
src, err := io.ReadAll(os.Stdin)
|
|
565
731
|
if err != nil {
|
|
566
|
-
fmt.Print(
|
|
732
|
+
fmt.Print(emptyResult())
|
|
567
733
|
return
|
|
568
734
|
}
|
|
569
735
|
fset := token.NewFileSet()
|
|
570
736
|
node, err := parser.ParseFile(fset, "src.go", src, 0)
|
|
571
737
|
if err != nil {
|
|
572
|
-
fmt.Print(
|
|
738
|
+
fmt.Print(emptyResult())
|
|
573
739
|
return
|
|
574
740
|
}
|
|
575
741
|
|
|
@@ -633,9 +799,43 @@ func main() {
|
|
|
633
799
|
}
|
|
634
800
|
}
|
|
635
801
|
|
|
636
|
-
|
|
802
|
+
refs := []Ref{}
|
|
803
|
+
ast.Inspect(node, func(n ast.Node) bool {
|
|
804
|
+
switch expr := n.(type) {
|
|
805
|
+
case *ast.CallExpr:
|
|
806
|
+
line := fset.Position(expr.Pos()).Line
|
|
807
|
+
switch fun := expr.Fun.(type) {
|
|
808
|
+
case *ast.Ident:
|
|
809
|
+
refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
|
|
810
|
+
case *ast.SelectorExpr:
|
|
811
|
+
// Record the selected name (\`Join\` of \`filepath.Join\`): it is the
|
|
812
|
+
// declared symbol name, so it resolves the same way the TypeScript
|
|
813
|
+
// and Python extractors' call refs do.
|
|
814
|
+
refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
|
|
815
|
+
}
|
|
816
|
+
case *ast.ImportSpec:
|
|
817
|
+
if expr.Path != nil {
|
|
818
|
+
if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
|
|
819
|
+
line := fset.Position(expr.Pos()).Line
|
|
820
|
+
// A Go import names a package, not a symbol; the package's
|
|
821
|
+
// last path segment is the name it is referenced by.
|
|
822
|
+
name := importPath
|
|
823
|
+
if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
|
|
824
|
+
name = importPath[idx+1:]
|
|
825
|
+
}
|
|
826
|
+
refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return true
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
if syms == nil {
|
|
834
|
+
syms = []Sym{}
|
|
835
|
+
}
|
|
836
|
+
data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
|
|
637
837
|
if err != nil {
|
|
638
|
-
fmt.Print(
|
|
838
|
+
fmt.Print(emptyResult())
|
|
639
839
|
return
|
|
640
840
|
}
|
|
641
841
|
fmt.Print(string(data))
|
|
@@ -796,16 +996,23 @@ function looksBinary(content) {
|
|
|
796
996
|
}
|
|
797
997
|
return bad / sample.length > 0.1;
|
|
798
998
|
}
|
|
799
|
-
function
|
|
800
|
-
|
|
801
|
-
let
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
999
|
+
function newlineOffsets2(content) {
|
|
1000
|
+
const offsets = [];
|
|
1001
|
+
for (let i = 0; i < content.length; i++) {
|
|
1002
|
+
if (content.charCodeAt(i) === 10) offsets.push(i);
|
|
1003
|
+
}
|
|
1004
|
+
return offsets;
|
|
1005
|
+
}
|
|
1006
|
+
function lineColAt(offsets, index) {
|
|
1007
|
+
let low = 0;
|
|
1008
|
+
let high = offsets.length;
|
|
1009
|
+
while (low < high) {
|
|
1010
|
+
const mid = low + high >>> 1;
|
|
1011
|
+
if ((offsets[mid] ?? 0) < index) low = mid + 1;
|
|
1012
|
+
else high = mid;
|
|
807
1013
|
}
|
|
808
|
-
|
|
1014
|
+
const lastNl = low > 0 ? offsets[low - 1] : -1;
|
|
1015
|
+
return { line: low + 1, col: index - lastNl };
|
|
809
1016
|
}
|
|
810
1017
|
function parseGeneric(opts) {
|
|
811
1018
|
const { file, lang } = opts;
|
|
@@ -818,6 +1025,7 @@ function parseGeneric(opts) {
|
|
|
818
1025
|
const patterns = patternsFor(lang);
|
|
819
1026
|
const symbols = [];
|
|
820
1027
|
const seen = /* @__PURE__ */ new Set();
|
|
1028
|
+
const nlOffsets = newlineOffsets2(content);
|
|
821
1029
|
for (const pattern of patterns) {
|
|
822
1030
|
const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
|
|
823
1031
|
re.lastIndex = 0;
|
|
@@ -831,7 +1039,7 @@ function parseGeneric(opts) {
|
|
|
831
1039
|
if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
|
|
832
1040
|
continue;
|
|
833
1041
|
}
|
|
834
|
-
const { line, col } = lineColAt(
|
|
1042
|
+
const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
|
|
835
1043
|
const key = `${name}\0${line}\0${pattern.kind}`;
|
|
836
1044
|
if (seen.has(key)) continue;
|
|
837
1045
|
seen.add(key);
|
|
@@ -987,9 +1195,13 @@ var init_generic_parser = __esm({
|
|
|
987
1195
|
],
|
|
988
1196
|
elixir: [
|
|
989
1197
|
{ re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
|
|
990
|
-
|
|
1198
|
+
// Dotted module names must be captured whole: `alias Foo.Bar` resolves
|
|
1199
|
+
// against this symbol, and a `Foo`-only capture never matches it.
|
|
1200
|
+
{ re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
|
|
991
1201
|
],
|
|
992
1202
|
haskell: [
|
|
1203
|
+
// Target of `import Data.List`.
|
|
1204
|
+
{ re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
|
|
993
1205
|
{ re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
|
|
994
1206
|
{ re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
|
|
995
1207
|
{ re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
|
|
@@ -1080,9 +1292,9 @@ __export(py_parser_exports, {
|
|
|
1080
1292
|
parseSymbols: () => parseSymbols4
|
|
1081
1293
|
});
|
|
1082
1294
|
import { spawn as spawn2 } from "node:child_process";
|
|
1083
|
-
import * as
|
|
1295
|
+
import * as fs5 from "node:fs/promises";
|
|
1084
1296
|
import * as os2 from "node:os";
|
|
1085
|
-
import * as
|
|
1297
|
+
import * as path7 from "node:path";
|
|
1086
1298
|
async function parseSymbols4(opts) {
|
|
1087
1299
|
const { file, content, lang } = opts;
|
|
1088
1300
|
try {
|
|
@@ -1160,10 +1372,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
|
1160
1372
|
async function syncPyParse(filePath, content, lang) {
|
|
1161
1373
|
try {
|
|
1162
1374
|
if (!_cachedScriptPath) {
|
|
1163
|
-
const tmpDir =
|
|
1164
|
-
await
|
|
1165
|
-
_cachedScriptPath =
|
|
1166
|
-
await
|
|
1375
|
+
const tmpDir = path7.join(os2.tmpdir(), "ws-py-parse");
|
|
1376
|
+
await fs5.mkdir(tmpDir, { recursive: true });
|
|
1377
|
+
_cachedScriptPath = path7.join(tmpDir, "parse.py");
|
|
1378
|
+
await fs5.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
1167
1379
|
}
|
|
1168
1380
|
cachedPyBinary ??= resolvePython();
|
|
1169
1381
|
const pyBinary = await cachedPyBinary;
|
|
@@ -1177,7 +1389,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1177
1389
|
if (code !== 0 || !stdout.trim()) {
|
|
1178
1390
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1179
1391
|
}
|
|
1180
|
-
const raw =
|
|
1392
|
+
const { symbols: raw, refs } = parseParserOutput(stdout, lang);
|
|
1181
1393
|
const symbols = raw.map((s) => ({
|
|
1182
1394
|
id: 0,
|
|
1183
1395
|
lang,
|
|
@@ -1191,7 +1403,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1191
1403
|
scope: s.scope ?? "",
|
|
1192
1404
|
text: `${s.name} ${s.signature ?? ""}`.trim()
|
|
1193
1405
|
}));
|
|
1194
|
-
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
1406
|
+
return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
|
|
1195
1407
|
} catch {
|
|
1196
1408
|
return null;
|
|
1197
1409
|
}
|
|
@@ -1202,6 +1414,7 @@ var init_py_parser = __esm({
|
|
|
1202
1414
|
"use strict";
|
|
1203
1415
|
init_win32_resolve();
|
|
1204
1416
|
init_generic_parser();
|
|
1417
|
+
init_parser_output();
|
|
1205
1418
|
init_spawn_gate();
|
|
1206
1419
|
init_languages();
|
|
1207
1420
|
PY_PARSE_SCRIPT = `import ast, json, sys, os
|
|
@@ -1263,7 +1476,18 @@ class Sym:
|
|
|
1263
1476
|
def is_private(name):
|
|
1264
1477
|
return name.startswith("__") and not name.endswith("__")
|
|
1265
1478
|
|
|
1479
|
+
def leaf_name(node):
|
|
1480
|
+
# Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
|
|
1481
|
+
# TypeScript and Go extractors record call refs, so resolution behaves the
|
|
1482
|
+
# same across languages.
|
|
1483
|
+
if isinstance(node, ast.Attribute):
|
|
1484
|
+
return node.attr
|
|
1485
|
+
if isinstance(node, ast.Name):
|
|
1486
|
+
return node.id
|
|
1487
|
+
return get_name(node).split(".")[-1]
|
|
1488
|
+
|
|
1266
1489
|
syms = []
|
|
1490
|
+
refs = []
|
|
1267
1491
|
errors = []
|
|
1268
1492
|
|
|
1269
1493
|
try:
|
|
@@ -1271,7 +1495,7 @@ try:
|
|
|
1271
1495
|
tree = ast.parse(source, filename=sys.argv[1])
|
|
1272
1496
|
except Exception as e:
|
|
1273
1497
|
errors.append(str(e))
|
|
1274
|
-
print("[]")
|
|
1498
|
+
print(json.dumps({"symbols": [], "refs": []}))
|
|
1275
1499
|
sys.exit(0)
|
|
1276
1500
|
|
|
1277
1501
|
# Module-level scope
|
|
@@ -1405,7 +1629,42 @@ class ModuleVisitor(ast.NodeVisitor):
|
|
|
1405
1629
|
visitor = ModuleVisitor()
|
|
1406
1630
|
visitor.visit(tree)
|
|
1407
1631
|
|
|
1408
|
-
|
|
1632
|
+
# Refs need a separate full walk: ModuleVisitor deliberately does not descend
|
|
1633
|
+
# into function bodies (it would index locals as symbols), but that is exactly
|
|
1634
|
+
# where the calls are.
|
|
1635
|
+
for node in ast.walk(tree):
|
|
1636
|
+
if isinstance(node, ast.Call):
|
|
1637
|
+
name = leaf_name(node.func)
|
|
1638
|
+
if name:
|
|
1639
|
+
refs.append({"toName": name, "callType": "call", "line": node.lineno})
|
|
1640
|
+
elif isinstance(node, ast.Import):
|
|
1641
|
+
for alias in node.names:
|
|
1642
|
+
refs.append({
|
|
1643
|
+
"toName": alias.name.split(".")[-1],
|
|
1644
|
+
"callType": "import",
|
|
1645
|
+
"line": node.lineno,
|
|
1646
|
+
"module": alias.name,
|
|
1647
|
+
})
|
|
1648
|
+
elif isinstance(node, ast.ImportFrom):
|
|
1649
|
+
# PEP 328: node.level is the number of leading dots. Preserving them is
|
|
1650
|
+
# what lets the resolver walk up from the importing file's package \u2014
|
|
1651
|
+
# dropping them made \`from .foo import X\` indistinguishable from an
|
|
1652
|
+
# absolute \`foo\`.
|
|
1653
|
+
module = ("." * (node.level or 0)) + (node.module or "")
|
|
1654
|
+
for alias in node.names:
|
|
1655
|
+
refs.append({
|
|
1656
|
+
"toName": alias.name,
|
|
1657
|
+
"callType": "import",
|
|
1658
|
+
"line": node.lineno,
|
|
1659
|
+
"module": module,
|
|
1660
|
+
})
|
|
1661
|
+
elif isinstance(node, ast.ClassDef):
|
|
1662
|
+
for base in node.bases:
|
|
1663
|
+
name = leaf_name(base)
|
|
1664
|
+
if name:
|
|
1665
|
+
refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
|
|
1666
|
+
|
|
1667
|
+
print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
|
|
1409
1668
|
`;
|
|
1410
1669
|
_cachedScriptPath = null;
|
|
1411
1670
|
}
|
|
@@ -1418,107 +1677,10 @@ __export(rs_parser_exports, {
|
|
|
1418
1677
|
parseSymbols: () => parseSymbols5
|
|
1419
1678
|
});
|
|
1420
1679
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
1421
|
-
import { execFile, spawn as spawn3 } from "node:child_process";
|
|
1422
|
-
import * as fs5 from "node:fs/promises";
|
|
1423
|
-
import * as path6 from "node:path";
|
|
1424
1680
|
async function parseSymbols5(opts) {
|
|
1425
1681
|
const { file, content, lang } = opts;
|
|
1426
|
-
const nativeAvailable = await checkNativeParser();
|
|
1427
|
-
if (nativeAvailable) {
|
|
1428
|
-
const result = await withSpawnGate(() => tryNativeParse(file, content));
|
|
1429
|
-
if (result) return result;
|
|
1430
|
-
}
|
|
1431
1682
|
return regexParse({ file, content, lang });
|
|
1432
1683
|
}
|
|
1433
|
-
function probe(command, args) {
|
|
1434
|
-
return new Promise((resolve4, reject) => {
|
|
1435
|
-
execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
|
|
1436
|
-
if (error) reject(error);
|
|
1437
|
-
else resolve4();
|
|
1438
|
-
});
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
1441
|
-
function checkNativeParser() {
|
|
1442
|
-
nativeParserAvailability ??= (async () => {
|
|
1443
|
-
try {
|
|
1444
|
-
await probe("rustc", ["--version"]);
|
|
1445
|
-
const toolsDir = path6.join(process.cwd(), "tools");
|
|
1446
|
-
await probe(
|
|
1447
|
-
"cargo",
|
|
1448
|
-
[
|
|
1449
|
-
"metadata",
|
|
1450
|
-
"--no-deps",
|
|
1451
|
-
"--format-version",
|
|
1452
|
-
"1",
|
|
1453
|
-
"--manifest-path",
|
|
1454
|
-
path6.join(toolsDir, "Cargo.toml")
|
|
1455
|
-
]
|
|
1456
|
-
);
|
|
1457
|
-
return true;
|
|
1458
|
-
} catch {
|
|
1459
|
-
return false;
|
|
1460
|
-
}
|
|
1461
|
-
})();
|
|
1462
|
-
return nativeParserAvailability;
|
|
1463
|
-
}
|
|
1464
|
-
async function tryNativeParse(file, content) {
|
|
1465
|
-
try {
|
|
1466
|
-
const toolsDir = path6.join(process.cwd(), "tools");
|
|
1467
|
-
const crateDir = path6.join(toolsDir, "syn-parser");
|
|
1468
|
-
const tmpFile = path6.join(crateDir, "src", "input.rs");
|
|
1469
|
-
await fs5.writeFile(tmpFile, content, "utf8");
|
|
1470
|
-
const cargoBinary = resolveWin32Command("cargo");
|
|
1471
|
-
const result = await new Promise(
|
|
1472
|
-
(resolve4, reject) => {
|
|
1473
|
-
let settled = false;
|
|
1474
|
-
const proc = spawn3(
|
|
1475
|
-
cargoBinary,
|
|
1476
|
-
["run", "--manifest-path", path6.join(toolsDir, "Cargo.toml")],
|
|
1477
|
-
{
|
|
1478
|
-
cwd: process.cwd(),
|
|
1479
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1480
|
-
windowsHide: true
|
|
1481
|
-
}
|
|
1482
|
-
);
|
|
1483
|
-
proc.on("error", (err) => {
|
|
1484
|
-
if (settled) return;
|
|
1485
|
-
settled = true;
|
|
1486
|
-
reject(err);
|
|
1487
|
-
});
|
|
1488
|
-
let stdout2 = "";
|
|
1489
|
-
proc.stdout?.on("data", (chunk) => {
|
|
1490
|
-
stdout2 += chunk.toString();
|
|
1491
|
-
});
|
|
1492
|
-
proc.stderr?.resume();
|
|
1493
|
-
const timer = setTimeout(() => {
|
|
1494
|
-
if (settled) return;
|
|
1495
|
-
settled = true;
|
|
1496
|
-
proc.kill("SIGKILL");
|
|
1497
|
-
reject(new Error("timeout"));
|
|
1498
|
-
}, 15e3);
|
|
1499
|
-
timer.unref?.();
|
|
1500
|
-
proc.on("close", (c) => {
|
|
1501
|
-
if (settled) return;
|
|
1502
|
-
settled = true;
|
|
1503
|
-
clearTimeout(timer);
|
|
1504
|
-
resolve4({ code: c, stdout: stdout2 });
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
);
|
|
1508
|
-
const { code, stdout } = result;
|
|
1509
|
-
if (code === 0 && stdout.trim()) {
|
|
1510
|
-
const symbols = JSON.parse(stdout.trim());
|
|
1511
|
-
return {
|
|
1512
|
-
file,
|
|
1513
|
-
lang: "rs",
|
|
1514
|
-
symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
|
|
1515
|
-
mtimeMs: Date.now()
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
} catch {
|
|
1519
|
-
}
|
|
1520
|
-
return null;
|
|
1521
|
-
}
|
|
1522
1684
|
function regexParse(opts) {
|
|
1523
1685
|
const { file, content, lang } = opts;
|
|
1524
1686
|
const symbols = [];
|
|
@@ -1574,12 +1736,10 @@ function regexParse(opts) {
|
|
|
1574
1736
|
});
|
|
1575
1737
|
return { file, lang, symbols: deduped, mtimeMs: Date.now() };
|
|
1576
1738
|
}
|
|
1577
|
-
var
|
|
1739
|
+
var RS_PATTERNS;
|
|
1578
1740
|
var init_rs_parser = __esm({
|
|
1579
1741
|
"src/codebase-index/rs-parser.ts"() {
|
|
1580
1742
|
"use strict";
|
|
1581
|
-
init_win32_resolve();
|
|
1582
|
-
init_spawn_gate();
|
|
1583
1743
|
init_languages();
|
|
1584
1744
|
RS_PATTERNS = [
|
|
1585
1745
|
{ regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
|
|
@@ -1602,7 +1762,7 @@ __export(json_parser_exports, {
|
|
|
1602
1762
|
parseSymbols: () => parseSymbols6
|
|
1603
1763
|
});
|
|
1604
1764
|
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
1605
|
-
import * as
|
|
1765
|
+
import * as path8 from "node:path";
|
|
1606
1766
|
function parseSymbols6(opts) {
|
|
1607
1767
|
const { file, content, lang } = opts;
|
|
1608
1768
|
try {
|
|
@@ -1614,7 +1774,7 @@ function parseSymbols6(opts) {
|
|
|
1614
1774
|
function regexParse2(opts) {
|
|
1615
1775
|
const { file, content, lang } = opts;
|
|
1616
1776
|
const symbols = [];
|
|
1617
|
-
const basename6 =
|
|
1777
|
+
const basename6 = path8.basename(file).toLowerCase();
|
|
1618
1778
|
const isPackageJson = basename6 === "package.json";
|
|
1619
1779
|
const isTsconfig = basename6 === "tsconfig.json" || basename6 === "tsconfig.build.json";
|
|
1620
1780
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
@@ -1640,11 +1800,11 @@ function regexParse2(opts) {
|
|
|
1640
1800
|
const line = lineFromOffset(offset);
|
|
1641
1801
|
symbols.push(
|
|
1642
1802
|
makeSymbol({
|
|
1643
|
-
name:
|
|
1803
|
+
name: path8.basename(file),
|
|
1644
1804
|
kind: "object",
|
|
1645
1805
|
line,
|
|
1646
1806
|
col: 0,
|
|
1647
|
-
signature: `"${
|
|
1807
|
+
signature: `"${path8.basename(file)}" = { ... }`,
|
|
1648
1808
|
file,
|
|
1649
1809
|
lang
|
|
1650
1810
|
})
|
|
@@ -1985,7 +2145,7 @@ import {
|
|
|
1985
2145
|
|
|
1986
2146
|
// src/codebase-index/indexer.ts
|
|
1987
2147
|
import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
|
|
1988
|
-
import { execFile
|
|
2148
|
+
import { execFile } from "node:child_process";
|
|
1989
2149
|
import * as fs8 from "node:fs/promises";
|
|
1990
2150
|
import { availableParallelism } from "node:os";
|
|
1991
2151
|
import * as path11 from "node:path";
|
|
@@ -2053,8 +2213,738 @@ async function loadGitignoreMatcher(projectRoot2) {
|
|
|
2053
2213
|
// src/codebase-index/indexer.ts
|
|
2054
2214
|
init_languages();
|
|
2055
2215
|
|
|
2216
|
+
// src/codebase-index/module-resolver.ts
|
|
2217
|
+
init_languages();
|
|
2218
|
+
import * as path4 from "node:path";
|
|
2219
|
+
|
|
2220
|
+
// src/codebase-index/module-roots.ts
|
|
2221
|
+
init_languages();
|
|
2222
|
+
import * as fs2 from "node:fs/promises";
|
|
2223
|
+
import * as path3 from "node:path";
|
|
2224
|
+
function toPortablePath(file) {
|
|
2225
|
+
return file.replace(/\\/g, "/");
|
|
2226
|
+
}
|
|
2227
|
+
async function readTextIfPresent(file) {
|
|
2228
|
+
try {
|
|
2229
|
+
return await fs2.readFile(file, "utf8");
|
|
2230
|
+
} catch {
|
|
2231
|
+
return void 0;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
function parsePackageJsonName(source) {
|
|
2235
|
+
try {
|
|
2236
|
+
const parsed2 = JSON.parse(source);
|
|
2237
|
+
return typeof parsed2.name === "string" && parsed2.name ? parsed2.name : void 0;
|
|
2238
|
+
} catch {
|
|
2239
|
+
return void 0;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
function parseGoModulePath(source) {
|
|
2243
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
2244
|
+
const line = rawLine.replace(/\/\/.*$/, "").trim();
|
|
2245
|
+
const match = /^module\s+(\S+)/.exec(line);
|
|
2246
|
+
if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
|
|
2247
|
+
}
|
|
2248
|
+
return void 0;
|
|
2249
|
+
}
|
|
2250
|
+
function parseTomlTableName(source, tables) {
|
|
2251
|
+
let current = "";
|
|
2252
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
2253
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
2254
|
+
if (line.startsWith("[[")) {
|
|
2255
|
+
current = "\0";
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
const table = /^\[([^\]]+)\]$/.exec(line);
|
|
2259
|
+
if (table?.[1]) {
|
|
2260
|
+
current = table[1].trim();
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
if (!tables.includes(current)) continue;
|
|
2264
|
+
const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
|
|
2265
|
+
if (match?.[1]) return match[1];
|
|
2266
|
+
}
|
|
2267
|
+
return void 0;
|
|
2268
|
+
}
|
|
2269
|
+
function parsePomArtifactId(source) {
|
|
2270
|
+
const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
2271
|
+
return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
|
|
2272
|
+
}
|
|
2273
|
+
var LANGS_BY_KIND = {
|
|
2274
|
+
npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
|
|
2275
|
+
cargo: ["rs"],
|
|
2276
|
+
go: ["go"],
|
|
2277
|
+
python: ["py"],
|
|
2278
|
+
maven: ["java", "kotlin", "scala"],
|
|
2279
|
+
gradle: ["java", "kotlin", "scala"],
|
|
2280
|
+
dotnet: ["csharp"]
|
|
2281
|
+
};
|
|
2282
|
+
function ancestorsOf(dir, stopAt) {
|
|
2283
|
+
const out = [];
|
|
2284
|
+
let current = dir;
|
|
2285
|
+
for (; ; ) {
|
|
2286
|
+
out.push(current);
|
|
2287
|
+
if (current === stopAt || current.length <= stopAt.length) break;
|
|
2288
|
+
const parent = path3.posix.dirname(current);
|
|
2289
|
+
if (parent === current) break;
|
|
2290
|
+
current = parent;
|
|
2291
|
+
}
|
|
2292
|
+
return out;
|
|
2293
|
+
}
|
|
2294
|
+
var MARKER_PROBES = [
|
|
2295
|
+
{
|
|
2296
|
+
kind: "npm",
|
|
2297
|
+
file: "package.json",
|
|
2298
|
+
build: (dir, source) => {
|
|
2299
|
+
const name = parsePackageJsonName(source) ?? path3.posix.basename(dir);
|
|
2300
|
+
return { name, importPath: name, sourceRoots: [dir] };
|
|
2301
|
+
}
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
kind: "cargo",
|
|
2305
|
+
file: "Cargo.toml",
|
|
2306
|
+
build: (dir, source) => {
|
|
2307
|
+
const name = parseTomlTableName(source, ["package"]);
|
|
2308
|
+
if (!name) return void 0;
|
|
2309
|
+
return {
|
|
2310
|
+
name: `crate:${name}`,
|
|
2311
|
+
// Rust paths use underscores where crate names often use dashes.
|
|
2312
|
+
importPath: name.replace(/-/g, "_"),
|
|
2313
|
+
sourceRoots: [path3.posix.join(dir, "src")]
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
kind: "go",
|
|
2319
|
+
file: "go.mod",
|
|
2320
|
+
build: (dir, source) => {
|
|
2321
|
+
const modulePath = parseGoModulePath(source);
|
|
2322
|
+
if (!modulePath) return void 0;
|
|
2323
|
+
return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
|
|
2324
|
+
}
|
|
2325
|
+
},
|
|
2326
|
+
{
|
|
2327
|
+
kind: "python",
|
|
2328
|
+
file: "pyproject.toml",
|
|
2329
|
+
build: (dir, source) => {
|
|
2330
|
+
const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path3.posix.basename(dir);
|
|
2331
|
+
return {
|
|
2332
|
+
name: `py:${name}`,
|
|
2333
|
+
importPath: void 0,
|
|
2334
|
+
// `src/` layout is the packaging-guide default; the root itself covers
|
|
2335
|
+
// the flat layout. Both are probed, missing ones simply never match.
|
|
2336
|
+
sourceRoots: [path3.posix.join(dir, "src"), dir]
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
},
|
|
2340
|
+
{
|
|
2341
|
+
kind: "python",
|
|
2342
|
+
file: "setup.py",
|
|
2343
|
+
build: (dir) => ({
|
|
2344
|
+
name: `py:${path3.posix.basename(dir)}`,
|
|
2345
|
+
importPath: void 0,
|
|
2346
|
+
sourceRoots: [path3.posix.join(dir, "src"), dir]
|
|
2347
|
+
})
|
|
2348
|
+
},
|
|
2349
|
+
{
|
|
2350
|
+
kind: "maven",
|
|
2351
|
+
file: "pom.xml",
|
|
2352
|
+
build: (dir, source) => {
|
|
2353
|
+
const artifactId = parsePomArtifactId(source) ?? path3.posix.basename(dir);
|
|
2354
|
+
return {
|
|
2355
|
+
name: `mvn:${artifactId}`,
|
|
2356
|
+
importPath: void 0,
|
|
2357
|
+
sourceRoots: [
|
|
2358
|
+
path3.posix.join(dir, "src/main/java"),
|
|
2359
|
+
path3.posix.join(dir, "src/main/kotlin"),
|
|
2360
|
+
path3.posix.join(dir, "src/main/scala"),
|
|
2361
|
+
path3.posix.join(dir, "src/test/java")
|
|
2362
|
+
]
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
},
|
|
2366
|
+
{
|
|
2367
|
+
kind: "gradle",
|
|
2368
|
+
file: "build.gradle",
|
|
2369
|
+
build: (dir) => buildGradleRoot(dir)
|
|
2370
|
+
},
|
|
2371
|
+
{
|
|
2372
|
+
kind: "gradle",
|
|
2373
|
+
file: "build.gradle.kts",
|
|
2374
|
+
build: (dir) => buildGradleRoot(dir)
|
|
2375
|
+
}
|
|
2376
|
+
];
|
|
2377
|
+
function buildGradleRoot(dir) {
|
|
2378
|
+
return {
|
|
2379
|
+
name: `gradle:${path3.posix.basename(dir)}`,
|
|
2380
|
+
importPath: void 0,
|
|
2381
|
+
sourceRoots: [
|
|
2382
|
+
path3.posix.join(dir, "src/main/java"),
|
|
2383
|
+
path3.posix.join(dir, "src/main/kotlin"),
|
|
2384
|
+
path3.posix.join(dir, "src/main/scala")
|
|
2385
|
+
]
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
async function probeDotnetRoot(dir) {
|
|
2389
|
+
let entries;
|
|
2390
|
+
try {
|
|
2391
|
+
entries = await fs2.readdir(dir);
|
|
2392
|
+
} catch {
|
|
2393
|
+
return void 0;
|
|
2394
|
+
}
|
|
2395
|
+
const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
|
|
2396
|
+
if (!project) return void 0;
|
|
2397
|
+
const name = project.slice(0, -".csproj".length);
|
|
2398
|
+
return {
|
|
2399
|
+
dir,
|
|
2400
|
+
kind: "dotnet",
|
|
2401
|
+
name: `csproj:${name}`,
|
|
2402
|
+
importPath: void 0,
|
|
2403
|
+
sourceRoots: [dir]
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
async function detectModuleRoots(projectRoot2, files) {
|
|
2407
|
+
const root = toPortablePath(projectRoot2).replace(/\/+$/, "");
|
|
2408
|
+
const langsByDir = /* @__PURE__ */ new Map();
|
|
2409
|
+
for (const file of files) {
|
|
2410
|
+
const portable = toPortablePath(file);
|
|
2411
|
+
const lang = detectLang(portable);
|
|
2412
|
+
if (!lang) continue;
|
|
2413
|
+
const dir = path3.posix.dirname(portable);
|
|
2414
|
+
let langs = langsByDir.get(dir);
|
|
2415
|
+
if (!langs) {
|
|
2416
|
+
langs = /* @__PURE__ */ new Set();
|
|
2417
|
+
langsByDir.set(dir, langs);
|
|
2418
|
+
}
|
|
2419
|
+
langs.add(lang);
|
|
2420
|
+
}
|
|
2421
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2422
|
+
for (const [dir, langs] of langsByDir) {
|
|
2423
|
+
for (const ancestor of ancestorsOf(dir, root)) {
|
|
2424
|
+
let merged = candidates.get(ancestor);
|
|
2425
|
+
if (!merged) {
|
|
2426
|
+
merged = /* @__PURE__ */ new Set();
|
|
2427
|
+
candidates.set(ancestor, merged);
|
|
2428
|
+
}
|
|
2429
|
+
for (const lang of langs) merged.add(lang);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
const roots = [];
|
|
2433
|
+
await Promise.all(
|
|
2434
|
+
[...candidates].map(async ([dir, langs]) => {
|
|
2435
|
+
for (const probe of MARKER_PROBES) {
|
|
2436
|
+
if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
|
|
2437
|
+
const source = await readTextIfPresent(path3.posix.join(dir, probe.file));
|
|
2438
|
+
if (source === void 0) continue;
|
|
2439
|
+
const built = probe.build(dir, source);
|
|
2440
|
+
if (built) roots.push({ dir, kind: probe.kind, ...built });
|
|
2441
|
+
}
|
|
2442
|
+
if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
|
|
2443
|
+
const dotnet = await probeDotnetRoot(dir);
|
|
2444
|
+
if (dotnet) roots.push(dotnet);
|
|
2445
|
+
}
|
|
2446
|
+
})
|
|
2447
|
+
);
|
|
2448
|
+
roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
|
|
2449
|
+
return { projectRoot: root, roots };
|
|
2450
|
+
}
|
|
2451
|
+
function findOwningRoot(structure, file, kinds) {
|
|
2452
|
+
const portable = toPortablePath(file);
|
|
2453
|
+
for (const root of structure.roots) {
|
|
2454
|
+
if (kinds && !kinds.includes(root.kind)) continue;
|
|
2455
|
+
if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
|
|
2456
|
+
}
|
|
2457
|
+
return void 0;
|
|
2458
|
+
}
|
|
2459
|
+
function derivePackageFromLayout(filePath) {
|
|
2460
|
+
const portable = toPortablePath(filePath);
|
|
2461
|
+
const packagesIdx = portable.indexOf("/packages/");
|
|
2462
|
+
if (packagesIdx !== -1) {
|
|
2463
|
+
const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
|
|
2464
|
+
if (segment) return `@wrongstack/${segment}`;
|
|
2465
|
+
}
|
|
2466
|
+
const appsIdx = portable.indexOf("/apps/");
|
|
2467
|
+
if (appsIdx !== -1) {
|
|
2468
|
+
const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
|
|
2469
|
+
if (segment) return `app:${segment}`;
|
|
2470
|
+
}
|
|
2471
|
+
return void 0;
|
|
2472
|
+
}
|
|
2473
|
+
function pythonPackageLabel(structure, file, initDirs) {
|
|
2474
|
+
const portable = toPortablePath(file);
|
|
2475
|
+
const dir = path3.posix.dirname(portable);
|
|
2476
|
+
if (!initDirs.has(dir)) return void 0;
|
|
2477
|
+
const segments = [];
|
|
2478
|
+
let current = dir;
|
|
2479
|
+
while (initDirs.has(current) && current.length > structure.projectRoot.length) {
|
|
2480
|
+
segments.unshift(path3.posix.basename(current));
|
|
2481
|
+
current = path3.posix.dirname(current);
|
|
2482
|
+
}
|
|
2483
|
+
return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
|
|
2484
|
+
}
|
|
2485
|
+
function assignPackageLabels(structure, files) {
|
|
2486
|
+
const initDirs = /* @__PURE__ */ new Set();
|
|
2487
|
+
for (const file of files) {
|
|
2488
|
+
const portable = toPortablePath(file);
|
|
2489
|
+
if (path3.posix.basename(portable) === "__init__.py") {
|
|
2490
|
+
initDirs.add(path3.posix.dirname(portable));
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
const labels = /* @__PURE__ */ new Map();
|
|
2494
|
+
for (const file of files) {
|
|
2495
|
+
const portable = toPortablePath(file);
|
|
2496
|
+
const lang = detectLang(portable);
|
|
2497
|
+
if (lang === "go") {
|
|
2498
|
+
const owner2 = findOwningRoot(structure, portable, ["go"]);
|
|
2499
|
+
const dir = path3.posix.dirname(portable);
|
|
2500
|
+
if (owner2?.importPath) {
|
|
2501
|
+
const relative3 = path3.posix.relative(owner2.dir, dir);
|
|
2502
|
+
labels.set(file, relative3 ? `${owner2.importPath}/${relative3}` : owner2.importPath);
|
|
2503
|
+
} else {
|
|
2504
|
+
labels.set(file, `go:${path3.posix.relative(structure.projectRoot, dir) || "."}`);
|
|
2505
|
+
}
|
|
2506
|
+
continue;
|
|
2507
|
+
}
|
|
2508
|
+
if (lang === "py") {
|
|
2509
|
+
const dotted = pythonPackageLabel(structure, portable, initDirs);
|
|
2510
|
+
if (dotted) {
|
|
2511
|
+
labels.set(file, dotted);
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
const owner = findOwningRoot(structure, portable);
|
|
2516
|
+
const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
|
|
2517
|
+
labels.set(file, label);
|
|
2518
|
+
}
|
|
2519
|
+
return labels;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// src/codebase-index/module-resolver.ts
|
|
2523
|
+
var EXTENSIONS = {
|
|
2524
|
+
js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
|
|
2525
|
+
py: [".py", ".pyi"],
|
|
2526
|
+
rs: [".rs"],
|
|
2527
|
+
jvm: [".java", ".kt", ".scala"],
|
|
2528
|
+
c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
|
|
2529
|
+
ruby: [".rb"],
|
|
2530
|
+
go: [".go"]
|
|
2531
|
+
};
|
|
2532
|
+
var DIRECTORY_ENTRIES = {
|
|
2533
|
+
js: ["index"],
|
|
2534
|
+
py: ["__init__"],
|
|
2535
|
+
rs: ["mod"],
|
|
2536
|
+
ruby: ["index"]
|
|
2537
|
+
};
|
|
2538
|
+
function normalizeNamespace(value) {
|
|
2539
|
+
return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
|
|
2540
|
+
}
|
|
2541
|
+
var ModuleResolver = class {
|
|
2542
|
+
structure;
|
|
2543
|
+
/** Lowercased portable path → the path as indexed (case is preserved). */
|
|
2544
|
+
byPath;
|
|
2545
|
+
/** Lowercased portable directory → files directly inside it, as indexed. */
|
|
2546
|
+
byDir;
|
|
2547
|
+
/** Normalized namespace → the file declaring it (first by path, stable). */
|
|
2548
|
+
byNamespace;
|
|
2549
|
+
constructor(structure, files, namespaces = []) {
|
|
2550
|
+
this.structure = structure;
|
|
2551
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
2552
|
+
this.byDir = /* @__PURE__ */ new Map();
|
|
2553
|
+
this.byNamespace = /* @__PURE__ */ new Map();
|
|
2554
|
+
const dirsByKey = /* @__PURE__ */ new Map();
|
|
2555
|
+
for (const file of files) {
|
|
2556
|
+
const portable = toPortablePath(file);
|
|
2557
|
+
const pathKey = portable.toLowerCase();
|
|
2558
|
+
const priorPath = this.byPath.get(pathKey);
|
|
2559
|
+
if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
|
|
2560
|
+
else this.byPath.set(pathKey, file);
|
|
2561
|
+
const dir = path4.posix.dirname(portable);
|
|
2562
|
+
const dirKey = dir.toLowerCase();
|
|
2563
|
+
const knownDir = dirsByKey.get(dirKey);
|
|
2564
|
+
if (knownDir === void 0) {
|
|
2565
|
+
dirsByKey.set(dirKey, dir);
|
|
2566
|
+
this.byDir.set(dirKey, [file]);
|
|
2567
|
+
} else if (knownDir === dir) {
|
|
2568
|
+
this.byDir.get(dirKey)?.push(file);
|
|
2569
|
+
} else {
|
|
2570
|
+
dirsByKey.delete(dirKey);
|
|
2571
|
+
this.byDir.delete(dirKey);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
for (const { name, file } of namespaces) {
|
|
2575
|
+
const lang = detectLang(file);
|
|
2576
|
+
if (!lang) continue;
|
|
2577
|
+
const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
|
|
2578
|
+
if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
|
|
2579
|
+
this.byNamespace.set(key, file);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Resolve `specifier` as written in `fromFile`.
|
|
2585
|
+
* Returns the indexed target path, or `undefined` when it is external or
|
|
2586
|
+
* cannot be located.
|
|
2587
|
+
*/
|
|
2588
|
+
resolve(fromFile, lang, specifier) {
|
|
2589
|
+
const spec = specifier.trim().replace(/\\/g, "/");
|
|
2590
|
+
if (!spec) return void 0;
|
|
2591
|
+
const from = toPortablePath(fromFile);
|
|
2592
|
+
switch (languageFamily(lang)) {
|
|
2593
|
+
case "js":
|
|
2594
|
+
return this.resolveJs(from, spec);
|
|
2595
|
+
case "go":
|
|
2596
|
+
return this.resolveGo(spec);
|
|
2597
|
+
case "py":
|
|
2598
|
+
return this.resolvePython(from, spec);
|
|
2599
|
+
case "rs":
|
|
2600
|
+
return this.resolveRust(from, spec);
|
|
2601
|
+
case "jvm":
|
|
2602
|
+
return this.resolveJvm(spec);
|
|
2603
|
+
case "c":
|
|
2604
|
+
return this.resolveInclude(from, spec);
|
|
2605
|
+
case "ruby":
|
|
2606
|
+
return this.resolveRuby(from, spec);
|
|
2607
|
+
case "dotnet":
|
|
2608
|
+
case "php":
|
|
2609
|
+
case "elixir":
|
|
2610
|
+
case "haskell":
|
|
2611
|
+
return this.resolveNamespace(lang, spec);
|
|
2612
|
+
default:
|
|
2613
|
+
return void 0;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Resolve a namespace specifier to the file declaring it.
|
|
2618
|
+
*
|
|
2619
|
+
* Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
|
|
2620
|
+
* names a namespace outright, while PHP's `use App\Models\User` names a
|
|
2621
|
+
* *class* inside `App\Models`, so the prefix is what was declared.
|
|
2622
|
+
*/
|
|
2623
|
+
resolveNamespace(lang, spec) {
|
|
2624
|
+
const family = languageFamily(lang);
|
|
2625
|
+
const normalized = normalizeNamespace(spec);
|
|
2626
|
+
const exact = this.byNamespace.get(`${family}:${normalized}`);
|
|
2627
|
+
if (exact) return exact;
|
|
2628
|
+
const segments = normalized.split(".").filter(Boolean);
|
|
2629
|
+
if (segments.length < 2) return void 0;
|
|
2630
|
+
return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
|
|
2631
|
+
}
|
|
2632
|
+
// ─── Lookup primitives ──────────────────────────────────────────────────────
|
|
2633
|
+
lookup(candidate) {
|
|
2634
|
+
return this.byPath.get(path4.posix.normalize(candidate).toLowerCase());
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Try `base` verbatim, then `base` + each extension, then each directory
|
|
2638
|
+
* entry point inside `base`.
|
|
2639
|
+
*/
|
|
2640
|
+
lookupWithExtensions(base, family) {
|
|
2641
|
+
const direct = this.lookup(base);
|
|
2642
|
+
if (direct) return direct;
|
|
2643
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
2644
|
+
const suffix = path4.posix.extname(base);
|
|
2645
|
+
const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
|
|
2646
|
+
for (const ext of extensions) {
|
|
2647
|
+
const hit = this.lookup(`${stem}${ext}`);
|
|
2648
|
+
if (hit) return hit;
|
|
2649
|
+
}
|
|
2650
|
+
for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
|
|
2651
|
+
for (const ext of extensions) {
|
|
2652
|
+
const hit = this.lookup(path4.posix.join(base, `${entry}${ext}`));
|
|
2653
|
+
if (hit) return hit;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return void 0;
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* A representative indexed file inside `dir`, for ecosystems whose import
|
|
2660
|
+
* unit is a directory rather than a file (Go packages, JVM wildcard imports).
|
|
2661
|
+
*
|
|
2662
|
+
* The choice is deterministic — a file named after the directory, else the
|
|
2663
|
+
* first by name — so the same import always produces the same edge. Package
|
|
2664
|
+
* grouping is unaffected either way: every file in the directory carries the
|
|
2665
|
+
* same package label, so the package-level edge is exact regardless of which
|
|
2666
|
+
* member represents it.
|
|
2667
|
+
*/
|
|
2668
|
+
representativeIn(dir, family) {
|
|
2669
|
+
const members = this.byDir.get(path4.posix.normalize(dir).toLowerCase());
|
|
2670
|
+
if (!members?.length) return void 0;
|
|
2671
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
2672
|
+
const eligible = members.filter((file) => extensions.includes(path4.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
|
|
2673
|
+
if (eligible.length === 0) return void 0;
|
|
2674
|
+
const base = path4.posix.basename(path4.posix.normalize(dir)).toLowerCase();
|
|
2675
|
+
const named = eligible.find(
|
|
2676
|
+
(file) => path4.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
|
|
2677
|
+
);
|
|
2678
|
+
return named ?? eligible[0];
|
|
2679
|
+
}
|
|
2680
|
+
// ─── Per-family resolution ──────────────────────────────────────────────────
|
|
2681
|
+
/** Relative specifiers, then workspace package names and their subpaths. */
|
|
2682
|
+
resolveJs(fromFile, spec) {
|
|
2683
|
+
if (spec.startsWith(".")) {
|
|
2684
|
+
const absolute = path4.posix.join(path4.posix.dirname(fromFile), spec);
|
|
2685
|
+
return this.lookupWithExtensions(absolute, "js");
|
|
2686
|
+
}
|
|
2687
|
+
const owner = this.structure.roots.find(
|
|
2688
|
+
(root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
2689
|
+
);
|
|
2690
|
+
if (!owner?.importPath) return void 0;
|
|
2691
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
2692
|
+
if (!subpath) {
|
|
2693
|
+
return this.lookupWithExtensions(path4.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "index"), "js");
|
|
2694
|
+
}
|
|
2695
|
+
return this.lookupWithExtensions(path4.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "src", subpath), "js");
|
|
2696
|
+
}
|
|
2697
|
+
/** Go import paths are absolute module paths; a package is a directory. */
|
|
2698
|
+
resolveGo(spec) {
|
|
2699
|
+
const owner = this.structure.roots.find(
|
|
2700
|
+
(root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
2701
|
+
);
|
|
2702
|
+
if (!owner?.importPath) return void 0;
|
|
2703
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
2704
|
+
return this.representativeIn(path4.posix.join(owner.dir, subpath), "go");
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
|
|
2708
|
+
* leading dots the extractor preserves (`.sibling`, `..parent.mod`).
|
|
2709
|
+
*/
|
|
2710
|
+
resolvePython(fromFile, spec) {
|
|
2711
|
+
const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
|
|
2712
|
+
if (leadingDots > 0) {
|
|
2713
|
+
let base = path4.posix.dirname(fromFile);
|
|
2714
|
+
for (let i = 1; i < leadingDots; i++) base = path4.posix.dirname(base);
|
|
2715
|
+
const rest = spec.slice(leadingDots).split(".").filter(Boolean);
|
|
2716
|
+
return this.lookupWithExtensions(path4.posix.join(base, ...rest), "py");
|
|
2717
|
+
}
|
|
2718
|
+
const segments = spec.split(".").filter(Boolean);
|
|
2719
|
+
if (segments.length === 0) return void 0;
|
|
2720
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
|
|
2721
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
2722
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, ...segments), "py");
|
|
2723
|
+
if (hit) return hit;
|
|
2724
|
+
if (segments.length > 1) {
|
|
2725
|
+
const parent = this.lookupWithExtensions(
|
|
2726
|
+
path4.posix.join(base, ...segments.slice(0, -1)),
|
|
2727
|
+
"py"
|
|
2728
|
+
);
|
|
2729
|
+
if (parent) return parent;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
return void 0;
|
|
2733
|
+
}
|
|
2734
|
+
/** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
|
|
2735
|
+
resolveRust(fromFile, spec) {
|
|
2736
|
+
const segments = spec.split("::").filter(Boolean);
|
|
2737
|
+
if (segments.length === 0) return void 0;
|
|
2738
|
+
const head = segments[0];
|
|
2739
|
+
if (head === "self" || head === "super") {
|
|
2740
|
+
let base = path4.posix.dirname(fromFile);
|
|
2741
|
+
for (const segment of segments) {
|
|
2742
|
+
if (segment === "super") base = path4.posix.dirname(base);
|
|
2743
|
+
else if (segment !== "self") break;
|
|
2744
|
+
}
|
|
2745
|
+
const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
|
|
2746
|
+
return this.lookupWithExtensions(path4.posix.join(base, ...rest2), "rs");
|
|
2747
|
+
}
|
|
2748
|
+
const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
|
|
2749
|
+
const crate = head === "crate" ? owningCrate : this.structure.roots.find(
|
|
2750
|
+
(root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
|
|
2751
|
+
);
|
|
2752
|
+
if (!crate) {
|
|
2753
|
+
return this.lookupWithExtensions(
|
|
2754
|
+
path4.posix.join(path4.posix.dirname(fromFile), ...segments),
|
|
2755
|
+
"rs"
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
const rest = segments.slice(1);
|
|
2759
|
+
for (const base of crate.sourceRoots) {
|
|
2760
|
+
const parent = rest.length > 1 ? this.lookupWithExtensions(path4.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
|
|
2761
|
+
const exact = this.lookupWithExtensions(path4.posix.join(base, ...rest), "rs");
|
|
2762
|
+
const hit = exact ?? parent ?? this.lookupWithExtensions(path4.posix.join(base, "lib"), "rs");
|
|
2763
|
+
if (hit) return hit;
|
|
2764
|
+
}
|
|
2765
|
+
return void 0;
|
|
2766
|
+
}
|
|
2767
|
+
/** `com.example.Thing` and `com.example.*` against JVM source roots. */
|
|
2768
|
+
resolveJvm(spec) {
|
|
2769
|
+
const segments = spec.split(".").filter(Boolean);
|
|
2770
|
+
if (segments.length === 0) return void 0;
|
|
2771
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
|
|
2772
|
+
const wildcard = segments[segments.length - 1] === "*";
|
|
2773
|
+
const parts = wildcard ? segments.slice(0, -1) : segments;
|
|
2774
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
2775
|
+
const target = path4.posix.join(base, ...parts);
|
|
2776
|
+
const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
|
|
2777
|
+
if (hit) return hit;
|
|
2778
|
+
}
|
|
2779
|
+
return void 0;
|
|
2780
|
+
}
|
|
2781
|
+
/** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
|
|
2782
|
+
resolveInclude(fromFile, spec) {
|
|
2783
|
+
const relative3 = this.lookupWithExtensions(
|
|
2784
|
+
path4.posix.join(path4.posix.dirname(fromFile), spec),
|
|
2785
|
+
"c"
|
|
2786
|
+
);
|
|
2787
|
+
if (relative3) return relative3;
|
|
2788
|
+
for (const base of [
|
|
2789
|
+
path4.posix.join(this.structure.projectRoot, "include"),
|
|
2790
|
+
this.structure.projectRoot
|
|
2791
|
+
]) {
|
|
2792
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "c");
|
|
2793
|
+
if (hit) return hit;
|
|
2794
|
+
}
|
|
2795
|
+
return void 0;
|
|
2796
|
+
}
|
|
2797
|
+
/** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
|
|
2798
|
+
resolveRuby(fromFile, spec) {
|
|
2799
|
+
const relative3 = this.lookupWithExtensions(
|
|
2800
|
+
path4.posix.join(path4.posix.dirname(fromFile), spec),
|
|
2801
|
+
"ruby"
|
|
2802
|
+
);
|
|
2803
|
+
if (relative3) return relative3;
|
|
2804
|
+
for (const base of [
|
|
2805
|
+
path4.posix.join(this.structure.projectRoot, "lib"),
|
|
2806
|
+
this.structure.projectRoot
|
|
2807
|
+
]) {
|
|
2808
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "ruby");
|
|
2809
|
+
if (hit) return hit;
|
|
2810
|
+
}
|
|
2811
|
+
return void 0;
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
|
|
2815
|
+
// src/codebase-index/import-extractor.ts
|
|
2816
|
+
var IMPORT_MAX_FILE_CHARS = 512 * 1024;
|
|
2817
|
+
var IMPORT_MAX_PER_FILE = 400;
|
|
2818
|
+
var DOTTED_IMPORT = [
|
|
2819
|
+
{ re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
|
|
2820
|
+
];
|
|
2821
|
+
var LANG_IMPORTS = {
|
|
2822
|
+
// Go and Python have real AST extractors; these patterns are the fallback for
|
|
2823
|
+
// machines with no Go toolchain or Python interpreter installed, where the
|
|
2824
|
+
// parser degrades to regex symbols and would otherwise contribute no edges.
|
|
2825
|
+
go: [
|
|
2826
|
+
{ re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
|
|
2827
|
+
// Grouped form: inside `import ( … )` each line is an optional alias plus a
|
|
2828
|
+
// quoted path. A stray match elsewhere resolves to no file and is dropped.
|
|
2829
|
+
{ re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
|
|
2830
|
+
],
|
|
2831
|
+
py: [
|
|
2832
|
+
{ re: /^[ \t]*import\s+([\w.]+)/gm },
|
|
2833
|
+
{ re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
|
|
2834
|
+
],
|
|
2835
|
+
rs: [
|
|
2836
|
+
// use a::b::C; | use a::b::{C, D}; → the path before any brace
|
|
2837
|
+
{ re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
|
|
2838
|
+
// mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
|
|
2839
|
+
{ re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
|
|
2840
|
+
],
|
|
2841
|
+
java: DOTTED_IMPORT,
|
|
2842
|
+
kotlin: DOTTED_IMPORT,
|
|
2843
|
+
scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
|
|
2844
|
+
csharp: [
|
|
2845
|
+
// using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
|
|
2846
|
+
{ re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
|
|
2847
|
+
{ re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
|
|
2848
|
+
],
|
|
2849
|
+
// Quoted includes only: <stdio.h> is a system header with no indexed file.
|
|
2850
|
+
c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
|
|
2851
|
+
cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
|
|
2852
|
+
ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
|
|
2853
|
+
php: [
|
|
2854
|
+
// `use A\B\C` imports the class C, which is what the index has a symbol
|
|
2855
|
+
// for — the namespace symbol only covers the `A\B` prefix.
|
|
2856
|
+
{ re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
|
|
2857
|
+
{ re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
|
|
2858
|
+
],
|
|
2859
|
+
swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
|
|
2860
|
+
dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
|
|
2861
|
+
lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
|
|
2862
|
+
elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
|
|
2863
|
+
haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
|
|
2864
|
+
zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
|
|
2865
|
+
proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
|
|
2866
|
+
// `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
|
|
2867
|
+
css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
|
|
2868
|
+
// A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
|
|
2869
|
+
vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
|
|
2870
|
+
svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
|
|
2871
|
+
html: [
|
|
2872
|
+
{ re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
|
|
2873
|
+
{ re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
|
|
2874
|
+
],
|
|
2875
|
+
shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
|
|
2876
|
+
r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
|
|
2877
|
+
};
|
|
2878
|
+
function lastSegment(specifier) {
|
|
2879
|
+
const pathLike = /[/\\]|::/.test(specifier);
|
|
2880
|
+
const segments = specifier.split(/[/\\]|::/).filter(Boolean);
|
|
2881
|
+
let last = segments[segments.length - 1] ?? specifier;
|
|
2882
|
+
if (last === "*" || last === "_") {
|
|
2883
|
+
last = segments[segments.length - 2] ?? specifier;
|
|
2884
|
+
}
|
|
2885
|
+
if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
|
|
2886
|
+
const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
|
|
2887
|
+
return dotted[dotted.length - 1] ?? last;
|
|
2888
|
+
}
|
|
2889
|
+
function newlineOffsets(content) {
|
|
2890
|
+
const offsets = [];
|
|
2891
|
+
for (let i = 0; i < content.length; i++) {
|
|
2892
|
+
if (content.charCodeAt(i) === 10) offsets.push(i);
|
|
2893
|
+
}
|
|
2894
|
+
return offsets;
|
|
2895
|
+
}
|
|
2896
|
+
function lineAt(offsets, index) {
|
|
2897
|
+
let low = 0;
|
|
2898
|
+
let high = offsets.length;
|
|
2899
|
+
while (low < high) {
|
|
2900
|
+
const mid = low + high >>> 1;
|
|
2901
|
+
if ((offsets[mid] ?? 0) < index) low = mid + 1;
|
|
2902
|
+
else high = mid;
|
|
2903
|
+
}
|
|
2904
|
+
return low + 1;
|
|
2905
|
+
}
|
|
2906
|
+
function hasImportPatterns(lang) {
|
|
2907
|
+
return LANG_IMPORTS[lang] !== void 0;
|
|
2908
|
+
}
|
|
2909
|
+
function extractImports(opts) {
|
|
2910
|
+
const patterns = LANG_IMPORTS[opts.lang];
|
|
2911
|
+
if (!patterns || !opts.content) return [];
|
|
2912
|
+
const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
|
|
2913
|
+
const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
|
|
2914
|
+
const refs = [];
|
|
2915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2916
|
+
const offsets = newlineOffsets(content);
|
|
2917
|
+
for (const pattern of patterns) {
|
|
2918
|
+
const re = new RegExp(pattern.re.source, pattern.re.flags);
|
|
2919
|
+
for (const match of content.matchAll(re)) {
|
|
2920
|
+
if (refs.length >= limit) return refs;
|
|
2921
|
+
const specifier = match[1]?.trim();
|
|
2922
|
+
if (!specifier) continue;
|
|
2923
|
+
const module = specifier;
|
|
2924
|
+
const toName = pattern.name === "full" ? module : lastSegment(module);
|
|
2925
|
+
if (!toName) continue;
|
|
2926
|
+
const key = `${module}\0${toName}`;
|
|
2927
|
+
if (seen.has(key)) continue;
|
|
2928
|
+
seen.add(key);
|
|
2929
|
+
refs.push({
|
|
2930
|
+
fromId: 0,
|
|
2931
|
+
toName,
|
|
2932
|
+
callType: "import",
|
|
2933
|
+
line: lineAt(offsets, match.index ?? 0),
|
|
2934
|
+
lang: opts.lang,
|
|
2935
|
+
module
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
return refs;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2056
2942
|
// src/codebase-index/parser-dispatch.ts
|
|
2057
2943
|
async function parseFileContent(file, content, lang) {
|
|
2944
|
+
const parsed2 = await dispatch(file, content, lang);
|
|
2945
|
+
return withRelations(parsed2, content, lang);
|
|
2946
|
+
}
|
|
2947
|
+
async function dispatch(file, content, lang) {
|
|
2058
2948
|
switch (lang) {
|
|
2059
2949
|
case "ts":
|
|
2060
2950
|
case "tsx":
|
|
@@ -2089,6 +2979,13 @@ async function parseFileContent(file, content, lang) {
|
|
|
2089
2979
|
}
|
|
2090
2980
|
}
|
|
2091
2981
|
}
|
|
2982
|
+
function withRelations(parsed2, content, lang) {
|
|
2983
|
+
let refs = parsed2.refs ?? [];
|
|
2984
|
+
if (refs.length === 0 && hasImportPatterns(lang)) {
|
|
2985
|
+
refs = extractImports({ content, lang });
|
|
2986
|
+
}
|
|
2987
|
+
return { ...parsed2, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
|
|
2988
|
+
}
|
|
2092
2989
|
|
|
2093
2990
|
// src/codebase-index/writer.ts
|
|
2094
2991
|
import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
|
|
@@ -2098,9 +2995,9 @@ import * as path10 from "node:path";
|
|
|
2098
2995
|
// src/codebase-index/bm25.ts
|
|
2099
2996
|
var K1 = 1.5;
|
|
2100
2997
|
var B = 0.75;
|
|
2998
|
+
var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
|
|
2101
2999
|
function tokenise(text) {
|
|
2102
|
-
|
|
2103
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
3000
|
+
return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
2104
3001
|
}
|
|
2105
3002
|
function splitName(name) {
|
|
2106
3003
|
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
@@ -2184,6 +3081,9 @@ var Bm25Index = class {
|
|
|
2184
3081
|
}
|
|
2185
3082
|
};
|
|
2186
3083
|
|
|
3084
|
+
// src/codebase-index/writer.ts
|
|
3085
|
+
init_languages();
|
|
3086
|
+
|
|
2187
3087
|
// src/codebase-index/lsp-kind.ts
|
|
2188
3088
|
function lspKindToInternalKind(k) {
|
|
2189
3089
|
switch (k) {
|
|
@@ -2218,7 +3118,7 @@ function lspKindToInternalKind(k) {
|
|
|
2218
3118
|
}
|
|
2219
3119
|
|
|
2220
3120
|
// src/codebase-index/schema.ts
|
|
2221
|
-
var SCHEMA_VERSION =
|
|
3121
|
+
var SCHEMA_VERSION = 4;
|
|
2222
3122
|
|
|
2223
3123
|
// src/codebase-index/sqlite-runtime.ts
|
|
2224
3124
|
import { createRequire } from "node:module";
|
|
@@ -2365,7 +3265,7 @@ function runSqliteWithRetry(fn) {
|
|
|
2365
3265
|
|
|
2366
3266
|
// src/codebase-index/writer-admin.ts
|
|
2367
3267
|
import * as fs6 from "node:fs";
|
|
2368
|
-
import * as
|
|
3268
|
+
import * as path9 from "node:path";
|
|
2369
3269
|
var DB_FILE = "index.db";
|
|
2370
3270
|
function getAllIndexableWithStatement(stmt) {
|
|
2371
3271
|
return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
|
|
@@ -2424,7 +3324,7 @@ function getAllFileMetasWithStatement(stmt) {
|
|
|
2424
3324
|
}
|
|
2425
3325
|
function getIndexDbSizeBytes(indexDir2) {
|
|
2426
3326
|
try {
|
|
2427
|
-
return fs6.statSync(
|
|
3327
|
+
return fs6.statSync(path9.join(indexDir2, DB_FILE)).size;
|
|
2428
3328
|
} catch {
|
|
2429
3329
|
return 0;
|
|
2430
3330
|
}
|
|
@@ -2475,49 +3375,43 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
|
|
|
2475
3375
|
}
|
|
2476
3376
|
function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
|
|
2477
3377
|
if (refs.length === 0) return;
|
|
2478
|
-
const chunkSize = Math.max(1, Math.floor(maxSqlVars /
|
|
3378
|
+
const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
|
|
2479
3379
|
for (let i = 0; i < refs.length; i += chunkSize) {
|
|
2480
3380
|
const chunk = refs.slice(i, i + chunkSize);
|
|
2481
|
-
const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
|
|
3381
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
|
|
2482
3382
|
const insert = stmt(
|
|
2483
|
-
`INSERT INTO refs(from_id, to_name, to_id, call_type, line
|
|
3383
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
|
|
3384
|
+
VALUES ${placeholders}`
|
|
2484
3385
|
);
|
|
2485
3386
|
const binds = [];
|
|
2486
3387
|
for (const ref of chunk) {
|
|
2487
|
-
binds.push(
|
|
3388
|
+
binds.push(
|
|
3389
|
+
ref.fromId,
|
|
3390
|
+
ref.toName,
|
|
3391
|
+
ref.toId ?? null,
|
|
3392
|
+
ref.callType,
|
|
3393
|
+
ref.line,
|
|
3394
|
+
ref.lang ?? "",
|
|
3395
|
+
ref.module ?? null,
|
|
3396
|
+
ref.toFile ?? null
|
|
3397
|
+
);
|
|
2488
3398
|
}
|
|
2489
3399
|
insert.run(...binds);
|
|
2490
3400
|
}
|
|
2491
3401
|
}
|
|
2492
3402
|
|
|
3403
|
+
// src/codebase-index/writer-graph-reader.ts
|
|
3404
|
+
init_languages();
|
|
3405
|
+
|
|
2493
3406
|
// src/codebase-index/writer-graph-helpers.ts
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
const f = filePath.replace(/\\/g, "/");
|
|
2497
|
-
const pkgsIdx = f.indexOf("/packages/");
|
|
2498
|
-
if (pkgsIdx !== -1) {
|
|
2499
|
-
const rest = f.slice(pkgsIdx + "/packages/".length);
|
|
2500
|
-
const seg = rest.split("/")[0];
|
|
2501
|
-
return seg ? `@wrongstack/${seg}` : void 0;
|
|
2502
|
-
}
|
|
2503
|
-
const appsIdx = f.indexOf("/apps/");
|
|
2504
|
-
if (appsIdx !== -1) {
|
|
2505
|
-
const rest = f.slice(appsIdx + "/apps/".length);
|
|
2506
|
-
const seg = rest.split("/")[0];
|
|
2507
|
-
return seg ? `app:${seg}` : void 0;
|
|
2508
|
-
}
|
|
2509
|
-
return void 0;
|
|
2510
|
-
}
|
|
2511
|
-
function packageFromImport(moduleName) {
|
|
2512
|
-
if (!moduleName.startsWith("@wrongstack/")) return void 0;
|
|
2513
|
-
const parts = moduleName.split("/");
|
|
2514
|
-
return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
|
|
3407
|
+
function createPackageLabeller(stored) {
|
|
3408
|
+
return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
|
|
2515
3409
|
}
|
|
2516
|
-
function buildPackageGraphNodes(fileCounts, files) {
|
|
3410
|
+
function buildPackageGraphNodes(fileCounts, files, packageOf) {
|
|
2517
3411
|
const pkgNodes = /* @__PURE__ */ new Map();
|
|
2518
3412
|
const fileToPkg = /* @__PURE__ */ new Map();
|
|
2519
3413
|
for (const { file, n } of fileCounts) {
|
|
2520
|
-
const pkg =
|
|
3414
|
+
const pkg = packageOf(file);
|
|
2521
3415
|
fileToPkg.set(file, pkg);
|
|
2522
3416
|
const node = pkgNodes.get(pkg);
|
|
2523
3417
|
if (node) {
|
|
@@ -2534,7 +3428,7 @@ function buildPackageGraphNodes(fileCounts, files) {
|
|
|
2534
3428
|
}
|
|
2535
3429
|
}
|
|
2536
3430
|
for (const { file } of files) {
|
|
2537
|
-
const pkg =
|
|
3431
|
+
const pkg = packageOf(file);
|
|
2538
3432
|
fileToPkg.set(file, pkg);
|
|
2539
3433
|
const node = pkgNodes.get(pkg);
|
|
2540
3434
|
if (node) {
|
|
@@ -2552,7 +3446,7 @@ function buildPackageGraphNodes(fileCounts, files) {
|
|
|
2552
3446
|
}
|
|
2553
3447
|
return { pkgNodes, fileToPkg };
|
|
2554
3448
|
}
|
|
2555
|
-
function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
3449
|
+
function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
|
|
2556
3450
|
const fileNodes = /* @__PURE__ */ new Map();
|
|
2557
3451
|
const symToFile = /* @__PURE__ */ new Map();
|
|
2558
3452
|
const fileStats = /* @__PURE__ */ new Map();
|
|
@@ -2571,7 +3465,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
|
2571
3465
|
id: `file:${file}`,
|
|
2572
3466
|
label: file.replace(/\\/g, "/").split("/").pop() ?? file,
|
|
2573
3467
|
kind: "file",
|
|
2574
|
-
package:
|
|
3468
|
+
package: packageOf(file),
|
|
2575
3469
|
file,
|
|
2576
3470
|
symbolCount: stats?.count ?? 0,
|
|
2577
3471
|
lang: stats?.lang,
|
|
@@ -2583,7 +3477,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
|
2583
3477
|
}
|
|
2584
3478
|
return { fileNodes, symToFile, fileStats, ensureFileNode };
|
|
2585
3479
|
}
|
|
2586
|
-
function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
3480
|
+
function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
|
|
2587
3481
|
return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
2588
3482
|
const aExternal = a.file === fileFilter ? 0 : 1;
|
|
2589
3483
|
const bExternal = b.file === fileFilter ? 0 : 1;
|
|
@@ -2595,7 +3489,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
|
2595
3489
|
symbolId: s.id,
|
|
2596
3490
|
symbolKind: s.kind,
|
|
2597
3491
|
file: s.file,
|
|
2598
|
-
package:
|
|
3492
|
+
package: packageOf(s.file),
|
|
2599
3493
|
lang: s.lang,
|
|
2600
3494
|
line: s.line,
|
|
2601
3495
|
signature: s.signature,
|
|
@@ -2603,29 +3497,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
|
2603
3497
|
external: s.file !== fileFilter
|
|
2604
3498
|
}));
|
|
2605
3499
|
}
|
|
2606
|
-
function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
|
|
2607
|
-
if (!moduleName.startsWith(".")) return void 0;
|
|
2608
|
-
const normalizedFrom = fromFile.replace(/\\/g, "/");
|
|
2609
|
-
const absolute = path9.posix.normalize(
|
|
2610
|
-
path9.posix.join(path9.posix.dirname(normalizedFrom), moduleName)
|
|
2611
|
-
);
|
|
2612
|
-
const extension = path9.posix.extname(absolute);
|
|
2613
|
-
const base = extension ? absolute.slice(0, -extension.length) : absolute;
|
|
2614
|
-
const candidates = [
|
|
2615
|
-
absolute,
|
|
2616
|
-
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
|
|
2617
|
-
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(absolute, `index${ext}`)),
|
|
2618
|
-
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(base, `index${ext}`))
|
|
2619
|
-
];
|
|
2620
|
-
const indexedByPortablePath = new Map(
|
|
2621
|
-
[...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
|
|
2622
|
-
);
|
|
2623
|
-
for (const candidate of candidates) {
|
|
2624
|
-
const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
|
|
2625
|
-
if (indexed) return indexed;
|
|
2626
|
-
}
|
|
2627
|
-
return void 0;
|
|
2628
|
-
}
|
|
2629
3500
|
function addWeightedEdge(edgeMap, source, target, callType, weight) {
|
|
2630
3501
|
const key = `${source}\0${target}`;
|
|
2631
3502
|
let edge = edgeMap.get(key);
|
|
@@ -2666,7 +3537,12 @@ function mapWriterRefRow(row) {
|
|
|
2666
3537
|
toName: row.to_name,
|
|
2667
3538
|
toId: row.to_id ?? void 0,
|
|
2668
3539
|
callType: row.call_type,
|
|
2669
|
-
line: row.line
|
|
3540
|
+
line: row.line,
|
|
3541
|
+
// `lang`/`module`/`to_file` are absent from the narrower column lists some
|
|
3542
|
+
// queries select; `undefined` keeps those rows valid Refs.
|
|
3543
|
+
lang: row.lang || void 0,
|
|
3544
|
+
module: row.module ?? void 0,
|
|
3545
|
+
toFile: row.to_file ?? void 0
|
|
2670
3546
|
};
|
|
2671
3547
|
}
|
|
2672
3548
|
|
|
@@ -2814,7 +3690,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
|
|
|
2814
3690
|
function getPackageGraphWithStatement(stmt) {
|
|
2815
3691
|
const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
|
|
2816
3692
|
const files = stmt("SELECT DISTINCT file FROM files").all();
|
|
2817
|
-
const
|
|
3693
|
+
const packageOf = readPackageLabeller(stmt);
|
|
3694
|
+
const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
|
|
2818
3695
|
const refRows = stmt(
|
|
2819
3696
|
`SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
|
|
2820
3697
|
FROM refs r
|
|
@@ -2825,32 +3702,42 @@ function getPackageGraphWithStatement(stmt) {
|
|
|
2825
3702
|
).all();
|
|
2826
3703
|
const edgeMap = /* @__PURE__ */ new Map();
|
|
2827
3704
|
for (const r of refRows) {
|
|
2828
|
-
const fromPkg = fileToPkg.get(r.from_file) ??
|
|
2829
|
-
const toPkg = fileToPkg.get(r.to_file) ??
|
|
3705
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
3706
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
2830
3707
|
if (fromPkg === toPkg) continue;
|
|
2831
3708
|
const n = Number(r.n) || 0;
|
|
2832
3709
|
addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
|
|
2833
3710
|
}
|
|
2834
3711
|
const importRows = stmt(
|
|
2835
|
-
`SELECT
|
|
3712
|
+
`SELECT s.file AS from_file,
|
|
3713
|
+
COALESCE(r.to_file, st.file) AS to_file,
|
|
3714
|
+
COUNT(*) AS n
|
|
2836
3715
|
FROM refs r
|
|
2837
3716
|
JOIN symbols s ON s.id = r.from_id
|
|
3717
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
2838
3718
|
WHERE r.call_type = 'import'
|
|
2839
|
-
|
|
3719
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
3720
|
+
GROUP BY s.file, COALESCE(r.to_file, st.file)`
|
|
2840
3721
|
).all();
|
|
2841
3722
|
for (const r of importRows) {
|
|
2842
|
-
const fromPkg = fileToPkg.get(r.from_file) ??
|
|
2843
|
-
const toPkg =
|
|
2844
|
-
if (
|
|
3723
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
3724
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
3725
|
+
if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
|
|
2845
3726
|
const n = Number(r.n) || 0;
|
|
2846
3727
|
addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
|
|
2847
3728
|
}
|
|
2848
3729
|
const edges = materializeWeightedEdges(edgeMap, "pkg");
|
|
2849
3730
|
return { nodes: [...pkgNodes.values()], edges };
|
|
2850
3731
|
}
|
|
3732
|
+
function readPackageLabeller(stmt) {
|
|
3733
|
+
const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
3734
|
+
return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
|
|
3735
|
+
}
|
|
2851
3736
|
function getFileGraphWithStatement(stmt, packageFilter) {
|
|
2852
3737
|
const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
|
|
2853
|
-
const
|
|
3738
|
+
const packageOf = readPackageLabeller(stmt);
|
|
3739
|
+
const langOf = (file) => detectLang(file) ?? "other";
|
|
3740
|
+
const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
|
|
2854
3741
|
const localFiles = new Set(pkgFilePaths);
|
|
2855
3742
|
if (localFiles.size === 0) return { nodes: [], edges: [] };
|
|
2856
3743
|
const filePlaceholders = [...localFiles].map(() => "?").join(",");
|
|
@@ -2859,9 +3746,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2859
3746
|
).all(...pkgFilePaths);
|
|
2860
3747
|
const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
|
|
2861
3748
|
pkgSyms,
|
|
2862
|
-
localFiles
|
|
3749
|
+
localFiles,
|
|
3750
|
+
packageOf
|
|
2863
3751
|
);
|
|
2864
|
-
const indexedFiles = new Set(allFiles.map((f) => f.file));
|
|
2865
3752
|
const refRows = stmt(
|
|
2866
3753
|
`SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
|
|
2867
3754
|
FROM refs r
|
|
@@ -2884,7 +3771,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2884
3771
|
for (const x of extras) {
|
|
2885
3772
|
symToFile.set(x.id, x.file);
|
|
2886
3773
|
if (!fileStats.has(x.file)) {
|
|
2887
|
-
fileStats.set(x.file, { count: 0, lang:
|
|
3774
|
+
fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
|
|
2888
3775
|
}
|
|
2889
3776
|
}
|
|
2890
3777
|
}
|
|
@@ -2901,17 +3788,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2901
3788
|
addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
|
|
2902
3789
|
}
|
|
2903
3790
|
const importRows = stmt(
|
|
2904
|
-
`SELECT r.from_id, r.
|
|
3791
|
+
`SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
|
|
2905
3792
|
FROM refs r
|
|
3793
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
2906
3794
|
WHERE r.call_type = 'import'
|
|
3795
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
2907
3796
|
AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
|
|
2908
|
-
GROUP BY r.from_id, r.
|
|
3797
|
+
GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
|
|
2909
3798
|
).all(...pkgFilePaths);
|
|
2910
3799
|
for (const r of importRows) {
|
|
2911
3800
|
const fromFile = symToFile.get(r.from_id);
|
|
2912
3801
|
if (!fromFile || !localFiles.has(fromFile)) continue;
|
|
2913
|
-
const toFile =
|
|
3802
|
+
const toFile = r.to_file;
|
|
2914
3803
|
if (!toFile || fromFile === toFile) continue;
|
|
3804
|
+
if (!fileStats.has(toFile)) {
|
|
3805
|
+
fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
|
|
3806
|
+
}
|
|
2915
3807
|
ensureFileNode(fromFile);
|
|
2916
3808
|
ensureFileNode(toFile);
|
|
2917
3809
|
const n = Number(r.n) || 0;
|
|
@@ -2961,7 +3853,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
|
|
|
2961
3853
|
).all(...missingIds);
|
|
2962
3854
|
for (const s of extras) symById.set(s.id, s);
|
|
2963
3855
|
}
|
|
2964
|
-
const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
|
|
3856
|
+
const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
|
|
2965
3857
|
return { nodes, edges };
|
|
2966
3858
|
}
|
|
2967
3859
|
|
|
@@ -2983,7 +3875,7 @@ function assignRefsToSymbols(refs, symbols) {
|
|
|
2983
3875
|
}
|
|
2984
3876
|
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
2985
3877
|
if (!owner || owner.id <= 0) continue;
|
|
2986
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
3878
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
2987
3879
|
if (seen.has(key)) continue;
|
|
2988
3880
|
seen.add(key);
|
|
2989
3881
|
assigned.push({ ...ref, fromId: owner.id });
|
|
@@ -3025,7 +3917,11 @@ var CORE_TABLES_SQL = `
|
|
|
3025
3917
|
lang TEXT NOT NULL,
|
|
3026
3918
|
mtime_ms INTEGER NOT NULL,
|
|
3027
3919
|
symbol_count INTEGER NOT NULL DEFAULT 0,
|
|
3028
|
-
last_indexed INTEGER NOT NULL
|
|
3920
|
+
last_indexed INTEGER NOT NULL,
|
|
3921
|
+
-- Code Atlas grouping label, computed at index time from the ecosystem's
|
|
3922
|
+
-- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
|
|
3923
|
+
-- re-derived per query because the evidence lives on disk, not in the DB.
|
|
3924
|
+
package TEXT NOT NULL DEFAULT ''
|
|
3029
3925
|
);
|
|
3030
3926
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
3031
3927
|
id INTEGER PRIMARY KEY,
|
|
@@ -3042,6 +3938,9 @@ var CORE_TABLES_SQL = `
|
|
|
3042
3938
|
file_fk TEXT NOT NULL
|
|
3043
3939
|
);
|
|
3044
3940
|
`;
|
|
3941
|
+
var FILE_INDEX_SQL = [
|
|
3942
|
+
"CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
|
|
3943
|
+
];
|
|
3045
3944
|
var SYMBOL_INDEX_SQL = [
|
|
3046
3945
|
"CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
|
|
3047
3946
|
"CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
|
|
@@ -3058,15 +3957,32 @@ var REFS_TABLE_SQL = `
|
|
|
3058
3957
|
to_name TEXT NOT NULL,
|
|
3059
3958
|
to_id INTEGER,
|
|
3060
3959
|
call_type TEXT NOT NULL,
|
|
3061
|
-
line INTEGER NOT NULL
|
|
3960
|
+
line INTEGER NOT NULL,
|
|
3961
|
+
lang TEXT NOT NULL DEFAULT '',
|
|
3962
|
+
module TEXT,
|
|
3963
|
+
to_file TEXT
|
|
3062
3964
|
);
|
|
3063
3965
|
`;
|
|
3064
3966
|
var REFS_INDEX_SQL = [
|
|
3065
3967
|
"CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
|
|
3066
3968
|
"CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
|
|
3067
3969
|
"CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
|
|
3068
|
-
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
|
|
3970
|
+
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
|
|
3971
|
+
// Name resolution matches (to_name, lang) pairs; the composite keeps the
|
|
3972
|
+
// language-scoped UPDATE from degrading into a scan of every same-named row.
|
|
3973
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
|
|
3974
|
+
// The post-index module resolution pass groups unresolved import refs by
|
|
3975
|
+
// (module, lang); graph readers then read to_file back.
|
|
3976
|
+
"CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
|
|
3977
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
|
|
3069
3978
|
];
|
|
3979
|
+
var LANG_FAMILY_TABLE_SQL = `
|
|
3980
|
+
CREATE TABLE IF NOT EXISTS lang_family (
|
|
3981
|
+
lang TEXT PRIMARY KEY,
|
|
3982
|
+
family TEXT NOT NULL
|
|
3983
|
+
);
|
|
3984
|
+
`;
|
|
3985
|
+
var LANG_FAMILY_WILDCARD = "*";
|
|
3070
3986
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
3071
3987
|
|
|
3072
3988
|
// src/codebase-index/writer-search-helpers.ts
|
|
@@ -3287,6 +4203,60 @@ var IndexStore = class _IndexStore {
|
|
|
3287
4203
|
runWithRetry(fn) {
|
|
3288
4204
|
return runSqliteWithRetry(fn);
|
|
3289
4205
|
}
|
|
4206
|
+
/**
|
|
4207
|
+
* Mirror the in-process language→family map into SQLite.
|
|
4208
|
+
*
|
|
4209
|
+
* Rewritten on every open rather than only on schema bumps: the mapping is
|
|
4210
|
+
* static lookup data, so a code-side change (a new language, a language
|
|
4211
|
+
* moving families) must take effect without forcing a full reindex.
|
|
4212
|
+
*/
|
|
4213
|
+
seedLangFamilies() {
|
|
4214
|
+
const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
|
|
4215
|
+
for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
|
|
4216
|
+
insert.run("", LANG_FAMILY_WILDCARD);
|
|
4217
|
+
}
|
|
4218
|
+
/**
|
|
4219
|
+
* Add any column the current schema expects but the on-disk table lacks.
|
|
4220
|
+
*
|
|
4221
|
+
* `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
|
|
4222
|
+
* and the version check above only rebuilds on a version *mismatch*. That
|
|
4223
|
+
* leaves a real gap: several wstack processes share this database, and while
|
|
4224
|
+
* a version upgrade is rolling out one of them may still be running the
|
|
4225
|
+
* previous build. That older process sees the newer version number, drops the
|
|
4226
|
+
* tables, and recreates them from *its* DDL — without the newer columns —
|
|
4227
|
+
* while the metadata row still reads the new version. Every later query for
|
|
4228
|
+
* one of those columns then fails with `no such column`, and no amount of
|
|
4229
|
+
* reindexing fixes it, because the version numbers already agree.
|
|
4230
|
+
*
|
|
4231
|
+
* Repairing column-by-column makes the schema self-healing from any of those
|
|
4232
|
+
* states. Table and column names are compile-time literals from this module,
|
|
4233
|
+
* never user input.
|
|
4234
|
+
*/
|
|
4235
|
+
repairMissingColumns() {
|
|
4236
|
+
const expected = [
|
|
4237
|
+
{ table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
|
|
4238
|
+
{
|
|
4239
|
+
table: "refs",
|
|
4240
|
+
columns: [
|
|
4241
|
+
["lang", "TEXT NOT NULL DEFAULT ''"],
|
|
4242
|
+
["module", "TEXT"],
|
|
4243
|
+
["to_file", "TEXT"]
|
|
4244
|
+
]
|
|
4245
|
+
}
|
|
4246
|
+
];
|
|
4247
|
+
for (const { table, columns } of expected) {
|
|
4248
|
+
const present = new Set(
|
|
4249
|
+
this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
|
|
4250
|
+
(row) => typeof row.name === "string" ? [row.name] : []
|
|
4251
|
+
)
|
|
4252
|
+
);
|
|
4253
|
+
if (present.size === 0) continue;
|
|
4254
|
+
for (const [name, type] of columns) {
|
|
4255
|
+
if (present.has(name)) continue;
|
|
4256
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
3290
4260
|
initSchema() {
|
|
3291
4261
|
this.db.exec(METADATA_TABLE_SQL);
|
|
3292
4262
|
const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
@@ -3309,9 +4279,13 @@ var IndexStore = class _IndexStore {
|
|
|
3309
4279
|
);
|
|
3310
4280
|
}
|
|
3311
4281
|
this.db.exec(CORE_TABLES_SQL);
|
|
3312
|
-
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
3313
4282
|
this.db.exec(REFS_TABLE_SQL);
|
|
4283
|
+
this.repairMissingColumns();
|
|
4284
|
+
for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
|
|
4285
|
+
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
3314
4286
|
for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
|
|
4287
|
+
this.db.exec(LANG_FAMILY_TABLE_SQL);
|
|
4288
|
+
this.seedLangFamilies();
|
|
3315
4289
|
try {
|
|
3316
4290
|
this.db.exec(SYMBOLS_FTS_SQL);
|
|
3317
4291
|
this.ftsAvailable = true;
|
|
@@ -3346,6 +4320,18 @@ var IndexStore = class _IndexStore {
|
|
|
3346
4320
|
static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
|
|
3347
4321
|
/** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
|
|
3348
4322
|
static MAX_SQL_VARS = 900;
|
|
4323
|
+
/**
|
|
4324
|
+
* Correlated predicate: the ref in `refs` and the candidate symbol aliased
|
|
4325
|
+
* `sym` belong to the same language family — or the ref carries no language,
|
|
4326
|
+
* in which case the wildcard bind matches everything.
|
|
4327
|
+
*
|
|
4328
|
+
* Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
|
|
4329
|
+
*/
|
|
4330
|
+
static FAMILY_MATCH_SQL = `(
|
|
4331
|
+
(SELECT family FROM lang_family WHERE lang = refs.lang) = ?
|
|
4332
|
+
OR (SELECT family FROM lang_family WHERE lang = sym.lang)
|
|
4333
|
+
= (SELECT family FROM lang_family WHERE lang = refs.lang)
|
|
4334
|
+
)`;
|
|
3349
4335
|
/**
|
|
3350
4336
|
* Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
|
|
3351
4337
|
* transaction on open; the first concurrent writer under BEGIN IMMEDIATE
|
|
@@ -3409,9 +4395,12 @@ var IndexStore = class _IndexStore {
|
|
|
3409
4395
|
const placeholders = chunk.map(() => "?").join(",");
|
|
3410
4396
|
const result = this.stmt(
|
|
3411
4397
|
`UPDATE refs
|
|
3412
|
-
SET to_id = (
|
|
4398
|
+
SET to_id = (
|
|
4399
|
+
SELECT MIN(sym.id) FROM symbols sym
|
|
4400
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
4401
|
+
)
|
|
3413
4402
|
WHERE to_name IN (${placeholders})`
|
|
3414
|
-
).run(...chunk);
|
|
4403
|
+
).run(LANG_FAMILY_WILDCARD, ...chunk);
|
|
3415
4404
|
changes += result.changes ?? 0;
|
|
3416
4405
|
}
|
|
3417
4406
|
return changes;
|
|
@@ -3538,6 +4527,115 @@ var IndexStore = class _IndexStore {
|
|
|
3538
4527
|
getAllFileMetas() {
|
|
3539
4528
|
return getAllFileMetasWithStatement((sql) => this.stmt(sql));
|
|
3540
4529
|
}
|
|
4530
|
+
// ─── Project structure & module resolution ──────────────────────────────────
|
|
4531
|
+
/** Store the Code Atlas grouping label for each indexed file. */
|
|
4532
|
+
setFilePackages(entries) {
|
|
4533
|
+
if (entries.size === 0) return;
|
|
4534
|
+
this.runWithRetry(() => {
|
|
4535
|
+
const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
|
|
4536
|
+
for (const [file, label] of entries) update.run(label, file);
|
|
4537
|
+
});
|
|
4538
|
+
}
|
|
4539
|
+
/**
|
|
4540
|
+
* Every indexed `namespace`/`module` declaration, for ecosystems whose import
|
|
4541
|
+
* specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
|
|
4542
|
+
* Ordered so the resolver's choice among duplicate declarations is stable.
|
|
4543
|
+
*/
|
|
4544
|
+
getNamespaceDeclarations() {
|
|
4545
|
+
return this.stmt(
|
|
4546
|
+
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
4547
|
+
).all();
|
|
4548
|
+
}
|
|
4549
|
+
/** `file → package` for every indexed file that has a label. */
|
|
4550
|
+
getFilePackages() {
|
|
4551
|
+
const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
4552
|
+
return new Map(rows.map((row) => [row.file, row.package]));
|
|
4553
|
+
}
|
|
4554
|
+
/**
|
|
4555
|
+
* Distinct `(fromFile, lang, module)` triples needing module resolution.
|
|
4556
|
+
*
|
|
4557
|
+
* Distinct rather than per-ref because resolution depends only on these three
|
|
4558
|
+
* values: a file importing the same module twenty times resolves it once.
|
|
4559
|
+
*/
|
|
4560
|
+
getUnresolvedImports(onlyFiles) {
|
|
4561
|
+
const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
|
|
4562
|
+
FROM refs r
|
|
4563
|
+
JOIN symbols s ON s.id = r.from_id
|
|
4564
|
+
WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
|
|
4565
|
+
if (!onlyFiles?.length) {
|
|
4566
|
+
return this.stmt(base).all();
|
|
4567
|
+
}
|
|
4568
|
+
const out = [];
|
|
4569
|
+
for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
|
|
4570
|
+
const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
|
|
4571
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
4572
|
+
out.push(
|
|
4573
|
+
...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
return out;
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Write resolved import targets back onto `refs.to_file`.
|
|
4580
|
+
*
|
|
4581
|
+
* Applied through a temp table and a single UPDATE: one statement per
|
|
4582
|
+
* resolution would mean thousands of round-trips on a first index.
|
|
4583
|
+
*/
|
|
4584
|
+
applyImportResolutions(resolutions) {
|
|
4585
|
+
if (resolutions.length === 0) return 0;
|
|
4586
|
+
return this.runWithRetry(() => {
|
|
4587
|
+
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
4588
|
+
this.db.exec(
|
|
4589
|
+
`CREATE TEMP TABLE import_resolution (
|
|
4590
|
+
from_file TEXT NOT NULL,
|
|
4591
|
+
lang TEXT NOT NULL,
|
|
4592
|
+
module TEXT NOT NULL,
|
|
4593
|
+
to_file TEXT NOT NULL
|
|
4594
|
+
)`
|
|
4595
|
+
);
|
|
4596
|
+
const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
|
|
4597
|
+
for (let i = 0; i < resolutions.length; i += chunkSize) {
|
|
4598
|
+
const chunk = resolutions.slice(i, i + chunkSize);
|
|
4599
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
4600
|
+
const binds = [];
|
|
4601
|
+
for (const entry of chunk) {
|
|
4602
|
+
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
4603
|
+
}
|
|
4604
|
+
this.stmt(
|
|
4605
|
+
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
4606
|
+
VALUES ${placeholders}`
|
|
4607
|
+
).run(...binds);
|
|
4608
|
+
}
|
|
4609
|
+
this.db.exec(
|
|
4610
|
+
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
4611
|
+
ON import_resolution(module, lang, from_file)`
|
|
4612
|
+
);
|
|
4613
|
+
const result = this.stmt(
|
|
4614
|
+
`UPDATE refs
|
|
4615
|
+
SET to_file = (
|
|
4616
|
+
SELECT ir.to_file
|
|
4617
|
+
FROM temp.import_resolution ir
|
|
4618
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
4619
|
+
WHERE ir.module = refs.module
|
|
4620
|
+
AND ir.lang = refs.lang
|
|
4621
|
+
AND ir.from_file = s.file
|
|
4622
|
+
LIMIT 1
|
|
4623
|
+
)
|
|
4624
|
+
WHERE refs.call_type = 'import'
|
|
4625
|
+
AND refs.module IS NOT NULL
|
|
4626
|
+
AND EXISTS (
|
|
4627
|
+
SELECT 1
|
|
4628
|
+
FROM temp.import_resolution ir
|
|
4629
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
4630
|
+
WHERE ir.module = refs.module
|
|
4631
|
+
AND ir.lang = refs.lang
|
|
4632
|
+
AND ir.from_file = s.file
|
|
4633
|
+
)`
|
|
4634
|
+
).run();
|
|
4635
|
+
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
4636
|
+
return result.changes ?? 0;
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
3541
4639
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
3542
4640
|
search(query, filter, opts) {
|
|
3543
4641
|
const built = this.buildSearchWhere(query, filter);
|
|
@@ -3924,9 +5022,12 @@ var IndexStore = class _IndexStore {
|
|
|
3924
5022
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
3925
5023
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
3926
5024
|
*
|
|
3927
|
-
*
|
|
3928
|
-
* the
|
|
3929
|
-
*
|
|
5025
|
+
* A match additionally requires the referencing ref and the target symbol to
|
|
5026
|
+
* be in the same {@link LangFamily}. Without that guard a name match is a
|
|
5027
|
+
* cross-language accident waiting to happen — `main`, `New`, `Parse` and
|
|
5028
|
+
* `Config` are declared in most languages at once, and each collision draws a
|
|
5029
|
+
* Code Atlas edge between files that never reference each other. Refs stored
|
|
5030
|
+
* without a language keep the old global behaviour via the `'*'` wildcard row.
|
|
3930
5031
|
*/
|
|
3931
5032
|
resolveRefs() {
|
|
3932
5033
|
return this.runWithRetry(() => {
|
|
@@ -3935,20 +5036,35 @@ var IndexStore = class _IndexStore {
|
|
|
3935
5036
|
`UPDATE refs
|
|
3936
5037
|
SET to_id = s.id
|
|
3937
5038
|
FROM (
|
|
3938
|
-
|
|
3939
|
-
|
|
5039
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
5040
|
+
FROM symbols sym
|
|
5041
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
5042
|
+
GROUP BY sym.name, lf.family
|
|
5043
|
+
UNION ALL
|
|
5044
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
5045
|
+
FROM symbols sym
|
|
5046
|
+
GROUP BY sym.name
|
|
5047
|
+
) AS s,
|
|
5048
|
+
lang_family AS rf
|
|
3940
5049
|
WHERE refs.to_id IS NULL
|
|
3941
5050
|
AND refs.to_name IS NOT NULL
|
|
3942
|
-
AND
|
|
5051
|
+
AND rf.lang = refs.lang
|
|
5052
|
+
AND s.name = refs.to_name
|
|
5053
|
+
AND s.family = rf.family`
|
|
3943
5054
|
).run();
|
|
3944
5055
|
return result.changes ?? 0;
|
|
3945
5056
|
} catch {
|
|
3946
5057
|
const result = this.stmt(
|
|
3947
5058
|
`UPDATE refs SET to_id = (
|
|
3948
|
-
SELECT id FROM symbols
|
|
5059
|
+
SELECT sym.id FROM symbols sym
|
|
5060
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
5061
|
+
ORDER BY sym.id LIMIT 1
|
|
3949
5062
|
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
3950
|
-
AND
|
|
3951
|
-
|
|
5063
|
+
AND EXISTS (
|
|
5064
|
+
SELECT 1 FROM symbols sym
|
|
5065
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
5066
|
+
)`
|
|
5067
|
+
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
3952
5068
|
return result.changes ?? 0;
|
|
3953
5069
|
}
|
|
3954
5070
|
});
|
|
@@ -4166,7 +5282,7 @@ function normalizeComparablePath(value) {
|
|
|
4166
5282
|
}
|
|
4167
5283
|
function gitOutput(projectRoot2, args) {
|
|
4168
5284
|
return new Promise((resolve4, reject) => {
|
|
4169
|
-
|
|
5285
|
+
execFile(
|
|
4170
5286
|
"git",
|
|
4171
5287
|
["-C", projectRoot2, ...args],
|
|
4172
5288
|
{
|
|
@@ -4297,13 +5413,40 @@ function assignRefsToSymbols2(refs, symbols) {
|
|
|
4297
5413
|
}
|
|
4298
5414
|
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
4299
5415
|
if (!owner || owner.id <= 0) continue;
|
|
4300
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
5416
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
4301
5417
|
if (seen.has(key)) continue;
|
|
4302
5418
|
seen.add(key);
|
|
4303
5419
|
assigned.push({ ...ref, fromId: owner.id });
|
|
4304
5420
|
}
|
|
4305
5421
|
return assigned;
|
|
4306
5422
|
}
|
|
5423
|
+
async function resolveProjectRelations(store, projectRoot2, opts) {
|
|
5424
|
+
if (opts.signal?.aborted) return;
|
|
5425
|
+
try {
|
|
5426
|
+
const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
|
|
5427
|
+
if (indexedFiles.length === 0) return;
|
|
5428
|
+
const structure = await detectModuleRoots(projectRoot2, indexedFiles);
|
|
5429
|
+
if (opts.signal?.aborted) return;
|
|
5430
|
+
store.setFilePackages(assignPackageLabels(structure, indexedFiles));
|
|
5431
|
+
const resolver = new ModuleResolver(
|
|
5432
|
+
structure,
|
|
5433
|
+
indexedFiles,
|
|
5434
|
+
store.getNamespaceDeclarations()
|
|
5435
|
+
);
|
|
5436
|
+
const pending = store.getUnresolvedImports(opts.onlyFiles);
|
|
5437
|
+
const resolutions = [];
|
|
5438
|
+
for (const entry of pending) {
|
|
5439
|
+
const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
|
|
5440
|
+
if (toFile && toFile !== entry.fromFile) {
|
|
5441
|
+
resolutions.push({ ...entry, toFile });
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
if (opts.signal?.aborted) return;
|
|
5445
|
+
store.applyImportResolutions(resolutions);
|
|
5446
|
+
} catch (err) {
|
|
5447
|
+
opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
4307
5450
|
async function runIndexerWithStore(store, opts) {
|
|
4308
5451
|
const { projectRoot: projectRoot2, langs, ignore = [], signal } = opts;
|
|
4309
5452
|
const relationGraphVersion = "2";
|
|
@@ -4548,6 +5691,14 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4548
5691
|
}
|
|
4549
5692
|
}
|
|
4550
5693
|
if (needsFullRefResolution) store.resolveRefs();
|
|
5694
|
+
await resolveProjectRelations(store, projectRoot2, {
|
|
5695
|
+
// A watcher run re-resolves only what it touched; a full run (or a contract
|
|
5696
|
+
// bump) re-resolves everything, because a newly indexed file can be the
|
|
5697
|
+
// target of imports written long before it.
|
|
5698
|
+
onlyFiles: needsFullRefResolution ? void 0 : opts.files,
|
|
5699
|
+
errors,
|
|
5700
|
+
signal
|
|
5701
|
+
});
|
|
4551
5702
|
store.setMetadata("ref_resolution_version", refResolutionVersion);
|
|
4552
5703
|
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
4553
5704
|
if (!opts.files || filesIndexed >= 50) store.optimize();
|
|
@@ -4694,7 +5845,9 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
|
|
|
4694
5845
|
var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
|
|
4695
5846
|
var buildIdCache;
|
|
4696
5847
|
function projectIndexServerBuildId(entrypoint) {
|
|
4697
|
-
const
|
|
5848
|
+
const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
|
|
5849
|
+
const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
|
|
5850
|
+
const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path12.resolve(cleanHref);
|
|
4698
5851
|
try {
|
|
4699
5852
|
const stat2 = fs9.statSync(file);
|
|
4700
5853
|
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat2.mtimeMs && buildIdCache.size === stat2.size) {
|