@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
|
@@ -14,23 +14,26 @@ var __export = (target, all) => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// src/codebase-index/languages.ts
|
|
17
|
-
import * as
|
|
17
|
+
import * as path from "node:path";
|
|
18
18
|
function detectLang(file) {
|
|
19
|
-
const base =
|
|
19
|
+
const base = path.basename(file);
|
|
20
20
|
const lowerBase = base.toLowerCase();
|
|
21
21
|
if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
|
|
22
22
|
return "ts";
|
|
23
23
|
}
|
|
24
24
|
const special = SPECIAL_FILENAMES[lowerBase];
|
|
25
25
|
if (special) return special;
|
|
26
|
-
const ext =
|
|
26
|
+
const ext = path.extname(base).toLowerCase();
|
|
27
27
|
if (!ext) return null;
|
|
28
28
|
return EXT_TO_LANG[ext] ?? null;
|
|
29
29
|
}
|
|
30
30
|
function isIndexablePath(file) {
|
|
31
31
|
return detectLang(file) !== null;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
function languageFamily(lang) {
|
|
34
|
+
return LANG_FAMILY[lang] ?? "other";
|
|
35
|
+
}
|
|
36
|
+
var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
|
|
34
37
|
var init_languages = __esm({
|
|
35
38
|
"src/codebase-index/languages.ts"() {
|
|
36
39
|
"use strict";
|
|
@@ -121,6 +124,52 @@ var init_languages = __esm({
|
|
|
121
124
|
procfile: "other",
|
|
122
125
|
justfile: "other"
|
|
123
126
|
};
|
|
127
|
+
LANG_FAMILY = {
|
|
128
|
+
// Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
|
|
129
|
+
// imports from — and is imported by — plain .ts files.
|
|
130
|
+
ts: "js",
|
|
131
|
+
tsx: "js",
|
|
132
|
+
js: "js",
|
|
133
|
+
jsx: "js",
|
|
134
|
+
vue: "js",
|
|
135
|
+
svelte: "js",
|
|
136
|
+
go: "go",
|
|
137
|
+
py: "py",
|
|
138
|
+
rs: "rs",
|
|
139
|
+
// The JVM resolves across languages: Kotlin and Scala call Java directly.
|
|
140
|
+
java: "jvm",
|
|
141
|
+
kotlin: "jvm",
|
|
142
|
+
scala: "jvm",
|
|
143
|
+
csharp: "dotnet",
|
|
144
|
+
// A .h header is consumed by both C and C++ translation units.
|
|
145
|
+
c: "c",
|
|
146
|
+
cpp: "c",
|
|
147
|
+
ruby: "ruby",
|
|
148
|
+
php: "php",
|
|
149
|
+
swift: "swift",
|
|
150
|
+
dart: "dart",
|
|
151
|
+
elixir: "elixir",
|
|
152
|
+
haskell: "haskell",
|
|
153
|
+
zig: "zig",
|
|
154
|
+
lua: "lua",
|
|
155
|
+
r: "r",
|
|
156
|
+
shell: "shell",
|
|
157
|
+
sql: "sql",
|
|
158
|
+
json: "data",
|
|
159
|
+
yaml: "data",
|
|
160
|
+
toml: "data",
|
|
161
|
+
html: "web",
|
|
162
|
+
css: "web",
|
|
163
|
+
proto: "proto",
|
|
164
|
+
graphql: "graphql",
|
|
165
|
+
md: "other",
|
|
166
|
+
other: "other"
|
|
167
|
+
};
|
|
168
|
+
LANG_FAMILY_ENTRIES = Object.freeze(
|
|
169
|
+
Object.entries(LANG_FAMILY).map(
|
|
170
|
+
([lang, family]) => Object.freeze([lang, family])
|
|
171
|
+
)
|
|
172
|
+
);
|
|
124
173
|
}
|
|
125
174
|
});
|
|
126
175
|
|
|
@@ -285,7 +334,7 @@ function getTypeName(name) {
|
|
|
285
334
|
function deduplicateRefs(refs) {
|
|
286
335
|
const seen = /* @__PURE__ */ new Set();
|
|
287
336
|
return refs.filter((r) => {
|
|
288
|
-
const key = `${r.toName}:${r.callType}:${r.line}`;
|
|
337
|
+
const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
|
|
289
338
|
if (seen.has(key)) return false;
|
|
290
339
|
seen.add(key);
|
|
291
340
|
return true;
|
|
@@ -295,10 +344,16 @@ function getImportSpecifierName(spec) {
|
|
|
295
344
|
return spec.propertyName?.text ?? spec.name.text;
|
|
296
345
|
}
|
|
297
346
|
function emitImportSpecifierRefs(node, refs, lineNum) {
|
|
347
|
+
const module = moduleSpecifierOf(node.moduleSpecifier);
|
|
298
348
|
const clause = node.importClause;
|
|
299
|
-
if (!clause)
|
|
349
|
+
if (!clause) {
|
|
350
|
+
if (module) {
|
|
351
|
+
refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
300
355
|
if (clause.name) {
|
|
301
|
-
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
|
|
356
|
+
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
|
|
302
357
|
}
|
|
303
358
|
const bindings = clause.namedBindings;
|
|
304
359
|
if (!bindings) return;
|
|
@@ -308,26 +363,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
|
|
|
308
363
|
fromId: 0,
|
|
309
364
|
toName: getImportSpecifierName(element),
|
|
310
365
|
callType: "import",
|
|
311
|
-
line: lineNum
|
|
366
|
+
line: lineNum,
|
|
367
|
+
module
|
|
312
368
|
});
|
|
313
369
|
}
|
|
314
370
|
} else if (ts.isNamespaceImport(bindings)) {
|
|
315
|
-
refs.push({
|
|
371
|
+
refs.push({
|
|
372
|
+
fromId: 0,
|
|
373
|
+
toName: bindings.name.text,
|
|
374
|
+
callType: "import",
|
|
375
|
+
line: lineNum,
|
|
376
|
+
module
|
|
377
|
+
});
|
|
316
378
|
}
|
|
317
379
|
}
|
|
380
|
+
function moduleSpecifierOf(node) {
|
|
381
|
+
return node && ts.isStringLiteral(node) ? node.text : void 0;
|
|
382
|
+
}
|
|
318
383
|
function emitExportSpecifierRefs(node, refs, lineNum) {
|
|
384
|
+
const module = moduleSpecifierOf(node.moduleSpecifier);
|
|
319
385
|
const clause = node.exportClause;
|
|
320
386
|
if (clause && ts.isNamespaceExport(clause)) {
|
|
321
|
-
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
|
|
387
|
+
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
|
|
322
388
|
return;
|
|
323
389
|
}
|
|
324
390
|
if (clause && ts.isNamedExports(clause)) {
|
|
325
391
|
for (const element of clause.elements) {
|
|
326
392
|
const originalName = element.propertyName?.text ?? element.name.text;
|
|
327
|
-
refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
|
|
393
|
+
refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
|
|
328
394
|
}
|
|
329
395
|
return;
|
|
330
396
|
}
|
|
397
|
+
if (module) {
|
|
398
|
+
refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
|
|
399
|
+
}
|
|
331
400
|
}
|
|
332
401
|
var ts, tsLoad, kindMapCache;
|
|
333
402
|
var init_ts_parser = __esm({
|
|
@@ -340,21 +409,21 @@ var init_ts_parser = __esm({
|
|
|
340
409
|
});
|
|
341
410
|
|
|
342
411
|
// src/_win32-resolve.ts
|
|
343
|
-
import * as
|
|
344
|
-
import * as
|
|
412
|
+
import * as fs7 from "node:fs";
|
|
413
|
+
import * as path8 from "node:path";
|
|
345
414
|
function resolveWin32Command(cmd) {
|
|
346
415
|
if (process.platform !== "win32") return cmd;
|
|
347
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
416
|
+
if (cmd.includes("/") || cmd.includes("\\") || path8.extname(cmd.replace(/\//g, "\\"))) {
|
|
348
417
|
return cmd;
|
|
349
418
|
}
|
|
350
419
|
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
351
|
-
const pathDirs = (process.env["PATH"] ?? "").split(
|
|
420
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path8.delimiter);
|
|
352
421
|
for (const dir of pathDirs) {
|
|
353
|
-
const base =
|
|
422
|
+
const base = path8.join(dir, cmd);
|
|
354
423
|
for (const ext of pathext) {
|
|
355
424
|
const full = `${base}${ext}`;
|
|
356
425
|
try {
|
|
357
|
-
|
|
426
|
+
fs7.accessSync(full, fs7.constants.X_OK);
|
|
358
427
|
return full;
|
|
359
428
|
} catch {
|
|
360
429
|
}
|
|
@@ -368,6 +437,82 @@ var init_win32_resolve = __esm({
|
|
|
368
437
|
}
|
|
369
438
|
});
|
|
370
439
|
|
|
440
|
+
// src/codebase-index/parser-output.ts
|
|
441
|
+
function coerceSymbols(value) {
|
|
442
|
+
if (!Array.isArray(value)) return [];
|
|
443
|
+
return value.flatMap((entry) => {
|
|
444
|
+
const candidate = entry;
|
|
445
|
+
if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
name: candidate.name,
|
|
449
|
+
kind: candidate.kind,
|
|
450
|
+
line: typeof candidate.line === "number" ? candidate.line : 1,
|
|
451
|
+
col: typeof candidate.col === "number" ? candidate.col : 0,
|
|
452
|
+
signature: typeof candidate.signature === "string" ? candidate.signature : "",
|
|
453
|
+
scope: typeof candidate.scope === "string" ? candidate.scope : ""
|
|
454
|
+
}
|
|
455
|
+
];
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function coerceRefs(value, lang) {
|
|
459
|
+
if (!Array.isArray(value)) return [];
|
|
460
|
+
return value.flatMap((entry) => {
|
|
461
|
+
const candidate = entry;
|
|
462
|
+
if (typeof candidate.toName !== "string" || !candidate.toName) return [];
|
|
463
|
+
if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
|
|
464
|
+
const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
|
|
465
|
+
return [
|
|
466
|
+
{
|
|
467
|
+
fromId: 0,
|
|
468
|
+
toName: candidate.toName,
|
|
469
|
+
callType: candidate.callType,
|
|
470
|
+
line: typeof candidate.line === "number" ? candidate.line : 1,
|
|
471
|
+
lang,
|
|
472
|
+
module
|
|
473
|
+
}
|
|
474
|
+
];
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function parseParserOutput(stdout, lang) {
|
|
478
|
+
const trimmed = stdout.trim();
|
|
479
|
+
if (!trimmed) return { symbols: [], refs: [] };
|
|
480
|
+
let parsed;
|
|
481
|
+
try {
|
|
482
|
+
parsed = JSON.parse(trimmed);
|
|
483
|
+
} catch {
|
|
484
|
+
return { symbols: [], refs: [] };
|
|
485
|
+
}
|
|
486
|
+
if (Array.isArray(parsed)) return { symbols: coerceSymbols(parsed), refs: [] };
|
|
487
|
+
const record = parsed;
|
|
488
|
+
return {
|
|
489
|
+
symbols: coerceSymbols(record.symbols),
|
|
490
|
+
refs: dedupeRefs(coerceRefs(record.refs, lang))
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function dedupeRefs(refs) {
|
|
494
|
+
const seen = /* @__PURE__ */ new Set();
|
|
495
|
+
return refs.filter((ref) => {
|
|
496
|
+
const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
|
|
497
|
+
if (seen.has(key)) return false;
|
|
498
|
+
seen.add(key);
|
|
499
|
+
return true;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
var CALL_TYPES;
|
|
503
|
+
var init_parser_output = __esm({
|
|
504
|
+
"src/codebase-index/parser-output.ts"() {
|
|
505
|
+
"use strict";
|
|
506
|
+
CALL_TYPES = /* @__PURE__ */ new Set([
|
|
507
|
+
"call",
|
|
508
|
+
"type_ref",
|
|
509
|
+
"inherit",
|
|
510
|
+
"implement",
|
|
511
|
+
"import"
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
371
516
|
// src/codebase-index/spawn-gate.ts
|
|
372
517
|
function withSpawnGate(fn) {
|
|
373
518
|
const run = chain.then(fn, fn);
|
|
@@ -393,8 +538,8 @@ __export(go_parser_exports, {
|
|
|
393
538
|
});
|
|
394
539
|
import { spawn as spawn2 } from "node:child_process";
|
|
395
540
|
import * as os2 from "node:os";
|
|
396
|
-
import * as
|
|
397
|
-
import * as
|
|
541
|
+
import * as path9 from "node:path";
|
|
542
|
+
import * as fs8 from "node:fs/promises";
|
|
398
543
|
async function parseSymbols2(opts) {
|
|
399
544
|
const { file, content, lang } = opts;
|
|
400
545
|
try {
|
|
@@ -402,7 +547,8 @@ async function parseSymbols2(opts) {
|
|
|
402
547
|
if (parsed.symbols.length > 0) {
|
|
403
548
|
return parsed;
|
|
404
549
|
}
|
|
405
|
-
|
|
550
|
+
const fallback = fallbackParse(file, content, lang);
|
|
551
|
+
return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
|
|
406
552
|
} catch {
|
|
407
553
|
return fallbackParse(file, content, lang);
|
|
408
554
|
}
|
|
@@ -466,9 +612,9 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
466
612
|
try {
|
|
467
613
|
let scriptPath = _cachedGoScriptPath;
|
|
468
614
|
if (!scriptPath) {
|
|
469
|
-
const tmpDir = await
|
|
470
|
-
scriptPath =
|
|
471
|
-
await
|
|
615
|
+
const tmpDir = await fs8.mkdtemp(path9.join(os2.tmpdir(), "ws-go-parse-"));
|
|
616
|
+
scriptPath = path9.join(tmpDir, "parse.go");
|
|
617
|
+
await fs8.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
472
618
|
_cachedGoScriptPath = scriptPath;
|
|
473
619
|
}
|
|
474
620
|
const goBinary = resolveWin32Command("go");
|
|
@@ -510,8 +656,8 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
510
656
|
if (code !== 0 || !stdout.trim()) {
|
|
511
657
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
512
658
|
}
|
|
513
|
-
const
|
|
514
|
-
const symbols =
|
|
659
|
+
const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
|
|
660
|
+
const symbols = rawSymbols.map((s) => ({
|
|
515
661
|
id: 0,
|
|
516
662
|
lang,
|
|
517
663
|
kind: s.kind,
|
|
@@ -524,7 +670,7 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
524
670
|
scope: s.scope ?? "",
|
|
525
671
|
text: `${s.name} ${s.signature ?? ""}`.trim()
|
|
526
672
|
}));
|
|
527
|
-
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
673
|
+
return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
|
|
528
674
|
} catch {
|
|
529
675
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
530
676
|
}
|
|
@@ -534,6 +680,7 @@ var init_go_parser = __esm({
|
|
|
534
680
|
"src/codebase-index/go-parser.ts"() {
|
|
535
681
|
"use strict";
|
|
536
682
|
init_win32_resolve();
|
|
683
|
+
init_parser_output();
|
|
537
684
|
init_spawn_gate();
|
|
538
685
|
init_languages();
|
|
539
686
|
GO_PARSE_SCRIPT = `
|
|
@@ -547,6 +694,7 @@ import (
|
|
|
547
694
|
"go/token"
|
|
548
695
|
"io"
|
|
549
696
|
"os"
|
|
697
|
+
"strconv"
|
|
550
698
|
"strings"
|
|
551
699
|
)
|
|
552
700
|
|
|
@@ -559,16 +707,34 @@ type Sym struct {
|
|
|
559
707
|
Scope string \`json:"scope"\`
|
|
560
708
|
}
|
|
561
709
|
|
|
710
|
+
// Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
|
|
711
|
+
// yields both. Module is the import path for CallType "import", else empty.
|
|
712
|
+
type Ref struct {
|
|
713
|
+
ToName string \`json:"toName"\`
|
|
714
|
+
CallType string \`json:"callType"\`
|
|
715
|
+
Line int \`json:"line"\`
|
|
716
|
+
Module string \`json:"module"\`
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
type Result struct {
|
|
720
|
+
Symbols []Sym \`json:"symbols"\`
|
|
721
|
+
Refs []Ref \`json:"refs"\`
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
func emptyResult() string {
|
|
725
|
+
return "{\\"symbols\\":[],\\"refs\\":[]}"
|
|
726
|
+
}
|
|
727
|
+
|
|
562
728
|
func main() {
|
|
563
729
|
src, err := io.ReadAll(os.Stdin)
|
|
564
730
|
if err != nil {
|
|
565
|
-
fmt.Print(
|
|
731
|
+
fmt.Print(emptyResult())
|
|
566
732
|
return
|
|
567
733
|
}
|
|
568
734
|
fset := token.NewFileSet()
|
|
569
735
|
node, err := parser.ParseFile(fset, "src.go", src, 0)
|
|
570
736
|
if err != nil {
|
|
571
|
-
fmt.Print(
|
|
737
|
+
fmt.Print(emptyResult())
|
|
572
738
|
return
|
|
573
739
|
}
|
|
574
740
|
|
|
@@ -632,9 +798,43 @@ func main() {
|
|
|
632
798
|
}
|
|
633
799
|
}
|
|
634
800
|
|
|
635
|
-
|
|
801
|
+
refs := []Ref{}
|
|
802
|
+
ast.Inspect(node, func(n ast.Node) bool {
|
|
803
|
+
switch expr := n.(type) {
|
|
804
|
+
case *ast.CallExpr:
|
|
805
|
+
line := fset.Position(expr.Pos()).Line
|
|
806
|
+
switch fun := expr.Fun.(type) {
|
|
807
|
+
case *ast.Ident:
|
|
808
|
+
refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
|
|
809
|
+
case *ast.SelectorExpr:
|
|
810
|
+
// Record the selected name (\`Join\` of \`filepath.Join\`): it is the
|
|
811
|
+
// declared symbol name, so it resolves the same way the TypeScript
|
|
812
|
+
// and Python extractors' call refs do.
|
|
813
|
+
refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
|
|
814
|
+
}
|
|
815
|
+
case *ast.ImportSpec:
|
|
816
|
+
if expr.Path != nil {
|
|
817
|
+
if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
|
|
818
|
+
line := fset.Position(expr.Pos()).Line
|
|
819
|
+
// A Go import names a package, not a symbol; the package's
|
|
820
|
+
// last path segment is the name it is referenced by.
|
|
821
|
+
name := importPath
|
|
822
|
+
if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
|
|
823
|
+
name = importPath[idx+1:]
|
|
824
|
+
}
|
|
825
|
+
refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return true
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
if syms == nil {
|
|
833
|
+
syms = []Sym{}
|
|
834
|
+
}
|
|
835
|
+
data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
|
|
636
836
|
if err != nil {
|
|
637
|
-
fmt.Print(
|
|
837
|
+
fmt.Print(emptyResult())
|
|
638
838
|
return
|
|
639
839
|
}
|
|
640
840
|
fmt.Print(string(data))
|
|
@@ -795,16 +995,23 @@ function looksBinary(content) {
|
|
|
795
995
|
}
|
|
796
996
|
return bad / sample.length > 0.1;
|
|
797
997
|
}
|
|
798
|
-
function
|
|
799
|
-
|
|
800
|
-
let
|
|
801
|
-
|
|
802
|
-
if (content.charCodeAt(i) === 10) {
|
|
803
|
-
line++;
|
|
804
|
-
lastNl = i;
|
|
805
|
-
}
|
|
998
|
+
function newlineOffsets2(content) {
|
|
999
|
+
const offsets = [];
|
|
1000
|
+
for (let i = 0; i < content.length; i++) {
|
|
1001
|
+
if (content.charCodeAt(i) === 10) offsets.push(i);
|
|
806
1002
|
}
|
|
807
|
-
return
|
|
1003
|
+
return offsets;
|
|
1004
|
+
}
|
|
1005
|
+
function lineColAt(offsets, index) {
|
|
1006
|
+
let low = 0;
|
|
1007
|
+
let high = offsets.length;
|
|
1008
|
+
while (low < high) {
|
|
1009
|
+
const mid = low + high >>> 1;
|
|
1010
|
+
if ((offsets[mid] ?? 0) < index) low = mid + 1;
|
|
1011
|
+
else high = mid;
|
|
1012
|
+
}
|
|
1013
|
+
const lastNl = low > 0 ? offsets[low - 1] : -1;
|
|
1014
|
+
return { line: low + 1, col: index - lastNl };
|
|
808
1015
|
}
|
|
809
1016
|
function parseGeneric(opts) {
|
|
810
1017
|
const { file, lang } = opts;
|
|
@@ -817,6 +1024,7 @@ function parseGeneric(opts) {
|
|
|
817
1024
|
const patterns = patternsFor(lang);
|
|
818
1025
|
const symbols = [];
|
|
819
1026
|
const seen = /* @__PURE__ */ new Set();
|
|
1027
|
+
const nlOffsets = newlineOffsets2(content);
|
|
820
1028
|
for (const pattern of patterns) {
|
|
821
1029
|
const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
|
|
822
1030
|
re.lastIndex = 0;
|
|
@@ -830,7 +1038,7 @@ function parseGeneric(opts) {
|
|
|
830
1038
|
if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
|
|
831
1039
|
continue;
|
|
832
1040
|
}
|
|
833
|
-
const { line, col } = lineColAt(
|
|
1041
|
+
const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
|
|
834
1042
|
const key = `${name}\0${line}\0${pattern.kind}`;
|
|
835
1043
|
if (seen.has(key)) continue;
|
|
836
1044
|
seen.add(key);
|
|
@@ -986,9 +1194,13 @@ var init_generic_parser = __esm({
|
|
|
986
1194
|
],
|
|
987
1195
|
elixir: [
|
|
988
1196
|
{ re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
|
|
989
|
-
|
|
1197
|
+
// Dotted module names must be captured whole: `alias Foo.Bar` resolves
|
|
1198
|
+
// against this symbol, and a `Foo`-only capture never matches it.
|
|
1199
|
+
{ re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
|
|
990
1200
|
],
|
|
991
1201
|
haskell: [
|
|
1202
|
+
// Target of `import Data.List`.
|
|
1203
|
+
{ re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
|
|
992
1204
|
{ re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
|
|
993
1205
|
{ re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
|
|
994
1206
|
{ re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
|
|
@@ -1079,9 +1291,9 @@ __export(py_parser_exports, {
|
|
|
1079
1291
|
parseSymbols: () => parseSymbols4
|
|
1080
1292
|
});
|
|
1081
1293
|
import { spawn as spawn3 } from "node:child_process";
|
|
1082
|
-
import * as
|
|
1294
|
+
import * as fs9 from "node:fs/promises";
|
|
1083
1295
|
import * as os3 from "node:os";
|
|
1084
|
-
import * as
|
|
1296
|
+
import * as path10 from "node:path";
|
|
1085
1297
|
async function parseSymbols4(opts) {
|
|
1086
1298
|
const { file, content, lang } = opts;
|
|
1087
1299
|
try {
|
|
@@ -1159,10 +1371,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
|
1159
1371
|
async function syncPyParse(filePath, content, lang) {
|
|
1160
1372
|
try {
|
|
1161
1373
|
if (!_cachedScriptPath) {
|
|
1162
|
-
const tmpDir =
|
|
1163
|
-
await
|
|
1164
|
-
_cachedScriptPath =
|
|
1165
|
-
await
|
|
1374
|
+
const tmpDir = path10.join(os3.tmpdir(), "ws-py-parse");
|
|
1375
|
+
await fs9.mkdir(tmpDir, { recursive: true });
|
|
1376
|
+
_cachedScriptPath = path10.join(tmpDir, "parse.py");
|
|
1377
|
+
await fs9.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
1166
1378
|
}
|
|
1167
1379
|
cachedPyBinary ??= resolvePython();
|
|
1168
1380
|
const pyBinary = await cachedPyBinary;
|
|
@@ -1176,7 +1388,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1176
1388
|
if (code !== 0 || !stdout.trim()) {
|
|
1177
1389
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1178
1390
|
}
|
|
1179
|
-
const raw =
|
|
1391
|
+
const { symbols: raw, refs } = parseParserOutput(stdout, lang);
|
|
1180
1392
|
const symbols = raw.map((s) => ({
|
|
1181
1393
|
id: 0,
|
|
1182
1394
|
lang,
|
|
@@ -1190,7 +1402,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1190
1402
|
scope: s.scope ?? "",
|
|
1191
1403
|
text: `${s.name} ${s.signature ?? ""}`.trim()
|
|
1192
1404
|
}));
|
|
1193
|
-
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
1405
|
+
return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
|
|
1194
1406
|
} catch {
|
|
1195
1407
|
return null;
|
|
1196
1408
|
}
|
|
@@ -1201,6 +1413,7 @@ var init_py_parser = __esm({
|
|
|
1201
1413
|
"use strict";
|
|
1202
1414
|
init_win32_resolve();
|
|
1203
1415
|
init_generic_parser();
|
|
1416
|
+
init_parser_output();
|
|
1204
1417
|
init_spawn_gate();
|
|
1205
1418
|
init_languages();
|
|
1206
1419
|
PY_PARSE_SCRIPT = `import ast, json, sys, os
|
|
@@ -1262,7 +1475,18 @@ class Sym:
|
|
|
1262
1475
|
def is_private(name):
|
|
1263
1476
|
return name.startswith("__") and not name.endswith("__")
|
|
1264
1477
|
|
|
1478
|
+
def leaf_name(node):
|
|
1479
|
+
# Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
|
|
1480
|
+
# TypeScript and Go extractors record call refs, so resolution behaves the
|
|
1481
|
+
# same across languages.
|
|
1482
|
+
if isinstance(node, ast.Attribute):
|
|
1483
|
+
return node.attr
|
|
1484
|
+
if isinstance(node, ast.Name):
|
|
1485
|
+
return node.id
|
|
1486
|
+
return get_name(node).split(".")[-1]
|
|
1487
|
+
|
|
1265
1488
|
syms = []
|
|
1489
|
+
refs = []
|
|
1266
1490
|
errors = []
|
|
1267
1491
|
|
|
1268
1492
|
try:
|
|
@@ -1270,7 +1494,7 @@ try:
|
|
|
1270
1494
|
tree = ast.parse(source, filename=sys.argv[1])
|
|
1271
1495
|
except Exception as e:
|
|
1272
1496
|
errors.append(str(e))
|
|
1273
|
-
print("[]")
|
|
1497
|
+
print(json.dumps({"symbols": [], "refs": []}))
|
|
1274
1498
|
sys.exit(0)
|
|
1275
1499
|
|
|
1276
1500
|
# Module-level scope
|
|
@@ -1404,7 +1628,42 @@ class ModuleVisitor(ast.NodeVisitor):
|
|
|
1404
1628
|
visitor = ModuleVisitor()
|
|
1405
1629
|
visitor.visit(tree)
|
|
1406
1630
|
|
|
1407
|
-
|
|
1631
|
+
# Refs need a separate full walk: ModuleVisitor deliberately does not descend
|
|
1632
|
+
# into function bodies (it would index locals as symbols), but that is exactly
|
|
1633
|
+
# where the calls are.
|
|
1634
|
+
for node in ast.walk(tree):
|
|
1635
|
+
if isinstance(node, ast.Call):
|
|
1636
|
+
name = leaf_name(node.func)
|
|
1637
|
+
if name:
|
|
1638
|
+
refs.append({"toName": name, "callType": "call", "line": node.lineno})
|
|
1639
|
+
elif isinstance(node, ast.Import):
|
|
1640
|
+
for alias in node.names:
|
|
1641
|
+
refs.append({
|
|
1642
|
+
"toName": alias.name.split(".")[-1],
|
|
1643
|
+
"callType": "import",
|
|
1644
|
+
"line": node.lineno,
|
|
1645
|
+
"module": alias.name,
|
|
1646
|
+
})
|
|
1647
|
+
elif isinstance(node, ast.ImportFrom):
|
|
1648
|
+
# PEP 328: node.level is the number of leading dots. Preserving them is
|
|
1649
|
+
# what lets the resolver walk up from the importing file's package \u2014
|
|
1650
|
+
# dropping them made \`from .foo import X\` indistinguishable from an
|
|
1651
|
+
# absolute \`foo\`.
|
|
1652
|
+
module = ("." * (node.level or 0)) + (node.module or "")
|
|
1653
|
+
for alias in node.names:
|
|
1654
|
+
refs.append({
|
|
1655
|
+
"toName": alias.name,
|
|
1656
|
+
"callType": "import",
|
|
1657
|
+
"line": node.lineno,
|
|
1658
|
+
"module": module,
|
|
1659
|
+
})
|
|
1660
|
+
elif isinstance(node, ast.ClassDef):
|
|
1661
|
+
for base in node.bases:
|
|
1662
|
+
name = leaf_name(base)
|
|
1663
|
+
if name:
|
|
1664
|
+
refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
|
|
1665
|
+
|
|
1666
|
+
print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
|
|
1408
1667
|
`;
|
|
1409
1668
|
_cachedScriptPath = null;
|
|
1410
1669
|
}
|
|
@@ -1417,107 +1676,10 @@ __export(rs_parser_exports, {
|
|
|
1417
1676
|
parseSymbols: () => parseSymbols5
|
|
1418
1677
|
});
|
|
1419
1678
|
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
1420
|
-
import { execFile, spawn as spawn4 } from "node:child_process";
|
|
1421
|
-
import * as fs9 from "node:fs/promises";
|
|
1422
|
-
import * as path10 from "node:path";
|
|
1423
1679
|
async function parseSymbols5(opts) {
|
|
1424
1680
|
const { file, content, lang } = opts;
|
|
1425
|
-
const nativeAvailable = await checkNativeParser();
|
|
1426
|
-
if (nativeAvailable) {
|
|
1427
|
-
const result = await withSpawnGate(() => tryNativeParse(file, content));
|
|
1428
|
-
if (result) return result;
|
|
1429
|
-
}
|
|
1430
1681
|
return regexParse({ file, content, lang });
|
|
1431
1682
|
}
|
|
1432
|
-
function probe(command, args) {
|
|
1433
|
-
return new Promise((resolve4, reject) => {
|
|
1434
|
-
execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
|
|
1435
|
-
if (error) reject(error);
|
|
1436
|
-
else resolve4();
|
|
1437
|
-
});
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
function checkNativeParser() {
|
|
1441
|
-
nativeParserAvailability ??= (async () => {
|
|
1442
|
-
try {
|
|
1443
|
-
await probe("rustc", ["--version"]);
|
|
1444
|
-
const toolsDir = path10.join(process.cwd(), "tools");
|
|
1445
|
-
await probe(
|
|
1446
|
-
"cargo",
|
|
1447
|
-
[
|
|
1448
|
-
"metadata",
|
|
1449
|
-
"--no-deps",
|
|
1450
|
-
"--format-version",
|
|
1451
|
-
"1",
|
|
1452
|
-
"--manifest-path",
|
|
1453
|
-
path10.join(toolsDir, "Cargo.toml")
|
|
1454
|
-
]
|
|
1455
|
-
);
|
|
1456
|
-
return true;
|
|
1457
|
-
} catch {
|
|
1458
|
-
return false;
|
|
1459
|
-
}
|
|
1460
|
-
})();
|
|
1461
|
-
return nativeParserAvailability;
|
|
1462
|
-
}
|
|
1463
|
-
async function tryNativeParse(file, content) {
|
|
1464
|
-
try {
|
|
1465
|
-
const toolsDir = path10.join(process.cwd(), "tools");
|
|
1466
|
-
const crateDir = path10.join(toolsDir, "syn-parser");
|
|
1467
|
-
const tmpFile = path10.join(crateDir, "src", "input.rs");
|
|
1468
|
-
await fs9.writeFile(tmpFile, content, "utf8");
|
|
1469
|
-
const cargoBinary = resolveWin32Command("cargo");
|
|
1470
|
-
const result = await new Promise(
|
|
1471
|
-
(resolve4, reject) => {
|
|
1472
|
-
let settled = false;
|
|
1473
|
-
const proc = spawn4(
|
|
1474
|
-
cargoBinary,
|
|
1475
|
-
["run", "--manifest-path", path10.join(toolsDir, "Cargo.toml")],
|
|
1476
|
-
{
|
|
1477
|
-
cwd: process.cwd(),
|
|
1478
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1479
|
-
windowsHide: true
|
|
1480
|
-
}
|
|
1481
|
-
);
|
|
1482
|
-
proc.on("error", (err) => {
|
|
1483
|
-
if (settled) return;
|
|
1484
|
-
settled = true;
|
|
1485
|
-
reject(err);
|
|
1486
|
-
});
|
|
1487
|
-
let stdout2 = "";
|
|
1488
|
-
proc.stdout?.on("data", (chunk) => {
|
|
1489
|
-
stdout2 += chunk.toString();
|
|
1490
|
-
});
|
|
1491
|
-
proc.stderr?.resume();
|
|
1492
|
-
const timer = setTimeout(() => {
|
|
1493
|
-
if (settled) return;
|
|
1494
|
-
settled = true;
|
|
1495
|
-
proc.kill("SIGKILL");
|
|
1496
|
-
reject(new Error("timeout"));
|
|
1497
|
-
}, 15e3);
|
|
1498
|
-
timer.unref?.();
|
|
1499
|
-
proc.on("close", (c) => {
|
|
1500
|
-
if (settled) return;
|
|
1501
|
-
settled = true;
|
|
1502
|
-
clearTimeout(timer);
|
|
1503
|
-
resolve4({ code: c, stdout: stdout2 });
|
|
1504
|
-
});
|
|
1505
|
-
}
|
|
1506
|
-
);
|
|
1507
|
-
const { code, stdout } = result;
|
|
1508
|
-
if (code === 0 && stdout.trim()) {
|
|
1509
|
-
const symbols = JSON.parse(stdout.trim());
|
|
1510
|
-
return {
|
|
1511
|
-
file,
|
|
1512
|
-
lang: "rs",
|
|
1513
|
-
symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
|
|
1514
|
-
mtimeMs: Date.now()
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
} catch {
|
|
1518
|
-
}
|
|
1519
|
-
return null;
|
|
1520
|
-
}
|
|
1521
1683
|
function regexParse(opts) {
|
|
1522
1684
|
const { file, content, lang } = opts;
|
|
1523
1685
|
const symbols = [];
|
|
@@ -1573,12 +1735,10 @@ function regexParse(opts) {
|
|
|
1573
1735
|
});
|
|
1574
1736
|
return { file, lang, symbols: deduped, mtimeMs: Date.now() };
|
|
1575
1737
|
}
|
|
1576
|
-
var
|
|
1738
|
+
var RS_PATTERNS;
|
|
1577
1739
|
var init_rs_parser = __esm({
|
|
1578
1740
|
"src/codebase-index/rs-parser.ts"() {
|
|
1579
1741
|
"use strict";
|
|
1580
|
-
init_win32_resolve();
|
|
1581
|
-
init_spawn_gate();
|
|
1582
1742
|
init_languages();
|
|
1583
1743
|
RS_PATTERNS = [
|
|
1584
1744
|
{ regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
|
|
@@ -1972,7 +2132,7 @@ var init_yaml_parser = __esm({
|
|
|
1972
2132
|
|
|
1973
2133
|
// src/codebase-index/project-server-client.ts
|
|
1974
2134
|
import { spawn } from "node:child_process";
|
|
1975
|
-
import * as
|
|
2135
|
+
import * as fs5 from "node:fs";
|
|
1976
2136
|
import * as net from "node:net";
|
|
1977
2137
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1978
2138
|
import { checkUnixSocketPath } from "@wrongstack/core/utils";
|
|
@@ -2062,23 +2222,23 @@ function resetIndexCircuitBreaker() {
|
|
|
2062
2222
|
|
|
2063
2223
|
// src/codebase-index/project-server-endpoint.ts
|
|
2064
2224
|
import { createHash } from "node:crypto";
|
|
2065
|
-
import * as
|
|
2225
|
+
import * as fs4 from "node:fs";
|
|
2066
2226
|
import * as os from "node:os";
|
|
2067
|
-
import * as
|
|
2227
|
+
import * as path5 from "node:path";
|
|
2068
2228
|
import { fileURLToPath } from "node:url";
|
|
2069
2229
|
import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
|
|
2070
2230
|
|
|
2071
2231
|
// src/codebase-index/writer.ts
|
|
2072
2232
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
2073
|
-
import * as
|
|
2074
|
-
import * as
|
|
2233
|
+
import * as fs3 from "node:fs";
|
|
2234
|
+
import * as path4 from "node:path";
|
|
2075
2235
|
|
|
2076
2236
|
// src/codebase-index/bm25.ts
|
|
2077
2237
|
var K1 = 1.5;
|
|
2078
2238
|
var B = 0.75;
|
|
2239
|
+
var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
|
|
2079
2240
|
function tokenise(text) {
|
|
2080
|
-
|
|
2081
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
2241
|
+
return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
2082
2242
|
}
|
|
2083
2243
|
function splitName(name) {
|
|
2084
2244
|
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();
|
|
@@ -2162,6 +2322,9 @@ var Bm25Index = class {
|
|
|
2162
2322
|
}
|
|
2163
2323
|
};
|
|
2164
2324
|
|
|
2325
|
+
// src/codebase-index/writer.ts
|
|
2326
|
+
init_languages();
|
|
2327
|
+
|
|
2165
2328
|
// src/codebase-index/lsp-kind.ts
|
|
2166
2329
|
function lspKindToInternalKind(k) {
|
|
2167
2330
|
switch (k) {
|
|
@@ -2225,7 +2388,7 @@ function internalKindToLspKind(k) {
|
|
|
2225
2388
|
}
|
|
2226
2389
|
|
|
2227
2390
|
// src/codebase-index/schema.ts
|
|
2228
|
-
var SCHEMA_VERSION =
|
|
2391
|
+
var SCHEMA_VERSION = 4;
|
|
2229
2392
|
|
|
2230
2393
|
// src/codebase-index/sqlite-runtime.ts
|
|
2231
2394
|
import { createRequire } from "node:module";
|
|
@@ -2296,7 +2459,7 @@ function runSqliteWithRetry(fn) {
|
|
|
2296
2459
|
|
|
2297
2460
|
// src/codebase-index/writer-admin.ts
|
|
2298
2461
|
import * as fs from "node:fs";
|
|
2299
|
-
import * as
|
|
2462
|
+
import * as path2 from "node:path";
|
|
2300
2463
|
var DB_FILE = "index.db";
|
|
2301
2464
|
function getAllIndexableWithStatement(stmt) {
|
|
2302
2465
|
return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
|
|
@@ -2355,7 +2518,7 @@ function getAllFileMetasWithStatement(stmt) {
|
|
|
2355
2518
|
}
|
|
2356
2519
|
function getIndexDbSizeBytes(indexDir) {
|
|
2357
2520
|
try {
|
|
2358
|
-
return fs.statSync(
|
|
2521
|
+
return fs.statSync(path2.join(indexDir, DB_FILE)).size;
|
|
2359
2522
|
} catch {
|
|
2360
2523
|
return 0;
|
|
2361
2524
|
}
|
|
@@ -2406,49 +2569,345 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
|
|
|
2406
2569
|
}
|
|
2407
2570
|
function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
|
|
2408
2571
|
if (refs.length === 0) return;
|
|
2409
|
-
const chunkSize = Math.max(1, Math.floor(maxSqlVars /
|
|
2572
|
+
const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
|
|
2410
2573
|
for (let i = 0; i < refs.length; i += chunkSize) {
|
|
2411
2574
|
const chunk = refs.slice(i, i + chunkSize);
|
|
2412
|
-
const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
|
|
2575
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
|
|
2413
2576
|
const insert = stmt(
|
|
2414
|
-
`INSERT INTO refs(from_id, to_name, to_id, call_type, line
|
|
2577
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
|
|
2578
|
+
VALUES ${placeholders}`
|
|
2415
2579
|
);
|
|
2416
2580
|
const binds = [];
|
|
2417
2581
|
for (const ref of chunk) {
|
|
2418
|
-
binds.push(
|
|
2582
|
+
binds.push(
|
|
2583
|
+
ref.fromId,
|
|
2584
|
+
ref.toName,
|
|
2585
|
+
ref.toId ?? null,
|
|
2586
|
+
ref.callType,
|
|
2587
|
+
ref.line,
|
|
2588
|
+
ref.lang ?? "",
|
|
2589
|
+
ref.module ?? null,
|
|
2590
|
+
ref.toFile ?? null
|
|
2591
|
+
);
|
|
2419
2592
|
}
|
|
2420
2593
|
insert.run(...binds);
|
|
2421
2594
|
}
|
|
2422
2595
|
}
|
|
2423
2596
|
|
|
2424
|
-
// src/codebase-index/writer-graph-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2597
|
+
// src/codebase-index/writer-graph-reader.ts
|
|
2598
|
+
init_languages();
|
|
2599
|
+
|
|
2600
|
+
// src/codebase-index/module-roots.ts
|
|
2601
|
+
init_languages();
|
|
2602
|
+
import * as fs2 from "node:fs/promises";
|
|
2603
|
+
import * as path3 from "node:path";
|
|
2604
|
+
function toPortablePath(file) {
|
|
2605
|
+
return file.replace(/\\/g, "/");
|
|
2606
|
+
}
|
|
2607
|
+
async function readTextIfPresent(file) {
|
|
2608
|
+
try {
|
|
2609
|
+
return await fs2.readFile(file, "utf8");
|
|
2610
|
+
} catch {
|
|
2611
|
+
return void 0;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
function parsePackageJsonName(source) {
|
|
2615
|
+
try {
|
|
2616
|
+
const parsed = JSON.parse(source);
|
|
2617
|
+
return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
|
|
2618
|
+
} catch {
|
|
2619
|
+
return void 0;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function parseGoModulePath(source) {
|
|
2623
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
2624
|
+
const line = rawLine.replace(/\/\/.*$/, "").trim();
|
|
2625
|
+
const match = /^module\s+(\S+)/.exec(line);
|
|
2626
|
+
if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
|
|
2627
|
+
}
|
|
2628
|
+
return void 0;
|
|
2629
|
+
}
|
|
2630
|
+
function parseTomlTableName(source, tables) {
|
|
2631
|
+
let current = "";
|
|
2632
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
2633
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
2634
|
+
if (line.startsWith("[[")) {
|
|
2635
|
+
current = "\0";
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
const table = /^\[([^\]]+)\]$/.exec(line);
|
|
2639
|
+
if (table?.[1]) {
|
|
2640
|
+
current = table[1].trim();
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
if (!tables.includes(current)) continue;
|
|
2644
|
+
const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
|
|
2645
|
+
if (match?.[1]) return match[1];
|
|
2646
|
+
}
|
|
2647
|
+
return void 0;
|
|
2648
|
+
}
|
|
2649
|
+
function parsePomArtifactId(source) {
|
|
2650
|
+
const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
2651
|
+
return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
|
|
2652
|
+
}
|
|
2653
|
+
var LANGS_BY_KIND = {
|
|
2654
|
+
npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
|
|
2655
|
+
cargo: ["rs"],
|
|
2656
|
+
go: ["go"],
|
|
2657
|
+
python: ["py"],
|
|
2658
|
+
maven: ["java", "kotlin", "scala"],
|
|
2659
|
+
gradle: ["java", "kotlin", "scala"],
|
|
2660
|
+
dotnet: ["csharp"]
|
|
2661
|
+
};
|
|
2662
|
+
function ancestorsOf(dir, stopAt) {
|
|
2663
|
+
const out = [];
|
|
2664
|
+
let current = dir;
|
|
2665
|
+
for (; ; ) {
|
|
2666
|
+
out.push(current);
|
|
2667
|
+
if (current === stopAt || current.length <= stopAt.length) break;
|
|
2668
|
+
const parent = path3.posix.dirname(current);
|
|
2669
|
+
if (parent === current) break;
|
|
2670
|
+
current = parent;
|
|
2671
|
+
}
|
|
2672
|
+
return out;
|
|
2673
|
+
}
|
|
2674
|
+
var MARKER_PROBES = [
|
|
2675
|
+
{
|
|
2676
|
+
kind: "npm",
|
|
2677
|
+
file: "package.json",
|
|
2678
|
+
build: (dir, source) => {
|
|
2679
|
+
const name = parsePackageJsonName(source) ?? path3.posix.basename(dir);
|
|
2680
|
+
return { name, importPath: name, sourceRoots: [dir] };
|
|
2681
|
+
}
|
|
2682
|
+
},
|
|
2683
|
+
{
|
|
2684
|
+
kind: "cargo",
|
|
2685
|
+
file: "Cargo.toml",
|
|
2686
|
+
build: (dir, source) => {
|
|
2687
|
+
const name = parseTomlTableName(source, ["package"]);
|
|
2688
|
+
if (!name) return void 0;
|
|
2689
|
+
return {
|
|
2690
|
+
name: `crate:${name}`,
|
|
2691
|
+
// Rust paths use underscores where crate names often use dashes.
|
|
2692
|
+
importPath: name.replace(/-/g, "_"),
|
|
2693
|
+
sourceRoots: [path3.posix.join(dir, "src")]
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
},
|
|
2697
|
+
{
|
|
2698
|
+
kind: "go",
|
|
2699
|
+
file: "go.mod",
|
|
2700
|
+
build: (dir, source) => {
|
|
2701
|
+
const modulePath = parseGoModulePath(source);
|
|
2702
|
+
if (!modulePath) return void 0;
|
|
2703
|
+
return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
|
|
2704
|
+
}
|
|
2705
|
+
},
|
|
2706
|
+
{
|
|
2707
|
+
kind: "python",
|
|
2708
|
+
file: "pyproject.toml",
|
|
2709
|
+
build: (dir, source) => {
|
|
2710
|
+
const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path3.posix.basename(dir);
|
|
2711
|
+
return {
|
|
2712
|
+
name: `py:${name}`,
|
|
2713
|
+
importPath: void 0,
|
|
2714
|
+
// `src/` layout is the packaging-guide default; the root itself covers
|
|
2715
|
+
// the flat layout. Both are probed, missing ones simply never match.
|
|
2716
|
+
sourceRoots: [path3.posix.join(dir, "src"), dir]
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
},
|
|
2720
|
+
{
|
|
2721
|
+
kind: "python",
|
|
2722
|
+
file: "setup.py",
|
|
2723
|
+
build: (dir) => ({
|
|
2724
|
+
name: `py:${path3.posix.basename(dir)}`,
|
|
2725
|
+
importPath: void 0,
|
|
2726
|
+
sourceRoots: [path3.posix.join(dir, "src"), dir]
|
|
2727
|
+
})
|
|
2728
|
+
},
|
|
2729
|
+
{
|
|
2730
|
+
kind: "maven",
|
|
2731
|
+
file: "pom.xml",
|
|
2732
|
+
build: (dir, source) => {
|
|
2733
|
+
const artifactId = parsePomArtifactId(source) ?? path3.posix.basename(dir);
|
|
2734
|
+
return {
|
|
2735
|
+
name: `mvn:${artifactId}`,
|
|
2736
|
+
importPath: void 0,
|
|
2737
|
+
sourceRoots: [
|
|
2738
|
+
path3.posix.join(dir, "src/main/java"),
|
|
2739
|
+
path3.posix.join(dir, "src/main/kotlin"),
|
|
2740
|
+
path3.posix.join(dir, "src/main/scala"),
|
|
2741
|
+
path3.posix.join(dir, "src/test/java")
|
|
2742
|
+
]
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
},
|
|
2746
|
+
{
|
|
2747
|
+
kind: "gradle",
|
|
2748
|
+
file: "build.gradle",
|
|
2749
|
+
build: (dir) => buildGradleRoot(dir)
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
kind: "gradle",
|
|
2753
|
+
file: "build.gradle.kts",
|
|
2754
|
+
build: (dir) => buildGradleRoot(dir)
|
|
2755
|
+
}
|
|
2756
|
+
];
|
|
2757
|
+
function buildGradleRoot(dir) {
|
|
2758
|
+
return {
|
|
2759
|
+
name: `gradle:${path3.posix.basename(dir)}`,
|
|
2760
|
+
importPath: void 0,
|
|
2761
|
+
sourceRoots: [
|
|
2762
|
+
path3.posix.join(dir, "src/main/java"),
|
|
2763
|
+
path3.posix.join(dir, "src/main/kotlin"),
|
|
2764
|
+
path3.posix.join(dir, "src/main/scala")
|
|
2765
|
+
]
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
async function probeDotnetRoot(dir) {
|
|
2769
|
+
let entries;
|
|
2770
|
+
try {
|
|
2771
|
+
entries = await fs2.readdir(dir);
|
|
2772
|
+
} catch {
|
|
2773
|
+
return void 0;
|
|
2774
|
+
}
|
|
2775
|
+
const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
|
|
2776
|
+
if (!project) return void 0;
|
|
2777
|
+
const name = project.slice(0, -".csproj".length);
|
|
2778
|
+
return {
|
|
2779
|
+
dir,
|
|
2780
|
+
kind: "dotnet",
|
|
2781
|
+
name: `csproj:${name}`,
|
|
2782
|
+
importPath: void 0,
|
|
2783
|
+
sourceRoots: [dir]
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
async function detectModuleRoots(projectRoot, files) {
|
|
2787
|
+
const root = toPortablePath(projectRoot).replace(/\/+$/, "");
|
|
2788
|
+
const langsByDir = /* @__PURE__ */ new Map();
|
|
2789
|
+
for (const file of files) {
|
|
2790
|
+
const portable = toPortablePath(file);
|
|
2791
|
+
const lang = detectLang(portable);
|
|
2792
|
+
if (!lang) continue;
|
|
2793
|
+
const dir = path3.posix.dirname(portable);
|
|
2794
|
+
let langs = langsByDir.get(dir);
|
|
2795
|
+
if (!langs) {
|
|
2796
|
+
langs = /* @__PURE__ */ new Set();
|
|
2797
|
+
langsByDir.set(dir, langs);
|
|
2798
|
+
}
|
|
2799
|
+
langs.add(lang);
|
|
2800
|
+
}
|
|
2801
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2802
|
+
for (const [dir, langs] of langsByDir) {
|
|
2803
|
+
for (const ancestor of ancestorsOf(dir, root)) {
|
|
2804
|
+
let merged = candidates.get(ancestor);
|
|
2805
|
+
if (!merged) {
|
|
2806
|
+
merged = /* @__PURE__ */ new Set();
|
|
2807
|
+
candidates.set(ancestor, merged);
|
|
2808
|
+
}
|
|
2809
|
+
for (const lang of langs) merged.add(lang);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
const roots = [];
|
|
2813
|
+
await Promise.all(
|
|
2814
|
+
[...candidates].map(async ([dir, langs]) => {
|
|
2815
|
+
for (const probe of MARKER_PROBES) {
|
|
2816
|
+
if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
|
|
2817
|
+
const source = await readTextIfPresent(path3.posix.join(dir, probe.file));
|
|
2818
|
+
if (source === void 0) continue;
|
|
2819
|
+
const built = probe.build(dir, source);
|
|
2820
|
+
if (built) roots.push({ dir, kind: probe.kind, ...built });
|
|
2821
|
+
}
|
|
2822
|
+
if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
|
|
2823
|
+
const dotnet = await probeDotnetRoot(dir);
|
|
2824
|
+
if (dotnet) roots.push(dotnet);
|
|
2825
|
+
}
|
|
2826
|
+
})
|
|
2827
|
+
);
|
|
2828
|
+
roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
|
|
2829
|
+
return { projectRoot: root, roots };
|
|
2830
|
+
}
|
|
2831
|
+
function findOwningRoot(structure, file, kinds) {
|
|
2832
|
+
const portable = toPortablePath(file);
|
|
2833
|
+
for (const root of structure.roots) {
|
|
2834
|
+
if (kinds && !kinds.includes(root.kind)) continue;
|
|
2835
|
+
if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
|
|
2836
|
+
}
|
|
2837
|
+
return void 0;
|
|
2838
|
+
}
|
|
2839
|
+
function derivePackageFromLayout(filePath) {
|
|
2840
|
+
const portable = toPortablePath(filePath);
|
|
2841
|
+
const packagesIdx = portable.indexOf("/packages/");
|
|
2842
|
+
if (packagesIdx !== -1) {
|
|
2843
|
+
const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
|
|
2844
|
+
if (segment) return `@wrongstack/${segment}`;
|
|
2845
|
+
}
|
|
2846
|
+
const appsIdx = portable.indexOf("/apps/");
|
|
2435
2847
|
if (appsIdx !== -1) {
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
return seg ? `app:${seg}` : void 0;
|
|
2848
|
+
const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
|
|
2849
|
+
if (segment) return `app:${segment}`;
|
|
2439
2850
|
}
|
|
2440
2851
|
return void 0;
|
|
2441
2852
|
}
|
|
2442
|
-
function
|
|
2443
|
-
|
|
2444
|
-
const
|
|
2445
|
-
|
|
2853
|
+
function pythonPackageLabel(structure, file, initDirs) {
|
|
2854
|
+
const portable = toPortablePath(file);
|
|
2855
|
+
const dir = path3.posix.dirname(portable);
|
|
2856
|
+
if (!initDirs.has(dir)) return void 0;
|
|
2857
|
+
const segments = [];
|
|
2858
|
+
let current = dir;
|
|
2859
|
+
while (initDirs.has(current) && current.length > structure.projectRoot.length) {
|
|
2860
|
+
segments.unshift(path3.posix.basename(current));
|
|
2861
|
+
current = path3.posix.dirname(current);
|
|
2862
|
+
}
|
|
2863
|
+
return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
|
|
2446
2864
|
}
|
|
2447
|
-
function
|
|
2865
|
+
function assignPackageLabels(structure, files) {
|
|
2866
|
+
const initDirs = /* @__PURE__ */ new Set();
|
|
2867
|
+
for (const file of files) {
|
|
2868
|
+
const portable = toPortablePath(file);
|
|
2869
|
+
if (path3.posix.basename(portable) === "__init__.py") {
|
|
2870
|
+
initDirs.add(path3.posix.dirname(portable));
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
const labels = /* @__PURE__ */ new Map();
|
|
2874
|
+
for (const file of files) {
|
|
2875
|
+
const portable = toPortablePath(file);
|
|
2876
|
+
const lang = detectLang(portable);
|
|
2877
|
+
if (lang === "go") {
|
|
2878
|
+
const owner2 = findOwningRoot(structure, portable, ["go"]);
|
|
2879
|
+
const dir = path3.posix.dirname(portable);
|
|
2880
|
+
if (owner2?.importPath) {
|
|
2881
|
+
const relative2 = path3.posix.relative(owner2.dir, dir);
|
|
2882
|
+
labels.set(file, relative2 ? `${owner2.importPath}/${relative2}` : owner2.importPath);
|
|
2883
|
+
} else {
|
|
2884
|
+
labels.set(file, `go:${path3.posix.relative(structure.projectRoot, dir) || "."}`);
|
|
2885
|
+
}
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
if (lang === "py") {
|
|
2889
|
+
const dotted = pythonPackageLabel(structure, portable, initDirs);
|
|
2890
|
+
if (dotted) {
|
|
2891
|
+
labels.set(file, dotted);
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
const owner = findOwningRoot(structure, portable);
|
|
2896
|
+
const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
|
|
2897
|
+
labels.set(file, label);
|
|
2898
|
+
}
|
|
2899
|
+
return labels;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
// src/codebase-index/writer-graph-helpers.ts
|
|
2903
|
+
function createPackageLabeller(stored) {
|
|
2904
|
+
return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
|
|
2905
|
+
}
|
|
2906
|
+
function buildPackageGraphNodes(fileCounts, files, packageOf) {
|
|
2448
2907
|
const pkgNodes = /* @__PURE__ */ new Map();
|
|
2449
2908
|
const fileToPkg = /* @__PURE__ */ new Map();
|
|
2450
2909
|
for (const { file, n } of fileCounts) {
|
|
2451
|
-
const pkg =
|
|
2910
|
+
const pkg = packageOf(file);
|
|
2452
2911
|
fileToPkg.set(file, pkg);
|
|
2453
2912
|
const node = pkgNodes.get(pkg);
|
|
2454
2913
|
if (node) {
|
|
@@ -2465,7 +2924,7 @@ function buildPackageGraphNodes(fileCounts, files) {
|
|
|
2465
2924
|
}
|
|
2466
2925
|
}
|
|
2467
2926
|
for (const { file } of files) {
|
|
2468
|
-
const pkg =
|
|
2927
|
+
const pkg = packageOf(file);
|
|
2469
2928
|
fileToPkg.set(file, pkg);
|
|
2470
2929
|
const node = pkgNodes.get(pkg);
|
|
2471
2930
|
if (node) {
|
|
@@ -2483,7 +2942,7 @@ function buildPackageGraphNodes(fileCounts, files) {
|
|
|
2483
2942
|
}
|
|
2484
2943
|
return { pkgNodes, fileToPkg };
|
|
2485
2944
|
}
|
|
2486
|
-
function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
2945
|
+
function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
|
|
2487
2946
|
const fileNodes = /* @__PURE__ */ new Map();
|
|
2488
2947
|
const symToFile = /* @__PURE__ */ new Map();
|
|
2489
2948
|
const fileStats = /* @__PURE__ */ new Map();
|
|
@@ -2502,7 +2961,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
|
2502
2961
|
id: `file:${file}`,
|
|
2503
2962
|
label: file.replace(/\\/g, "/").split("/").pop() ?? file,
|
|
2504
2963
|
kind: "file",
|
|
2505
|
-
package:
|
|
2964
|
+
package: packageOf(file),
|
|
2506
2965
|
file,
|
|
2507
2966
|
symbolCount: stats?.count ?? 0,
|
|
2508
2967
|
lang: stats?.lang,
|
|
@@ -2514,7 +2973,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
|
2514
2973
|
}
|
|
2515
2974
|
return { fileNodes, symToFile, fileStats, ensureFileNode };
|
|
2516
2975
|
}
|
|
2517
|
-
function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
2976
|
+
function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
|
|
2518
2977
|
return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
2519
2978
|
const aExternal = a.file === fileFilter ? 0 : 1;
|
|
2520
2979
|
const bExternal = b.file === fileFilter ? 0 : 1;
|
|
@@ -2526,7 +2985,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
|
2526
2985
|
symbolId: s.id,
|
|
2527
2986
|
symbolKind: s.kind,
|
|
2528
2987
|
file: s.file,
|
|
2529
|
-
package:
|
|
2988
|
+
package: packageOf(s.file),
|
|
2530
2989
|
lang: s.lang,
|
|
2531
2990
|
line: s.line,
|
|
2532
2991
|
signature: s.signature,
|
|
@@ -2534,29 +2993,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
|
2534
2993
|
external: s.file !== fileFilter
|
|
2535
2994
|
}));
|
|
2536
2995
|
}
|
|
2537
|
-
function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
|
|
2538
|
-
if (!moduleName.startsWith(".")) return void 0;
|
|
2539
|
-
const normalizedFrom = fromFile.replace(/\\/g, "/");
|
|
2540
|
-
const absolute = path2.posix.normalize(
|
|
2541
|
-
path2.posix.join(path2.posix.dirname(normalizedFrom), moduleName)
|
|
2542
|
-
);
|
|
2543
|
-
const extension = path2.posix.extname(absolute);
|
|
2544
|
-
const base = extension ? absolute.slice(0, -extension.length) : absolute;
|
|
2545
|
-
const candidates = [
|
|
2546
|
-
absolute,
|
|
2547
|
-
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
|
|
2548
|
-
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path2.posix.join(absolute, `index${ext}`)),
|
|
2549
|
-
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path2.posix.join(base, `index${ext}`))
|
|
2550
|
-
];
|
|
2551
|
-
const indexedByPortablePath = new Map(
|
|
2552
|
-
[...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
|
|
2553
|
-
);
|
|
2554
|
-
for (const candidate of candidates) {
|
|
2555
|
-
const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
|
|
2556
|
-
if (indexed) return indexed;
|
|
2557
|
-
}
|
|
2558
|
-
return void 0;
|
|
2559
|
-
}
|
|
2560
2996
|
function addWeightedEdge(edgeMap, source, target, callType, weight) {
|
|
2561
2997
|
const key = `${source}\0${target}`;
|
|
2562
2998
|
let edge = edgeMap.get(key);
|
|
@@ -2597,7 +3033,12 @@ function mapWriterRefRow(row) {
|
|
|
2597
3033
|
toName: row.to_name,
|
|
2598
3034
|
toId: row.to_id ?? void 0,
|
|
2599
3035
|
callType: row.call_type,
|
|
2600
|
-
line: row.line
|
|
3036
|
+
line: row.line,
|
|
3037
|
+
// `lang`/`module`/`to_file` are absent from the narrower column lists some
|
|
3038
|
+
// queries select; `undefined` keeps those rows valid Refs.
|
|
3039
|
+
lang: row.lang || void 0,
|
|
3040
|
+
module: row.module ?? void 0,
|
|
3041
|
+
toFile: row.to_file ?? void 0
|
|
2601
3042
|
};
|
|
2602
3043
|
}
|
|
2603
3044
|
|
|
@@ -2745,7 +3186,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
|
|
|
2745
3186
|
function getPackageGraphWithStatement(stmt) {
|
|
2746
3187
|
const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
|
|
2747
3188
|
const files = stmt("SELECT DISTINCT file FROM files").all();
|
|
2748
|
-
const
|
|
3189
|
+
const packageOf = readPackageLabeller(stmt);
|
|
3190
|
+
const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
|
|
2749
3191
|
const refRows = stmt(
|
|
2750
3192
|
`SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
|
|
2751
3193
|
FROM refs r
|
|
@@ -2756,32 +3198,42 @@ function getPackageGraphWithStatement(stmt) {
|
|
|
2756
3198
|
).all();
|
|
2757
3199
|
const edgeMap = /* @__PURE__ */ new Map();
|
|
2758
3200
|
for (const r of refRows) {
|
|
2759
|
-
const fromPkg = fileToPkg.get(r.from_file) ??
|
|
2760
|
-
const toPkg = fileToPkg.get(r.to_file) ??
|
|
3201
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
3202
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
2761
3203
|
if (fromPkg === toPkg) continue;
|
|
2762
3204
|
const n = Number(r.n) || 0;
|
|
2763
3205
|
addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
|
|
2764
3206
|
}
|
|
2765
3207
|
const importRows = stmt(
|
|
2766
|
-
`SELECT
|
|
3208
|
+
`SELECT s.file AS from_file,
|
|
3209
|
+
COALESCE(r.to_file, st.file) AS to_file,
|
|
3210
|
+
COUNT(*) AS n
|
|
2767
3211
|
FROM refs r
|
|
2768
3212
|
JOIN symbols s ON s.id = r.from_id
|
|
3213
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
2769
3214
|
WHERE r.call_type = 'import'
|
|
2770
|
-
|
|
3215
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
3216
|
+
GROUP BY s.file, COALESCE(r.to_file, st.file)`
|
|
2771
3217
|
).all();
|
|
2772
3218
|
for (const r of importRows) {
|
|
2773
|
-
const fromPkg = fileToPkg.get(r.from_file) ??
|
|
2774
|
-
const toPkg =
|
|
2775
|
-
if (
|
|
3219
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
3220
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
3221
|
+
if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
|
|
2776
3222
|
const n = Number(r.n) || 0;
|
|
2777
3223
|
addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
|
|
2778
3224
|
}
|
|
2779
3225
|
const edges = materializeWeightedEdges(edgeMap, "pkg");
|
|
2780
3226
|
return { nodes: [...pkgNodes.values()], edges };
|
|
2781
3227
|
}
|
|
3228
|
+
function readPackageLabeller(stmt) {
|
|
3229
|
+
const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
3230
|
+
return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
|
|
3231
|
+
}
|
|
2782
3232
|
function getFileGraphWithStatement(stmt, packageFilter) {
|
|
2783
3233
|
const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
|
|
2784
|
-
const
|
|
3234
|
+
const packageOf = readPackageLabeller(stmt);
|
|
3235
|
+
const langOf = (file) => detectLang(file) ?? "other";
|
|
3236
|
+
const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
|
|
2785
3237
|
const localFiles = new Set(pkgFilePaths);
|
|
2786
3238
|
if (localFiles.size === 0) return { nodes: [], edges: [] };
|
|
2787
3239
|
const filePlaceholders = [...localFiles].map(() => "?").join(",");
|
|
@@ -2790,9 +3242,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2790
3242
|
).all(...pkgFilePaths);
|
|
2791
3243
|
const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
|
|
2792
3244
|
pkgSyms,
|
|
2793
|
-
localFiles
|
|
3245
|
+
localFiles,
|
|
3246
|
+
packageOf
|
|
2794
3247
|
);
|
|
2795
|
-
const indexedFiles = new Set(allFiles.map((f) => f.file));
|
|
2796
3248
|
const refRows = stmt(
|
|
2797
3249
|
`SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
|
|
2798
3250
|
FROM refs r
|
|
@@ -2815,7 +3267,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2815
3267
|
for (const x of extras) {
|
|
2816
3268
|
symToFile.set(x.id, x.file);
|
|
2817
3269
|
if (!fileStats.has(x.file)) {
|
|
2818
|
-
fileStats.set(x.file, { count: 0, lang:
|
|
3270
|
+
fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
|
|
2819
3271
|
}
|
|
2820
3272
|
}
|
|
2821
3273
|
}
|
|
@@ -2832,17 +3284,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2832
3284
|
addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
|
|
2833
3285
|
}
|
|
2834
3286
|
const importRows = stmt(
|
|
2835
|
-
`SELECT r.from_id, r.
|
|
3287
|
+
`SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
|
|
2836
3288
|
FROM refs r
|
|
3289
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
2837
3290
|
WHERE r.call_type = 'import'
|
|
3291
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
2838
3292
|
AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
|
|
2839
|
-
GROUP BY r.from_id, r.
|
|
3293
|
+
GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
|
|
2840
3294
|
).all(...pkgFilePaths);
|
|
2841
3295
|
for (const r of importRows) {
|
|
2842
3296
|
const fromFile = symToFile.get(r.from_id);
|
|
2843
3297
|
if (!fromFile || !localFiles.has(fromFile)) continue;
|
|
2844
|
-
const toFile =
|
|
3298
|
+
const toFile = r.to_file;
|
|
2845
3299
|
if (!toFile || fromFile === toFile) continue;
|
|
3300
|
+
if (!fileStats.has(toFile)) {
|
|
3301
|
+
fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
|
|
3302
|
+
}
|
|
2846
3303
|
ensureFileNode(fromFile);
|
|
2847
3304
|
ensureFileNode(toFile);
|
|
2848
3305
|
const n = Number(r.n) || 0;
|
|
@@ -2892,7 +3349,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
|
|
|
2892
3349
|
).all(...missingIds);
|
|
2893
3350
|
for (const s of extras) symById.set(s.id, s);
|
|
2894
3351
|
}
|
|
2895
|
-
const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
|
|
3352
|
+
const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
|
|
2896
3353
|
return { nodes, edges };
|
|
2897
3354
|
}
|
|
2898
3355
|
|
|
@@ -2914,7 +3371,7 @@ function assignRefsToSymbols(refs, symbols) {
|
|
|
2914
3371
|
}
|
|
2915
3372
|
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
2916
3373
|
if (!owner || owner.id <= 0) continue;
|
|
2917
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
3374
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
2918
3375
|
if (seen.has(key)) continue;
|
|
2919
3376
|
seen.add(key);
|
|
2920
3377
|
assigned.push({ ...ref, fromId: owner.id });
|
|
@@ -2960,7 +3417,11 @@ var CORE_TABLES_SQL = `
|
|
|
2960
3417
|
lang TEXT NOT NULL,
|
|
2961
3418
|
mtime_ms INTEGER NOT NULL,
|
|
2962
3419
|
symbol_count INTEGER NOT NULL DEFAULT 0,
|
|
2963
|
-
last_indexed INTEGER NOT NULL
|
|
3420
|
+
last_indexed INTEGER NOT NULL,
|
|
3421
|
+
-- Code Atlas grouping label, computed at index time from the ecosystem's
|
|
3422
|
+
-- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
|
|
3423
|
+
-- re-derived per query because the evidence lives on disk, not in the DB.
|
|
3424
|
+
package TEXT NOT NULL DEFAULT ''
|
|
2964
3425
|
);
|
|
2965
3426
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
2966
3427
|
id INTEGER PRIMARY KEY,
|
|
@@ -2977,6 +3438,9 @@ var CORE_TABLES_SQL = `
|
|
|
2977
3438
|
file_fk TEXT NOT NULL
|
|
2978
3439
|
);
|
|
2979
3440
|
`;
|
|
3441
|
+
var FILE_INDEX_SQL = [
|
|
3442
|
+
"CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
|
|
3443
|
+
];
|
|
2980
3444
|
var SYMBOL_INDEX_SQL = [
|
|
2981
3445
|
"CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
|
|
2982
3446
|
"CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
|
|
@@ -2993,15 +3457,32 @@ var REFS_TABLE_SQL = `
|
|
|
2993
3457
|
to_name TEXT NOT NULL,
|
|
2994
3458
|
to_id INTEGER,
|
|
2995
3459
|
call_type TEXT NOT NULL,
|
|
2996
|
-
line INTEGER NOT NULL
|
|
3460
|
+
line INTEGER NOT NULL,
|
|
3461
|
+
lang TEXT NOT NULL DEFAULT '',
|
|
3462
|
+
module TEXT,
|
|
3463
|
+
to_file TEXT
|
|
2997
3464
|
);
|
|
2998
3465
|
`;
|
|
2999
3466
|
var REFS_INDEX_SQL = [
|
|
3000
3467
|
"CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
|
|
3001
3468
|
"CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
|
|
3002
3469
|
"CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
|
|
3003
|
-
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
|
|
3470
|
+
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
|
|
3471
|
+
// Name resolution matches (to_name, lang) pairs; the composite keeps the
|
|
3472
|
+
// language-scoped UPDATE from degrading into a scan of every same-named row.
|
|
3473
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
|
|
3474
|
+
// The post-index module resolution pass groups unresolved import refs by
|
|
3475
|
+
// (module, lang); graph readers then read to_file back.
|
|
3476
|
+
"CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
|
|
3477
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
|
|
3004
3478
|
];
|
|
3479
|
+
var LANG_FAMILY_TABLE_SQL = `
|
|
3480
|
+
CREATE TABLE IF NOT EXISTS lang_family (
|
|
3481
|
+
lang TEXT PRIMARY KEY,
|
|
3482
|
+
family TEXT NOT NULL
|
|
3483
|
+
);
|
|
3484
|
+
`;
|
|
3485
|
+
var LANG_FAMILY_WILDCARD = "*";
|
|
3005
3486
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
3006
3487
|
|
|
3007
3488
|
// src/codebase-index/writer-search-helpers.ts
|
|
@@ -3213,15 +3694,69 @@ var IndexStore = class _IndexStore {
|
|
|
3213
3694
|
}
|
|
3214
3695
|
constructor(projectRoot, opts = {}) {
|
|
3215
3696
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
3216
|
-
|
|
3697
|
+
fs3.mkdirSync(this.indexDir, { recursive: true });
|
|
3217
3698
|
const Database = loadDatabaseSync();
|
|
3218
|
-
this.db = new Database(
|
|
3699
|
+
this.db = new Database(path4.join(this.indexDir, DB_FILE2));
|
|
3219
3700
|
applyIndexStorePragmas(this.db);
|
|
3220
3701
|
this.initSchema();
|
|
3221
3702
|
}
|
|
3222
3703
|
runWithRetry(fn) {
|
|
3223
3704
|
return runSqliteWithRetry(fn);
|
|
3224
3705
|
}
|
|
3706
|
+
/**
|
|
3707
|
+
* Mirror the in-process language→family map into SQLite.
|
|
3708
|
+
*
|
|
3709
|
+
* Rewritten on every open rather than only on schema bumps: the mapping is
|
|
3710
|
+
* static lookup data, so a code-side change (a new language, a language
|
|
3711
|
+
* moving families) must take effect without forcing a full reindex.
|
|
3712
|
+
*/
|
|
3713
|
+
seedLangFamilies() {
|
|
3714
|
+
const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
|
|
3715
|
+
for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
|
|
3716
|
+
insert.run("", LANG_FAMILY_WILDCARD);
|
|
3717
|
+
}
|
|
3718
|
+
/**
|
|
3719
|
+
* Add any column the current schema expects but the on-disk table lacks.
|
|
3720
|
+
*
|
|
3721
|
+
* `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
|
|
3722
|
+
* and the version check above only rebuilds on a version *mismatch*. That
|
|
3723
|
+
* leaves a real gap: several wstack processes share this database, and while
|
|
3724
|
+
* a version upgrade is rolling out one of them may still be running the
|
|
3725
|
+
* previous build. That older process sees the newer version number, drops the
|
|
3726
|
+
* tables, and recreates them from *its* DDL — without the newer columns —
|
|
3727
|
+
* while the metadata row still reads the new version. Every later query for
|
|
3728
|
+
* one of those columns then fails with `no such column`, and no amount of
|
|
3729
|
+
* reindexing fixes it, because the version numbers already agree.
|
|
3730
|
+
*
|
|
3731
|
+
* Repairing column-by-column makes the schema self-healing from any of those
|
|
3732
|
+
* states. Table and column names are compile-time literals from this module,
|
|
3733
|
+
* never user input.
|
|
3734
|
+
*/
|
|
3735
|
+
repairMissingColumns() {
|
|
3736
|
+
const expected = [
|
|
3737
|
+
{ table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
|
|
3738
|
+
{
|
|
3739
|
+
table: "refs",
|
|
3740
|
+
columns: [
|
|
3741
|
+
["lang", "TEXT NOT NULL DEFAULT ''"],
|
|
3742
|
+
["module", "TEXT"],
|
|
3743
|
+
["to_file", "TEXT"]
|
|
3744
|
+
]
|
|
3745
|
+
}
|
|
3746
|
+
];
|
|
3747
|
+
for (const { table, columns } of expected) {
|
|
3748
|
+
const present = new Set(
|
|
3749
|
+
this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
|
|
3750
|
+
(row) => typeof row.name === "string" ? [row.name] : []
|
|
3751
|
+
)
|
|
3752
|
+
);
|
|
3753
|
+
if (present.size === 0) continue;
|
|
3754
|
+
for (const [name, type] of columns) {
|
|
3755
|
+
if (present.has(name)) continue;
|
|
3756
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3225
3760
|
initSchema() {
|
|
3226
3761
|
this.db.exec(METADATA_TABLE_SQL);
|
|
3227
3762
|
const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
@@ -3244,9 +3779,13 @@ var IndexStore = class _IndexStore {
|
|
|
3244
3779
|
);
|
|
3245
3780
|
}
|
|
3246
3781
|
this.db.exec(CORE_TABLES_SQL);
|
|
3247
|
-
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
3248
3782
|
this.db.exec(REFS_TABLE_SQL);
|
|
3783
|
+
this.repairMissingColumns();
|
|
3784
|
+
for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
|
|
3785
|
+
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
3249
3786
|
for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
|
|
3787
|
+
this.db.exec(LANG_FAMILY_TABLE_SQL);
|
|
3788
|
+
this.seedLangFamilies();
|
|
3250
3789
|
try {
|
|
3251
3790
|
this.db.exec(SYMBOLS_FTS_SQL);
|
|
3252
3791
|
this.ftsAvailable = true;
|
|
@@ -3281,6 +3820,18 @@ var IndexStore = class _IndexStore {
|
|
|
3281
3820
|
static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
|
|
3282
3821
|
/** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
|
|
3283
3822
|
static MAX_SQL_VARS = 900;
|
|
3823
|
+
/**
|
|
3824
|
+
* Correlated predicate: the ref in `refs` and the candidate symbol aliased
|
|
3825
|
+
* `sym` belong to the same language family — or the ref carries no language,
|
|
3826
|
+
* in which case the wildcard bind matches everything.
|
|
3827
|
+
*
|
|
3828
|
+
* Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
|
|
3829
|
+
*/
|
|
3830
|
+
static FAMILY_MATCH_SQL = `(
|
|
3831
|
+
(SELECT family FROM lang_family WHERE lang = refs.lang) = ?
|
|
3832
|
+
OR (SELECT family FROM lang_family WHERE lang = sym.lang)
|
|
3833
|
+
= (SELECT family FROM lang_family WHERE lang = refs.lang)
|
|
3834
|
+
)`;
|
|
3284
3835
|
/**
|
|
3285
3836
|
* Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
|
|
3286
3837
|
* transaction on open; the first concurrent writer under BEGIN IMMEDIATE
|
|
@@ -3344,9 +3895,12 @@ var IndexStore = class _IndexStore {
|
|
|
3344
3895
|
const placeholders = chunk.map(() => "?").join(",");
|
|
3345
3896
|
const result = this.stmt(
|
|
3346
3897
|
`UPDATE refs
|
|
3347
|
-
SET to_id = (
|
|
3898
|
+
SET to_id = (
|
|
3899
|
+
SELECT MIN(sym.id) FROM symbols sym
|
|
3900
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
3901
|
+
)
|
|
3348
3902
|
WHERE to_name IN (${placeholders})`
|
|
3349
|
-
).run(...chunk);
|
|
3903
|
+
).run(LANG_FAMILY_WILDCARD, ...chunk);
|
|
3350
3904
|
changes += result.changes ?? 0;
|
|
3351
3905
|
}
|
|
3352
3906
|
return changes;
|
|
@@ -3473,6 +4027,115 @@ var IndexStore = class _IndexStore {
|
|
|
3473
4027
|
getAllFileMetas() {
|
|
3474
4028
|
return getAllFileMetasWithStatement((sql) => this.stmt(sql));
|
|
3475
4029
|
}
|
|
4030
|
+
// ─── Project structure & module resolution ──────────────────────────────────
|
|
4031
|
+
/** Store the Code Atlas grouping label for each indexed file. */
|
|
4032
|
+
setFilePackages(entries) {
|
|
4033
|
+
if (entries.size === 0) return;
|
|
4034
|
+
this.runWithRetry(() => {
|
|
4035
|
+
const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
|
|
4036
|
+
for (const [file, label] of entries) update.run(label, file);
|
|
4037
|
+
});
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Every indexed `namespace`/`module` declaration, for ecosystems whose import
|
|
4041
|
+
* specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
|
|
4042
|
+
* Ordered so the resolver's choice among duplicate declarations is stable.
|
|
4043
|
+
*/
|
|
4044
|
+
getNamespaceDeclarations() {
|
|
4045
|
+
return this.stmt(
|
|
4046
|
+
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
4047
|
+
).all();
|
|
4048
|
+
}
|
|
4049
|
+
/** `file → package` for every indexed file that has a label. */
|
|
4050
|
+
getFilePackages() {
|
|
4051
|
+
const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
4052
|
+
return new Map(rows.map((row) => [row.file, row.package]));
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Distinct `(fromFile, lang, module)` triples needing module resolution.
|
|
4056
|
+
*
|
|
4057
|
+
* Distinct rather than per-ref because resolution depends only on these three
|
|
4058
|
+
* values: a file importing the same module twenty times resolves it once.
|
|
4059
|
+
*/
|
|
4060
|
+
getUnresolvedImports(onlyFiles) {
|
|
4061
|
+
const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
|
|
4062
|
+
FROM refs r
|
|
4063
|
+
JOIN symbols s ON s.id = r.from_id
|
|
4064
|
+
WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
|
|
4065
|
+
if (!onlyFiles?.length) {
|
|
4066
|
+
return this.stmt(base).all();
|
|
4067
|
+
}
|
|
4068
|
+
const out = [];
|
|
4069
|
+
for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
|
|
4070
|
+
const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
|
|
4071
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
4072
|
+
out.push(
|
|
4073
|
+
...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
|
|
4074
|
+
);
|
|
4075
|
+
}
|
|
4076
|
+
return out;
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* Write resolved import targets back onto `refs.to_file`.
|
|
4080
|
+
*
|
|
4081
|
+
* Applied through a temp table and a single UPDATE: one statement per
|
|
4082
|
+
* resolution would mean thousands of round-trips on a first index.
|
|
4083
|
+
*/
|
|
4084
|
+
applyImportResolutions(resolutions) {
|
|
4085
|
+
if (resolutions.length === 0) return 0;
|
|
4086
|
+
return this.runWithRetry(() => {
|
|
4087
|
+
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
4088
|
+
this.db.exec(
|
|
4089
|
+
`CREATE TEMP TABLE import_resolution (
|
|
4090
|
+
from_file TEXT NOT NULL,
|
|
4091
|
+
lang TEXT NOT NULL,
|
|
4092
|
+
module TEXT NOT NULL,
|
|
4093
|
+
to_file TEXT NOT NULL
|
|
4094
|
+
)`
|
|
4095
|
+
);
|
|
4096
|
+
const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
|
|
4097
|
+
for (let i = 0; i < resolutions.length; i += chunkSize) {
|
|
4098
|
+
const chunk = resolutions.slice(i, i + chunkSize);
|
|
4099
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
4100
|
+
const binds = [];
|
|
4101
|
+
for (const entry of chunk) {
|
|
4102
|
+
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
4103
|
+
}
|
|
4104
|
+
this.stmt(
|
|
4105
|
+
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
4106
|
+
VALUES ${placeholders}`
|
|
4107
|
+
).run(...binds);
|
|
4108
|
+
}
|
|
4109
|
+
this.db.exec(
|
|
4110
|
+
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
4111
|
+
ON import_resolution(module, lang, from_file)`
|
|
4112
|
+
);
|
|
4113
|
+
const result = this.stmt(
|
|
4114
|
+
`UPDATE refs
|
|
4115
|
+
SET to_file = (
|
|
4116
|
+
SELECT ir.to_file
|
|
4117
|
+
FROM temp.import_resolution ir
|
|
4118
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
4119
|
+
WHERE ir.module = refs.module
|
|
4120
|
+
AND ir.lang = refs.lang
|
|
4121
|
+
AND ir.from_file = s.file
|
|
4122
|
+
LIMIT 1
|
|
4123
|
+
)
|
|
4124
|
+
WHERE refs.call_type = 'import'
|
|
4125
|
+
AND refs.module IS NOT NULL
|
|
4126
|
+
AND EXISTS (
|
|
4127
|
+
SELECT 1
|
|
4128
|
+
FROM temp.import_resolution ir
|
|
4129
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
4130
|
+
WHERE ir.module = refs.module
|
|
4131
|
+
AND ir.lang = refs.lang
|
|
4132
|
+
AND ir.from_file = s.file
|
|
4133
|
+
)`
|
|
4134
|
+
).run();
|
|
4135
|
+
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
4136
|
+
return result.changes ?? 0;
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
3476
4139
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
3477
4140
|
search(query, filter, opts) {
|
|
3478
4141
|
const built = this.buildSearchWhere(query, filter);
|
|
@@ -3859,9 +4522,12 @@ var IndexStore = class _IndexStore {
|
|
|
3859
4522
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
3860
4523
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
3861
4524
|
*
|
|
3862
|
-
*
|
|
3863
|
-
* the
|
|
3864
|
-
*
|
|
4525
|
+
* A match additionally requires the referencing ref and the target symbol to
|
|
4526
|
+
* be in the same {@link LangFamily}. Without that guard a name match is a
|
|
4527
|
+
* cross-language accident waiting to happen — `main`, `New`, `Parse` and
|
|
4528
|
+
* `Config` are declared in most languages at once, and each collision draws a
|
|
4529
|
+
* Code Atlas edge between files that never reference each other. Refs stored
|
|
4530
|
+
* without a language keep the old global behaviour via the `'*'` wildcard row.
|
|
3865
4531
|
*/
|
|
3866
4532
|
resolveRefs() {
|
|
3867
4533
|
return this.runWithRetry(() => {
|
|
@@ -3870,20 +4536,35 @@ var IndexStore = class _IndexStore {
|
|
|
3870
4536
|
`UPDATE refs
|
|
3871
4537
|
SET to_id = s.id
|
|
3872
4538
|
FROM (
|
|
3873
|
-
|
|
3874
|
-
|
|
4539
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
4540
|
+
FROM symbols sym
|
|
4541
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
4542
|
+
GROUP BY sym.name, lf.family
|
|
4543
|
+
UNION ALL
|
|
4544
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
4545
|
+
FROM symbols sym
|
|
4546
|
+
GROUP BY sym.name
|
|
4547
|
+
) AS s,
|
|
4548
|
+
lang_family AS rf
|
|
3875
4549
|
WHERE refs.to_id IS NULL
|
|
3876
4550
|
AND refs.to_name IS NOT NULL
|
|
3877
|
-
AND
|
|
4551
|
+
AND rf.lang = refs.lang
|
|
4552
|
+
AND s.name = refs.to_name
|
|
4553
|
+
AND s.family = rf.family`
|
|
3878
4554
|
).run();
|
|
3879
4555
|
return result.changes ?? 0;
|
|
3880
4556
|
} catch {
|
|
3881
4557
|
const result = this.stmt(
|
|
3882
4558
|
`UPDATE refs SET to_id = (
|
|
3883
|
-
SELECT id FROM symbols
|
|
4559
|
+
SELECT sym.id FROM symbols sym
|
|
4560
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
4561
|
+
ORDER BY sym.id LIMIT 1
|
|
3884
4562
|
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
3885
|
-
AND
|
|
3886
|
-
|
|
4563
|
+
AND EXISTS (
|
|
4564
|
+
SELECT 1 FROM symbols sym
|
|
4565
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
4566
|
+
)`
|
|
4567
|
+
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
3887
4568
|
return result.changes ?? 0;
|
|
3888
4569
|
}
|
|
3889
4570
|
});
|
|
@@ -4072,21 +4753,23 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
|
|
|
4072
4753
|
var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
|
|
4073
4754
|
var buildIdCache;
|
|
4074
4755
|
function projectIndexServerBuildId(entrypoint) {
|
|
4075
|
-
const
|
|
4756
|
+
const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
|
|
4757
|
+
const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
|
|
4758
|
+
const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path5.resolve(cleanHref);
|
|
4076
4759
|
try {
|
|
4077
|
-
const stat2 =
|
|
4760
|
+
const stat2 = fs4.statSync(file);
|
|
4078
4761
|
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat2.mtimeMs && buildIdCache.size === stat2.size) {
|
|
4079
4762
|
return buildIdCache.buildId;
|
|
4080
4763
|
}
|
|
4081
|
-
const buildId = createHash("sha256").update(
|
|
4764
|
+
const buildId = createHash("sha256").update(fs4.readFileSync(file)).digest("hex").slice(0, 24);
|
|
4082
4765
|
buildIdCache = { file, mtimeMs: stat2.mtimeMs, size: stat2.size, buildId };
|
|
4083
4766
|
return buildId;
|
|
4084
4767
|
} catch {
|
|
4085
|
-
return `unreadable:${
|
|
4768
|
+
return `unreadable:${path5.basename(file)}`;
|
|
4086
4769
|
}
|
|
4087
4770
|
}
|
|
4088
4771
|
function normalizeLocalPath(value) {
|
|
4089
|
-
const resolved =
|
|
4772
|
+
const resolved = path5.resolve(value);
|
|
4090
4773
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
4091
4774
|
}
|
|
4092
4775
|
function projectIndexServerKey(projectRoot, indexDir) {
|
|
@@ -4098,11 +4781,11 @@ function projectIndexServerEndpoint(projectRoot, indexDir) {
|
|
|
4098
4781
|
if (process.platform === "win32") {
|
|
4099
4782
|
return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
|
|
4100
4783
|
}
|
|
4101
|
-
return
|
|
4784
|
+
return path5.join(os.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
|
|
4102
4785
|
}
|
|
4103
4786
|
function projectIndexServerMetadataPath(projectRoot, indexDir) {
|
|
4104
|
-
return
|
|
4105
|
-
|
|
4787
|
+
return path5.join(
|
|
4788
|
+
path5.resolve(resolveIndexDir(projectRoot, indexDir)),
|
|
4106
4789
|
PROJECT_INDEX_SERVER_METADATA_FILE
|
|
4107
4790
|
);
|
|
4108
4791
|
}
|
|
@@ -4142,7 +4825,7 @@ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
|
|
|
4142
4825
|
for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
|
|
4143
4826
|
try {
|
|
4144
4827
|
const url = new URL(rel, import.meta.url);
|
|
4145
|
-
if (url.protocol === "file:" &&
|
|
4828
|
+
if (url.protocol === "file:" && fs5.existsSync(fileURLToPath2(url))) {
|
|
4146
4829
|
builtUrl = url;
|
|
4147
4830
|
break;
|
|
4148
4831
|
}
|
|
@@ -4399,7 +5082,7 @@ var ProjectServerConnection = class {
|
|
|
4399
5082
|
currentAuthToken() {
|
|
4400
5083
|
if (this.authToken === void 0) {
|
|
4401
5084
|
try {
|
|
4402
|
-
const raw =
|
|
5085
|
+
const raw = fs5.readFileSync(
|
|
4403
5086
|
projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
|
|
4404
5087
|
"utf8"
|
|
4405
5088
|
);
|
|
@@ -4664,7 +5347,7 @@ var ProjectServerConnection = class {
|
|
|
4664
5347
|
if (!url) throw new Error("built codebase-index project server is unavailable");
|
|
4665
5348
|
if (process.platform !== "win32") {
|
|
4666
5349
|
try {
|
|
4667
|
-
|
|
5350
|
+
fs5.rmSync(this.endpoint, { force: true });
|
|
4668
5351
|
} catch {
|
|
4669
5352
|
}
|
|
4670
5353
|
}
|
|
@@ -4688,8 +5371,8 @@ var ProjectServerConnection = class {
|
|
|
4688
5371
|
process.kill(pid);
|
|
4689
5372
|
const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
|
|
4690
5373
|
try {
|
|
4691
|
-
const metadata = JSON.parse(
|
|
4692
|
-
if (metadata.pid === pid)
|
|
5374
|
+
const metadata = JSON.parse(fs5.readFileSync(metadataPath, "utf8"));
|
|
5375
|
+
if (metadata.pid === pid) fs5.rmSync(metadataPath, { force: true });
|
|
4693
5376
|
} catch {
|
|
4694
5377
|
}
|
|
4695
5378
|
return true;
|
|
@@ -4793,7 +5476,7 @@ import { Worker } from "node:worker_threads";
|
|
|
4793
5476
|
|
|
4794
5477
|
// src/codebase-index/indexer.ts
|
|
4795
5478
|
import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
|
|
4796
|
-
import { execFile
|
|
5479
|
+
import { execFile } from "node:child_process";
|
|
4797
5480
|
import * as fs10 from "node:fs/promises";
|
|
4798
5481
|
import { availableParallelism } from "node:os";
|
|
4799
5482
|
import * as path12 from "node:path";
|
|
@@ -4804,8 +5487,8 @@ import {
|
|
|
4804
5487
|
} from "@wrongstack/core/utils";
|
|
4805
5488
|
|
|
4806
5489
|
// src/codebase-index/gitignore.ts
|
|
4807
|
-
import * as
|
|
4808
|
-
import * as
|
|
5490
|
+
import * as fs6 from "node:fs/promises";
|
|
5491
|
+
import * as path6 from "node:path";
|
|
4809
5492
|
import { compileGlob } from "@wrongstack/core/utils";
|
|
4810
5493
|
function globBody(glob) {
|
|
4811
5494
|
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
@@ -4851,7 +5534,7 @@ function compileGitignore(lines) {
|
|
|
4851
5534
|
async function loadGitignoreMatcher(projectRoot) {
|
|
4852
5535
|
let lines = [];
|
|
4853
5536
|
try {
|
|
4854
|
-
const raw = await
|
|
5537
|
+
const raw = await fs6.readFile(path6.join(projectRoot, ".gitignore"), "utf8");
|
|
4855
5538
|
lines = raw.split("\n");
|
|
4856
5539
|
} catch {
|
|
4857
5540
|
}
|
|
@@ -4861,8 +5544,434 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
4861
5544
|
// src/codebase-index/indexer.ts
|
|
4862
5545
|
init_languages();
|
|
4863
5546
|
|
|
5547
|
+
// src/codebase-index/module-resolver.ts
|
|
5548
|
+
init_languages();
|
|
5549
|
+
import * as path7 from "node:path";
|
|
5550
|
+
var EXTENSIONS = {
|
|
5551
|
+
js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
|
|
5552
|
+
py: [".py", ".pyi"],
|
|
5553
|
+
rs: [".rs"],
|
|
5554
|
+
jvm: [".java", ".kt", ".scala"],
|
|
5555
|
+
c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
|
|
5556
|
+
ruby: [".rb"],
|
|
5557
|
+
go: [".go"]
|
|
5558
|
+
};
|
|
5559
|
+
var DIRECTORY_ENTRIES = {
|
|
5560
|
+
js: ["index"],
|
|
5561
|
+
py: ["__init__"],
|
|
5562
|
+
rs: ["mod"],
|
|
5563
|
+
ruby: ["index"]
|
|
5564
|
+
};
|
|
5565
|
+
function normalizeNamespace(value) {
|
|
5566
|
+
return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
|
|
5567
|
+
}
|
|
5568
|
+
var ModuleResolver = class {
|
|
5569
|
+
structure;
|
|
5570
|
+
/** Lowercased portable path → the path as indexed (case is preserved). */
|
|
5571
|
+
byPath;
|
|
5572
|
+
/** Lowercased portable directory → files directly inside it, as indexed. */
|
|
5573
|
+
byDir;
|
|
5574
|
+
/** Normalized namespace → the file declaring it (first by path, stable). */
|
|
5575
|
+
byNamespace;
|
|
5576
|
+
constructor(structure, files, namespaces = []) {
|
|
5577
|
+
this.structure = structure;
|
|
5578
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
5579
|
+
this.byDir = /* @__PURE__ */ new Map();
|
|
5580
|
+
this.byNamespace = /* @__PURE__ */ new Map();
|
|
5581
|
+
const dirsByKey = /* @__PURE__ */ new Map();
|
|
5582
|
+
for (const file of files) {
|
|
5583
|
+
const portable = toPortablePath(file);
|
|
5584
|
+
const pathKey = portable.toLowerCase();
|
|
5585
|
+
const priorPath = this.byPath.get(pathKey);
|
|
5586
|
+
if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
|
|
5587
|
+
else this.byPath.set(pathKey, file);
|
|
5588
|
+
const dir = path7.posix.dirname(portable);
|
|
5589
|
+
const dirKey = dir.toLowerCase();
|
|
5590
|
+
const knownDir = dirsByKey.get(dirKey);
|
|
5591
|
+
if (knownDir === void 0) {
|
|
5592
|
+
dirsByKey.set(dirKey, dir);
|
|
5593
|
+
this.byDir.set(dirKey, [file]);
|
|
5594
|
+
} else if (knownDir === dir) {
|
|
5595
|
+
this.byDir.get(dirKey)?.push(file);
|
|
5596
|
+
} else {
|
|
5597
|
+
dirsByKey.delete(dirKey);
|
|
5598
|
+
this.byDir.delete(dirKey);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
for (const { name, file } of namespaces) {
|
|
5602
|
+
const lang = detectLang(file);
|
|
5603
|
+
if (!lang) continue;
|
|
5604
|
+
const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
|
|
5605
|
+
if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
|
|
5606
|
+
this.byNamespace.set(key, file);
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
}
|
|
5610
|
+
/**
|
|
5611
|
+
* Resolve `specifier` as written in `fromFile`.
|
|
5612
|
+
* Returns the indexed target path, or `undefined` when it is external or
|
|
5613
|
+
* cannot be located.
|
|
5614
|
+
*/
|
|
5615
|
+
resolve(fromFile, lang, specifier) {
|
|
5616
|
+
const spec = specifier.trim().replace(/\\/g, "/");
|
|
5617
|
+
if (!spec) return void 0;
|
|
5618
|
+
const from = toPortablePath(fromFile);
|
|
5619
|
+
switch (languageFamily(lang)) {
|
|
5620
|
+
case "js":
|
|
5621
|
+
return this.resolveJs(from, spec);
|
|
5622
|
+
case "go":
|
|
5623
|
+
return this.resolveGo(spec);
|
|
5624
|
+
case "py":
|
|
5625
|
+
return this.resolvePython(from, spec);
|
|
5626
|
+
case "rs":
|
|
5627
|
+
return this.resolveRust(from, spec);
|
|
5628
|
+
case "jvm":
|
|
5629
|
+
return this.resolveJvm(spec);
|
|
5630
|
+
case "c":
|
|
5631
|
+
return this.resolveInclude(from, spec);
|
|
5632
|
+
case "ruby":
|
|
5633
|
+
return this.resolveRuby(from, spec);
|
|
5634
|
+
case "dotnet":
|
|
5635
|
+
case "php":
|
|
5636
|
+
case "elixir":
|
|
5637
|
+
case "haskell":
|
|
5638
|
+
return this.resolveNamespace(lang, spec);
|
|
5639
|
+
default:
|
|
5640
|
+
return void 0;
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
/**
|
|
5644
|
+
* Resolve a namespace specifier to the file declaring it.
|
|
5645
|
+
*
|
|
5646
|
+
* Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
|
|
5647
|
+
* names a namespace outright, while PHP's `use App\Models\User` names a
|
|
5648
|
+
* *class* inside `App\Models`, so the prefix is what was declared.
|
|
5649
|
+
*/
|
|
5650
|
+
resolveNamespace(lang, spec) {
|
|
5651
|
+
const family = languageFamily(lang);
|
|
5652
|
+
const normalized = normalizeNamespace(spec);
|
|
5653
|
+
const exact = this.byNamespace.get(`${family}:${normalized}`);
|
|
5654
|
+
if (exact) return exact;
|
|
5655
|
+
const segments = normalized.split(".").filter(Boolean);
|
|
5656
|
+
if (segments.length < 2) return void 0;
|
|
5657
|
+
return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
|
|
5658
|
+
}
|
|
5659
|
+
// ─── Lookup primitives ──────────────────────────────────────────────────────
|
|
5660
|
+
lookup(candidate) {
|
|
5661
|
+
return this.byPath.get(path7.posix.normalize(candidate).toLowerCase());
|
|
5662
|
+
}
|
|
5663
|
+
/**
|
|
5664
|
+
* Try `base` verbatim, then `base` + each extension, then each directory
|
|
5665
|
+
* entry point inside `base`.
|
|
5666
|
+
*/
|
|
5667
|
+
lookupWithExtensions(base, family) {
|
|
5668
|
+
const direct = this.lookup(base);
|
|
5669
|
+
if (direct) return direct;
|
|
5670
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
5671
|
+
const suffix = path7.posix.extname(base);
|
|
5672
|
+
const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
|
|
5673
|
+
for (const ext of extensions) {
|
|
5674
|
+
const hit = this.lookup(`${stem}${ext}`);
|
|
5675
|
+
if (hit) return hit;
|
|
5676
|
+
}
|
|
5677
|
+
for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
|
|
5678
|
+
for (const ext of extensions) {
|
|
5679
|
+
const hit = this.lookup(path7.posix.join(base, `${entry}${ext}`));
|
|
5680
|
+
if (hit) return hit;
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
return void 0;
|
|
5684
|
+
}
|
|
5685
|
+
/**
|
|
5686
|
+
* A representative indexed file inside `dir`, for ecosystems whose import
|
|
5687
|
+
* unit is a directory rather than a file (Go packages, JVM wildcard imports).
|
|
5688
|
+
*
|
|
5689
|
+
* The choice is deterministic — a file named after the directory, else the
|
|
5690
|
+
* first by name — so the same import always produces the same edge. Package
|
|
5691
|
+
* grouping is unaffected either way: every file in the directory carries the
|
|
5692
|
+
* same package label, so the package-level edge is exact regardless of which
|
|
5693
|
+
* member represents it.
|
|
5694
|
+
*/
|
|
5695
|
+
representativeIn(dir, family) {
|
|
5696
|
+
const members = this.byDir.get(path7.posix.normalize(dir).toLowerCase());
|
|
5697
|
+
if (!members?.length) return void 0;
|
|
5698
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
5699
|
+
const eligible = members.filter((file) => extensions.includes(path7.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
|
|
5700
|
+
if (eligible.length === 0) return void 0;
|
|
5701
|
+
const base = path7.posix.basename(path7.posix.normalize(dir)).toLowerCase();
|
|
5702
|
+
const named = eligible.find(
|
|
5703
|
+
(file) => path7.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
|
|
5704
|
+
);
|
|
5705
|
+
return named ?? eligible[0];
|
|
5706
|
+
}
|
|
5707
|
+
// ─── Per-family resolution ──────────────────────────────────────────────────
|
|
5708
|
+
/** Relative specifiers, then workspace package names and their subpaths. */
|
|
5709
|
+
resolveJs(fromFile, spec) {
|
|
5710
|
+
if (spec.startsWith(".")) {
|
|
5711
|
+
const absolute = path7.posix.join(path7.posix.dirname(fromFile), spec);
|
|
5712
|
+
return this.lookupWithExtensions(absolute, "js");
|
|
5713
|
+
}
|
|
5714
|
+
const owner = this.structure.roots.find(
|
|
5715
|
+
(root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
5716
|
+
);
|
|
5717
|
+
if (!owner?.importPath) return void 0;
|
|
5718
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
5719
|
+
if (!subpath) {
|
|
5720
|
+
return this.lookupWithExtensions(path7.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path7.posix.join(owner.dir, "index"), "js");
|
|
5721
|
+
}
|
|
5722
|
+
return this.lookupWithExtensions(path7.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path7.posix.join(owner.dir, "src", subpath), "js");
|
|
5723
|
+
}
|
|
5724
|
+
/** Go import paths are absolute module paths; a package is a directory. */
|
|
5725
|
+
resolveGo(spec) {
|
|
5726
|
+
const owner = this.structure.roots.find(
|
|
5727
|
+
(root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
5728
|
+
);
|
|
5729
|
+
if (!owner?.importPath) return void 0;
|
|
5730
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
5731
|
+
return this.representativeIn(path7.posix.join(owner.dir, subpath), "go");
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
|
|
5735
|
+
* leading dots the extractor preserves (`.sibling`, `..parent.mod`).
|
|
5736
|
+
*/
|
|
5737
|
+
resolvePython(fromFile, spec) {
|
|
5738
|
+
const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
|
|
5739
|
+
if (leadingDots > 0) {
|
|
5740
|
+
let base = path7.posix.dirname(fromFile);
|
|
5741
|
+
for (let i = 1; i < leadingDots; i++) base = path7.posix.dirname(base);
|
|
5742
|
+
const rest = spec.slice(leadingDots).split(".").filter(Boolean);
|
|
5743
|
+
return this.lookupWithExtensions(path7.posix.join(base, ...rest), "py");
|
|
5744
|
+
}
|
|
5745
|
+
const segments = spec.split(".").filter(Boolean);
|
|
5746
|
+
if (segments.length === 0) return void 0;
|
|
5747
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
|
|
5748
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
5749
|
+
const hit = this.lookupWithExtensions(path7.posix.join(base, ...segments), "py");
|
|
5750
|
+
if (hit) return hit;
|
|
5751
|
+
if (segments.length > 1) {
|
|
5752
|
+
const parent = this.lookupWithExtensions(
|
|
5753
|
+
path7.posix.join(base, ...segments.slice(0, -1)),
|
|
5754
|
+
"py"
|
|
5755
|
+
);
|
|
5756
|
+
if (parent) return parent;
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
return void 0;
|
|
5760
|
+
}
|
|
5761
|
+
/** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
|
|
5762
|
+
resolveRust(fromFile, spec) {
|
|
5763
|
+
const segments = spec.split("::").filter(Boolean);
|
|
5764
|
+
if (segments.length === 0) return void 0;
|
|
5765
|
+
const head = segments[0];
|
|
5766
|
+
if (head === "self" || head === "super") {
|
|
5767
|
+
let base = path7.posix.dirname(fromFile);
|
|
5768
|
+
for (const segment of segments) {
|
|
5769
|
+
if (segment === "super") base = path7.posix.dirname(base);
|
|
5770
|
+
else if (segment !== "self") break;
|
|
5771
|
+
}
|
|
5772
|
+
const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
|
|
5773
|
+
return this.lookupWithExtensions(path7.posix.join(base, ...rest2), "rs");
|
|
5774
|
+
}
|
|
5775
|
+
const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
|
|
5776
|
+
const crate = head === "crate" ? owningCrate : this.structure.roots.find(
|
|
5777
|
+
(root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
|
|
5778
|
+
);
|
|
5779
|
+
if (!crate) {
|
|
5780
|
+
return this.lookupWithExtensions(
|
|
5781
|
+
path7.posix.join(path7.posix.dirname(fromFile), ...segments),
|
|
5782
|
+
"rs"
|
|
5783
|
+
);
|
|
5784
|
+
}
|
|
5785
|
+
const rest = segments.slice(1);
|
|
5786
|
+
for (const base of crate.sourceRoots) {
|
|
5787
|
+
const parent = rest.length > 1 ? this.lookupWithExtensions(path7.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
|
|
5788
|
+
const exact = this.lookupWithExtensions(path7.posix.join(base, ...rest), "rs");
|
|
5789
|
+
const hit = exact ?? parent ?? this.lookupWithExtensions(path7.posix.join(base, "lib"), "rs");
|
|
5790
|
+
if (hit) return hit;
|
|
5791
|
+
}
|
|
5792
|
+
return void 0;
|
|
5793
|
+
}
|
|
5794
|
+
/** `com.example.Thing` and `com.example.*` against JVM source roots. */
|
|
5795
|
+
resolveJvm(spec) {
|
|
5796
|
+
const segments = spec.split(".").filter(Boolean);
|
|
5797
|
+
if (segments.length === 0) return void 0;
|
|
5798
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
|
|
5799
|
+
const wildcard = segments[segments.length - 1] === "*";
|
|
5800
|
+
const parts = wildcard ? segments.slice(0, -1) : segments;
|
|
5801
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
5802
|
+
const target = path7.posix.join(base, ...parts);
|
|
5803
|
+
const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
|
|
5804
|
+
if (hit) return hit;
|
|
5805
|
+
}
|
|
5806
|
+
return void 0;
|
|
5807
|
+
}
|
|
5808
|
+
/** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
|
|
5809
|
+
resolveInclude(fromFile, spec) {
|
|
5810
|
+
const relative2 = this.lookupWithExtensions(
|
|
5811
|
+
path7.posix.join(path7.posix.dirname(fromFile), spec),
|
|
5812
|
+
"c"
|
|
5813
|
+
);
|
|
5814
|
+
if (relative2) return relative2;
|
|
5815
|
+
for (const base of [
|
|
5816
|
+
path7.posix.join(this.structure.projectRoot, "include"),
|
|
5817
|
+
this.structure.projectRoot
|
|
5818
|
+
]) {
|
|
5819
|
+
const hit = this.lookupWithExtensions(path7.posix.join(base, spec), "c");
|
|
5820
|
+
if (hit) return hit;
|
|
5821
|
+
}
|
|
5822
|
+
return void 0;
|
|
5823
|
+
}
|
|
5824
|
+
/** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
|
|
5825
|
+
resolveRuby(fromFile, spec) {
|
|
5826
|
+
const relative2 = this.lookupWithExtensions(
|
|
5827
|
+
path7.posix.join(path7.posix.dirname(fromFile), spec),
|
|
5828
|
+
"ruby"
|
|
5829
|
+
);
|
|
5830
|
+
if (relative2) return relative2;
|
|
5831
|
+
for (const base of [
|
|
5832
|
+
path7.posix.join(this.structure.projectRoot, "lib"),
|
|
5833
|
+
this.structure.projectRoot
|
|
5834
|
+
]) {
|
|
5835
|
+
const hit = this.lookupWithExtensions(path7.posix.join(base, spec), "ruby");
|
|
5836
|
+
if (hit) return hit;
|
|
5837
|
+
}
|
|
5838
|
+
return void 0;
|
|
5839
|
+
}
|
|
5840
|
+
};
|
|
5841
|
+
|
|
5842
|
+
// src/codebase-index/import-extractor.ts
|
|
5843
|
+
var IMPORT_MAX_FILE_CHARS = 512 * 1024;
|
|
5844
|
+
var IMPORT_MAX_PER_FILE = 400;
|
|
5845
|
+
var DOTTED_IMPORT = [
|
|
5846
|
+
{ re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
|
|
5847
|
+
];
|
|
5848
|
+
var LANG_IMPORTS = {
|
|
5849
|
+
// Go and Python have real AST extractors; these patterns are the fallback for
|
|
5850
|
+
// machines with no Go toolchain or Python interpreter installed, where the
|
|
5851
|
+
// parser degrades to regex symbols and would otherwise contribute no edges.
|
|
5852
|
+
go: [
|
|
5853
|
+
{ re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
|
|
5854
|
+
// Grouped form: inside `import ( … )` each line is an optional alias plus a
|
|
5855
|
+
// quoted path. A stray match elsewhere resolves to no file and is dropped.
|
|
5856
|
+
{ re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
|
|
5857
|
+
],
|
|
5858
|
+
py: [
|
|
5859
|
+
{ re: /^[ \t]*import\s+([\w.]+)/gm },
|
|
5860
|
+
{ re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
|
|
5861
|
+
],
|
|
5862
|
+
rs: [
|
|
5863
|
+
// use a::b::C; | use a::b::{C, D}; → the path before any brace
|
|
5864
|
+
{ re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
|
|
5865
|
+
// mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
|
|
5866
|
+
{ re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
|
|
5867
|
+
],
|
|
5868
|
+
java: DOTTED_IMPORT,
|
|
5869
|
+
kotlin: DOTTED_IMPORT,
|
|
5870
|
+
scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
|
|
5871
|
+
csharp: [
|
|
5872
|
+
// using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
|
|
5873
|
+
{ re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
|
|
5874
|
+
{ re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
|
|
5875
|
+
],
|
|
5876
|
+
// Quoted includes only: <stdio.h> is a system header with no indexed file.
|
|
5877
|
+
c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
|
|
5878
|
+
cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
|
|
5879
|
+
ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
|
|
5880
|
+
php: [
|
|
5881
|
+
// `use A\B\C` imports the class C, which is what the index has a symbol
|
|
5882
|
+
// for — the namespace symbol only covers the `A\B` prefix.
|
|
5883
|
+
{ re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
|
|
5884
|
+
{ re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
|
|
5885
|
+
],
|
|
5886
|
+
swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
|
|
5887
|
+
dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
|
|
5888
|
+
lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
|
|
5889
|
+
elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
|
|
5890
|
+
haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
|
|
5891
|
+
zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
|
|
5892
|
+
proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
|
|
5893
|
+
// `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
|
|
5894
|
+
css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
|
|
5895
|
+
// A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
|
|
5896
|
+
vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
|
|
5897
|
+
svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
|
|
5898
|
+
html: [
|
|
5899
|
+
{ re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
|
|
5900
|
+
{ re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
|
|
5901
|
+
],
|
|
5902
|
+
shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
|
|
5903
|
+
r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
|
|
5904
|
+
};
|
|
5905
|
+
function lastSegment(specifier) {
|
|
5906
|
+
const pathLike = /[/\\]|::/.test(specifier);
|
|
5907
|
+
const segments = specifier.split(/[/\\]|::/).filter(Boolean);
|
|
5908
|
+
let last = segments[segments.length - 1] ?? specifier;
|
|
5909
|
+
if (last === "*" || last === "_") {
|
|
5910
|
+
last = segments[segments.length - 2] ?? specifier;
|
|
5911
|
+
}
|
|
5912
|
+
if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
|
|
5913
|
+
const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
|
|
5914
|
+
return dotted[dotted.length - 1] ?? last;
|
|
5915
|
+
}
|
|
5916
|
+
function newlineOffsets(content) {
|
|
5917
|
+
const offsets = [];
|
|
5918
|
+
for (let i = 0; i < content.length; i++) {
|
|
5919
|
+
if (content.charCodeAt(i) === 10) offsets.push(i);
|
|
5920
|
+
}
|
|
5921
|
+
return offsets;
|
|
5922
|
+
}
|
|
5923
|
+
function lineAt(offsets, index) {
|
|
5924
|
+
let low = 0;
|
|
5925
|
+
let high = offsets.length;
|
|
5926
|
+
while (low < high) {
|
|
5927
|
+
const mid = low + high >>> 1;
|
|
5928
|
+
if ((offsets[mid] ?? 0) < index) low = mid + 1;
|
|
5929
|
+
else high = mid;
|
|
5930
|
+
}
|
|
5931
|
+
return low + 1;
|
|
5932
|
+
}
|
|
5933
|
+
function hasImportPatterns(lang) {
|
|
5934
|
+
return LANG_IMPORTS[lang] !== void 0;
|
|
5935
|
+
}
|
|
5936
|
+
function extractImports(opts) {
|
|
5937
|
+
const patterns = LANG_IMPORTS[opts.lang];
|
|
5938
|
+
if (!patterns || !opts.content) return [];
|
|
5939
|
+
const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
|
|
5940
|
+
const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
|
|
5941
|
+
const refs = [];
|
|
5942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5943
|
+
const offsets = newlineOffsets(content);
|
|
5944
|
+
for (const pattern of patterns) {
|
|
5945
|
+
const re = new RegExp(pattern.re.source, pattern.re.flags);
|
|
5946
|
+
for (const match of content.matchAll(re)) {
|
|
5947
|
+
if (refs.length >= limit) return refs;
|
|
5948
|
+
const specifier = match[1]?.trim();
|
|
5949
|
+
if (!specifier) continue;
|
|
5950
|
+
const module = specifier;
|
|
5951
|
+
const toName = pattern.name === "full" ? module : lastSegment(module);
|
|
5952
|
+
if (!toName) continue;
|
|
5953
|
+
const key = `${module}\0${toName}`;
|
|
5954
|
+
if (seen.has(key)) continue;
|
|
5955
|
+
seen.add(key);
|
|
5956
|
+
refs.push({
|
|
5957
|
+
fromId: 0,
|
|
5958
|
+
toName,
|
|
5959
|
+
callType: "import",
|
|
5960
|
+
line: lineAt(offsets, match.index ?? 0),
|
|
5961
|
+
lang: opts.lang,
|
|
5962
|
+
module
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
return refs;
|
|
5967
|
+
}
|
|
5968
|
+
|
|
4864
5969
|
// src/codebase-index/parser-dispatch.ts
|
|
4865
5970
|
async function parseFileContent(file, content, lang) {
|
|
5971
|
+
const parsed = await dispatch(file, content, lang);
|
|
5972
|
+
return withRelations(parsed, content, lang);
|
|
5973
|
+
}
|
|
5974
|
+
async function dispatch(file, content, lang) {
|
|
4866
5975
|
switch (lang) {
|
|
4867
5976
|
case "ts":
|
|
4868
5977
|
case "tsx":
|
|
@@ -4897,6 +6006,13 @@ async function parseFileContent(file, content, lang) {
|
|
|
4897
6006
|
}
|
|
4898
6007
|
}
|
|
4899
6008
|
}
|
|
6009
|
+
function withRelations(parsed, content, lang) {
|
|
6010
|
+
let refs = parsed.refs ?? [];
|
|
6011
|
+
if (refs.length === 0 && hasImportPatterns(lang)) {
|
|
6012
|
+
refs = extractImports({ content, lang });
|
|
6013
|
+
}
|
|
6014
|
+
return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
|
|
6015
|
+
}
|
|
4900
6016
|
|
|
4901
6017
|
// src/codebase-index/indexer.ts
|
|
4902
6018
|
var YIELD_EVERY_N = 50;
|
|
@@ -4933,7 +6049,7 @@ function normalizeComparablePath(value) {
|
|
|
4933
6049
|
}
|
|
4934
6050
|
function gitOutput(projectRoot, args) {
|
|
4935
6051
|
return new Promise((resolve4, reject) => {
|
|
4936
|
-
|
|
6052
|
+
execFile(
|
|
4937
6053
|
"git",
|
|
4938
6054
|
["-C", projectRoot, ...args],
|
|
4939
6055
|
{
|
|
@@ -5064,13 +6180,40 @@ function assignRefsToSymbols2(refs, symbols) {
|
|
|
5064
6180
|
}
|
|
5065
6181
|
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
5066
6182
|
if (!owner || owner.id <= 0) continue;
|
|
5067
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
6183
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
5068
6184
|
if (seen.has(key)) continue;
|
|
5069
6185
|
seen.add(key);
|
|
5070
6186
|
assigned.push({ ...ref, fromId: owner.id });
|
|
5071
6187
|
}
|
|
5072
6188
|
return assigned;
|
|
5073
6189
|
}
|
|
6190
|
+
async function resolveProjectRelations(store, projectRoot, opts) {
|
|
6191
|
+
if (opts.signal?.aborted) return;
|
|
6192
|
+
try {
|
|
6193
|
+
const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
|
|
6194
|
+
if (indexedFiles.length === 0) return;
|
|
6195
|
+
const structure = await detectModuleRoots(projectRoot, indexedFiles);
|
|
6196
|
+
if (opts.signal?.aborted) return;
|
|
6197
|
+
store.setFilePackages(assignPackageLabels(structure, indexedFiles));
|
|
6198
|
+
const resolver = new ModuleResolver(
|
|
6199
|
+
structure,
|
|
6200
|
+
indexedFiles,
|
|
6201
|
+
store.getNamespaceDeclarations()
|
|
6202
|
+
);
|
|
6203
|
+
const pending2 = store.getUnresolvedImports(opts.onlyFiles);
|
|
6204
|
+
const resolutions = [];
|
|
6205
|
+
for (const entry of pending2) {
|
|
6206
|
+
const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
|
|
6207
|
+
if (toFile && toFile !== entry.fromFile) {
|
|
6208
|
+
resolutions.push({ ...entry, toFile });
|
|
6209
|
+
}
|
|
6210
|
+
}
|
|
6211
|
+
if (opts.signal?.aborted) return;
|
|
6212
|
+
store.applyImportResolutions(resolutions);
|
|
6213
|
+
} catch (err) {
|
|
6214
|
+
opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
5074
6217
|
async function runIndexer(_ctx, opts) {
|
|
5075
6218
|
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
5076
6219
|
try {
|
|
@@ -5326,6 +6469,14 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5326
6469
|
}
|
|
5327
6470
|
}
|
|
5328
6471
|
if (needsFullRefResolution) store.resolveRefs();
|
|
6472
|
+
await resolveProjectRelations(store, projectRoot, {
|
|
6473
|
+
// A watcher run re-resolves only what it touched; a full run (or a contract
|
|
6474
|
+
// bump) re-resolves everything, because a newly indexed file can be the
|
|
6475
|
+
// target of imports written long before it.
|
|
6476
|
+
onlyFiles: needsFullRefResolution ? void 0 : opts.files,
|
|
6477
|
+
errors,
|
|
6478
|
+
signal
|
|
6479
|
+
});
|
|
5329
6480
|
store.setMetadata("ref_resolution_version", refResolutionVersion);
|
|
5330
6481
|
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
5331
6482
|
if (!opts.files || filesIndexed >= 50) store.optimize();
|