@maxgfr/codeindex 2.11.0 → 2.11.1
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/README.md +2 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +2 -2
- package/scripts/engine.mjs +86 -60
- package/src/extract/code.ts +54 -58
- package/src/lang/common.ts +64 -0
- package/src/lang/registry.ts +21 -6
- package/src/types.ts +7 -3
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# codeindex
|
|
2
2
|
|
|
3
|
+
[](https://maxgfr.github.io/codeindex/)
|
|
4
|
+
|
|
3
5
|
Self-contained, deterministic **repo-indexing engine**: file walking, language
|
|
4
6
|
detection, symbol/import extraction (tree-sitter AST with a regex fallback),
|
|
5
7
|
import resolution, a typed cross-file link-graph, and graph analytics — shipped
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxgfr/codeindex",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.1",
|
|
4
4
|
"description": "Self-contained, deterministic repo-indexing engine: walk + language detection + symbol/import extraction (tree-sitter AST with regex fallback) + import resolution + typed cross-file link-graph + analytics. Ships as a single zero-dependency engine.mjs that consumer tools vendor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/engine.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
declare const ENGINE_VERSION = "2.11.
|
|
1
|
+
declare const ENGINE_VERSION = "2.11.1";
|
|
2
2
|
declare const SCHEMA_VERSION = 4;
|
|
3
|
-
declare const EXTRACTOR_VERSION =
|
|
3
|
+
declare const EXTRACTOR_VERSION = 8;
|
|
4
4
|
type FileKind = "code" | "doc" | "config" | "asset" | "other";
|
|
5
5
|
type EdgeKind = "contains" | "doc-link" | "import" | "call" | "use" | "mention";
|
|
6
6
|
type Tier = 0 | 1 | 2;
|
package/scripts/engine.mjs
CHANGED
|
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
|
|
|
14
14
|
var init_types = __esm({
|
|
15
15
|
"src/types.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
ENGINE_VERSION = "2.11.
|
|
17
|
+
ENGINE_VERSION = "2.11.1";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
|
-
EXTRACTOR_VERSION =
|
|
19
|
+
EXTRACTOR_VERSION = 8;
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -731,7 +731,57 @@ function scan(rel, content, lang, rules) {
|
|
|
731
731
|
function extToLang(ext) {
|
|
732
732
|
return EXT_LANG[ext] ?? "other";
|
|
733
733
|
}
|
|
734
|
-
|
|
734
|
+
function extractReexports(rel, content, localSymbols) {
|
|
735
|
+
if (!REEXPORT_EXTS.has(rel.slice(rel.lastIndexOf(".")))) return [];
|
|
736
|
+
const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
|
|
737
|
+
const out2 = [];
|
|
738
|
+
const seen = /* @__PURE__ */ new Set();
|
|
739
|
+
const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
|
|
740
|
+
const localKindOf = /* @__PURE__ */ new Map();
|
|
741
|
+
for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
|
|
742
|
+
const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
|
|
743
|
+
let m;
|
|
744
|
+
while ((m = named.exec(content)) && out2.length < 60) {
|
|
745
|
+
const from = m[2];
|
|
746
|
+
for (const part of m[1].split(",")) {
|
|
747
|
+
const p = part.trim().replace(/^type\s+/, "");
|
|
748
|
+
const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
|
|
749
|
+
const orig = as ? as[1] : p;
|
|
750
|
+
const name2 = as ? as[2] : p;
|
|
751
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
|
|
752
|
+
seen.add(name2);
|
|
753
|
+
const mirroredKind = !from ? localKindOf.get(orig) : void 0;
|
|
754
|
+
out2.push({
|
|
755
|
+
name: name2,
|
|
756
|
+
kind: mirroredKind ?? "reexport",
|
|
757
|
+
file: rel,
|
|
758
|
+
line: lineAt(m.index),
|
|
759
|
+
signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
|
|
760
|
+
exported: true,
|
|
761
|
+
lang
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
|
|
766
|
+
while ((m = star.exec(content)) && out2.length < 60) {
|
|
767
|
+
const ns = m[1];
|
|
768
|
+
const from = m[2];
|
|
769
|
+
const key = "*" + (ns ?? from);
|
|
770
|
+
if (seen.has(key)) continue;
|
|
771
|
+
seen.add(key);
|
|
772
|
+
out2.push({
|
|
773
|
+
name: ns ?? `* (${from})`,
|
|
774
|
+
kind: ns ? "reexport" : "reexport-all",
|
|
775
|
+
file: rel,
|
|
776
|
+
line: lineAt(m.index),
|
|
777
|
+
signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
|
|
778
|
+
exported: true,
|
|
779
|
+
lang
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return out2;
|
|
783
|
+
}
|
|
784
|
+
var EXT_LANG, REEXPORT_EXTS;
|
|
735
785
|
var init_common = __esm({
|
|
736
786
|
"src/lang/common.ts"() {
|
|
737
787
|
"use strict";
|
|
@@ -799,6 +849,7 @@ var init_common = __esm({
|
|
|
799
849
|
".svelte": "svelte",
|
|
800
850
|
".astro": "astro"
|
|
801
851
|
};
|
|
852
|
+
REEXPORT_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
802
853
|
}
|
|
803
854
|
});
|
|
804
855
|
|
|
@@ -1231,12 +1282,18 @@ var init_scala = __esm({
|
|
|
1231
1282
|
// src/lang/registry.ts
|
|
1232
1283
|
function extractSymbols(rel, ext, content) {
|
|
1233
1284
|
const extractor = BY_EXT.get(ext);
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1285
|
+
let symbols;
|
|
1286
|
+
if (!extractor) symbols = [];
|
|
1287
|
+
else {
|
|
1288
|
+
try {
|
|
1289
|
+
symbols = extractor.extract(rel, content);
|
|
1290
|
+
} catch {
|
|
1291
|
+
symbols = [];
|
|
1292
|
+
}
|
|
1239
1293
|
}
|
|
1294
|
+
const known = new Set(symbols.map((s) => s.name));
|
|
1295
|
+
const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
|
|
1296
|
+
return reexports.length ? [...symbols, ...reexports] : symbols;
|
|
1240
1297
|
}
|
|
1241
1298
|
function languageOf(ext) {
|
|
1242
1299
|
return BY_EXT.get(ext)?.lang ?? extToLang(ext);
|
|
@@ -6340,58 +6397,9 @@ function extractImports(ext, content) {
|
|
|
6340
6397
|
}
|
|
6341
6398
|
return [...specs].map((spec) => ({ kind: "import", spec }));
|
|
6342
6399
|
}
|
|
6343
|
-
function
|
|
6344
|
-
if (!JS_TS.has(rel.slice(rel.lastIndexOf(".")))) return [];
|
|
6345
|
-
const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
|
|
6346
|
-
const out2 = [];
|
|
6347
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6348
|
-
const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
|
|
6349
|
-
const localKindOf = /* @__PURE__ */ new Map();
|
|
6350
|
-
for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
|
|
6351
|
-
const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
|
|
6352
|
-
let m;
|
|
6353
|
-
while ((m = named.exec(content)) && out2.length < 60) {
|
|
6354
|
-
const from = m[2];
|
|
6355
|
-
for (const part of m[1].split(",")) {
|
|
6356
|
-
const p = part.trim().replace(/^type\s+/, "");
|
|
6357
|
-
const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
|
|
6358
|
-
const orig = as ? as[1] : p;
|
|
6359
|
-
const name2 = as ? as[2] : p;
|
|
6360
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
|
|
6361
|
-
seen.add(name2);
|
|
6362
|
-
const mirroredKind = !from ? localKindOf.get(orig) : void 0;
|
|
6363
|
-
out2.push({
|
|
6364
|
-
name: name2,
|
|
6365
|
-
kind: mirroredKind ?? "reexport",
|
|
6366
|
-
file: rel,
|
|
6367
|
-
line: lineAt(m.index),
|
|
6368
|
-
signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
|
|
6369
|
-
exported: true,
|
|
6370
|
-
lang
|
|
6371
|
-
});
|
|
6372
|
-
}
|
|
6373
|
-
}
|
|
6374
|
-
const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
|
|
6375
|
-
while ((m = star.exec(content)) && out2.length < 60) {
|
|
6376
|
-
const ns = m[1];
|
|
6377
|
-
const from = m[2];
|
|
6378
|
-
const key = "*" + (ns ?? from);
|
|
6379
|
-
if (seen.has(key)) continue;
|
|
6380
|
-
seen.add(key);
|
|
6381
|
-
out2.push({
|
|
6382
|
-
name: ns ?? `* (${from})`,
|
|
6383
|
-
kind: ns ? "reexport" : "reexport-all",
|
|
6384
|
-
file: rel,
|
|
6385
|
-
line: lineAt(m.index),
|
|
6386
|
-
signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
|
|
6387
|
-
exported: true,
|
|
6388
|
-
lang
|
|
6389
|
-
});
|
|
6390
|
-
}
|
|
6391
|
-
return out2;
|
|
6392
|
-
}
|
|
6393
|
-
function collectCallsRegex(content) {
|
|
6400
|
+
function collectCallsRegex(content, symbols = []) {
|
|
6394
6401
|
const out2 = /* @__PURE__ */ new Map();
|
|
6402
|
+
const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
|
|
6395
6403
|
const lines = content.split("\n");
|
|
6396
6404
|
const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
6397
6405
|
for (let i2 = 0; i2 < lines.length && out2.size < 512; i2++) {
|
|
@@ -6399,13 +6407,28 @@ function collectCallsRegex(content) {
|
|
|
6399
6407
|
const trimmed = line.trimStart();
|
|
6400
6408
|
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
|
|
6401
6409
|
CALL_RE.lastIndex = 0;
|
|
6410
|
+
let probe;
|
|
6411
|
+
const introducerCaught = /* @__PURE__ */ new Set();
|
|
6412
|
+
while ((probe = CALL_RE.exec(line)) !== null) {
|
|
6413
|
+
const name2 = probe[2];
|
|
6414
|
+
const key = `${name2} ${i2 + 1}`;
|
|
6415
|
+
if (ownDefLines.has(key) && DEF_INTRODUCERS.test(line.slice(0, probe.index))) introducerCaught.add(key);
|
|
6416
|
+
}
|
|
6417
|
+
CALL_RE.lastIndex = 0;
|
|
6402
6418
|
let m;
|
|
6419
|
+
const fallbackExcluded = /* @__PURE__ */ new Set();
|
|
6403
6420
|
while ((m = CALL_RE.exec(line)) !== null && out2.size < 512) {
|
|
6404
6421
|
const receiver = m[1];
|
|
6405
6422
|
const name2 = m[2];
|
|
6406
6423
|
if (name2.length < 2 || CALL_KEYWORDS.has(name2)) continue;
|
|
6407
6424
|
if (DEF_INTRODUCERS.test(line.slice(0, m.index))) continue;
|
|
6408
6425
|
const key = `${name2} ${i2 + 1}`;
|
|
6426
|
+
if (ownDefLines.has(key) && !introducerCaught.has(key)) {
|
|
6427
|
+
if (!fallbackExcluded.has(key)) {
|
|
6428
|
+
fallbackExcluded.add(key);
|
|
6429
|
+
continue;
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6409
6432
|
if (!out2.has(key)) out2.set(key, receiver ? { name: name2, line: i2 + 1, receiver } : { name: name2, line: i2 + 1 });
|
|
6410
6433
|
}
|
|
6411
6434
|
}
|
|
@@ -6426,7 +6449,9 @@ function extractCode(rel, ext, content) {
|
|
|
6426
6449
|
idents: ast?.idents,
|
|
6427
6450
|
// AST call sites when a grammar parsed the file; the conservative regex
|
|
6428
6451
|
// collector otherwise, so caller indexes exist without the wasm sidecar.
|
|
6429
|
-
|
|
6452
|
+
// `symbols` (this file's own regex-extracted defs) lets the collector
|
|
6453
|
+
// exclude a definition's own name+line from its call candidates.
|
|
6454
|
+
calls: ast ? ast.calls : collectCallsRegex(content, symbols),
|
|
6430
6455
|
importedNames: ast?.importedNames
|
|
6431
6456
|
};
|
|
6432
6457
|
}
|
|
@@ -6436,6 +6461,7 @@ var init_code = __esm({
|
|
|
6436
6461
|
"use strict";
|
|
6437
6462
|
init_registry();
|
|
6438
6463
|
init_extract();
|
|
6464
|
+
init_common();
|
|
6439
6465
|
JS_TS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6440
6466
|
PY = /* @__PURE__ */ new Set([".py", ".pyi"]);
|
|
6441
6467
|
C_CPP = /* @__PURE__ */ new Set([".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"]);
|
package/src/extract/code.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CodeSymbol, RawRef } from "../types.js";
|
|
2
2
|
import { extractSymbols } from "../lang/registry.js";
|
|
3
3
|
import { extractAst } from "../ast/extract.js";
|
|
4
|
+
import { extractReexports } from "../lang/common.js";
|
|
4
5
|
|
|
5
6
|
export interface CodeInfo {
|
|
6
7
|
symbols: CodeSymbol[];
|
|
@@ -253,62 +254,6 @@ function extractImports(ext: string, content: string): RawRef[] {
|
|
|
253
254
|
return [...specs].map((spec) => ({ kind: "import" as const, spec }));
|
|
254
255
|
}
|
|
255
256
|
|
|
256
|
-
// Barrel re-exports (`export { A, B as C } from './x'`, `export * from './y'`).
|
|
257
|
-
// The line-based lang extractor can't capture multi-name lists, but these ARE
|
|
258
|
-
// the public facade of a module — so list them as exported symbols here.
|
|
259
|
-
//
|
|
260
|
-
// An ALIAS with no `from` clause (`export { b as c }`) renames an in-file
|
|
261
|
-
// declaration — `localSymbols` (already extracted by the AST or regex tier)
|
|
262
|
-
// lets us resolve `b` and mirror ITS kind onto `c` (e.g. "function"), so the
|
|
263
|
-
// alias reads as the real symbol it is rather than the generic "reexport".
|
|
264
|
-
// A true cross-module re-export (`export { b as c } from "./mod"`) has no
|
|
265
|
-
// local `b` to resolve — and an alias the local pass genuinely can't see
|
|
266
|
-
// (destructured/ambient/etc.) falls back the same way — both keep "reexport".
|
|
267
|
-
function extractReexports(rel: string, content: string, localSymbols: CodeSymbol[]): CodeSymbol[] {
|
|
268
|
-
if (!JS_TS.has(rel.slice(rel.lastIndexOf(".")))) return [];
|
|
269
|
-
const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
|
|
270
|
-
const out: CodeSymbol[] = [];
|
|
271
|
-
const seen = new Set<string>();
|
|
272
|
-
const lineAt = (idx: number): number => content.slice(0, idx).split(/\r?\n/).length;
|
|
273
|
-
const localKindOf = new Map<string, string>();
|
|
274
|
-
for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
|
|
275
|
-
|
|
276
|
-
const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
|
|
277
|
-
let m: RegExpExecArray | null;
|
|
278
|
-
while ((m = named.exec(content)) && out.length < 60) {
|
|
279
|
-
const from = m[2];
|
|
280
|
-
for (const part of m[1]!.split(",")) {
|
|
281
|
-
const p = part.trim().replace(/^type\s+/, "");
|
|
282
|
-
const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
|
|
283
|
-
const orig = as ? as[1]! : p;
|
|
284
|
-
const name = as ? as[2]! : p;
|
|
285
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(name) || name === "default" || seen.has(name)) continue;
|
|
286
|
-
seen.add(name);
|
|
287
|
-
const mirroredKind = !from ? localKindOf.get(orig) : undefined;
|
|
288
|
-
out.push({
|
|
289
|
-
name, kind: mirroredKind ?? "reexport", file: rel, line: lineAt(m.index),
|
|
290
|
-
signature: from ? `export { ${name} } from "${from}"` : `export { ${name} }`,
|
|
291
|
-
exported: true, lang,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
|
|
297
|
-
while ((m = star.exec(content)) && out.length < 60) {
|
|
298
|
-
const ns = m[1];
|
|
299
|
-
const from = m[2]!;
|
|
300
|
-
const key = "*" + (ns ?? from);
|
|
301
|
-
if (seen.has(key)) continue;
|
|
302
|
-
seen.add(key);
|
|
303
|
-
out.push({
|
|
304
|
-
name: ns ?? `* (${from})`, kind: ns ? "reexport" : "reexport-all", file: rel,
|
|
305
|
-
line: lineAt(m.index), signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
|
|
306
|
-
exported: true, lang,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
return out;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
257
|
// Control-flow and declaration keywords that syntactically precede `(` but are
|
|
313
258
|
// never call targets — the union across supported languages. Deliberately does
|
|
314
259
|
// NOT list real builtins (python's `print`, go's `make`…): a false call to a
|
|
@@ -331,8 +276,35 @@ const DEF_INTRODUCERS = /(?:\bfunction|\bdef|\bfunc|\bfun|\bfn|\bclass|\bsub|\bm
|
|
|
331
276
|
// immediate `receiver.` prefix is captured too (`axios.get(` → receiver
|
|
332
277
|
// "axios"; `a.b.c(` → receiver "b" — the group anchors to the segment right
|
|
333
278
|
// before the called name); bare calls carry no receiver.
|
|
334
|
-
|
|
279
|
+
//
|
|
280
|
+
// `symbols` is the file's OWN regex-extracted symbols (name + definition
|
|
281
|
+
// line). DEF_INTRODUCERS already excludes definitions that read `function
|
|
282
|
+
// foo(`/`def foo(`/etc. (per OCCURRENCE, wherever on the line it sits), but
|
|
283
|
+
// C/C++ function definitions have no such introducer (`void load(void) {`) —
|
|
284
|
+
// the bare name reads exactly like a call to itself on its own definition
|
|
285
|
+
// line. For those, a call candidate whose (name, line) exactly matches one of
|
|
286
|
+
// `symbols` is excluded too — but ONLY its first (leftmost) occurrence on that
|
|
287
|
+
// line, never every same-named occurrence: dense/minified one-liners can pack
|
|
288
|
+
// a genuine call to the same name on the same physical line as its own
|
|
289
|
+
// definition (`function aa(){}function bb(){return aa()+cc()}function cc(){}`
|
|
290
|
+
// — bb's calls to aa() and cc() must survive), and even a bodyless
|
|
291
|
+
// single-line recursive definition (`function foo(){foo();}`) has a real self
|
|
292
|
+
// -call to keep. Two-tier: if ANY occurrence of a def'd name on this line is
|
|
293
|
+
// already caught by DEF_INTRODUCERS (JS/Python/…), that occurrence alone is
|
|
294
|
+
// excluded (existing per-occurrence check below) and no further exclusion is
|
|
295
|
+
// applied — every OTHER occurrence is a genuine call. Only when NO occurrence
|
|
296
|
+
// carries an introducer (C/C++) does this fall back to excluding just the
|
|
297
|
+
// first occurrence: C/C++'s own definition regex requires column 0, so on a
|
|
298
|
+
// line where it matches at all, the first occurrence IS that definition.
|
|
299
|
+
// Exported for direct testing (extraction-v8.test.ts): once a wasm sidecar/
|
|
300
|
+
// grammar is loaded, extractCode never reaches this path for C/C++, so tests
|
|
301
|
+
// exercise it directly rather than through extractCode.
|
|
302
|
+
export function collectCallsRegex(
|
|
303
|
+
content: string,
|
|
304
|
+
symbols: Pick<CodeSymbol, "name" | "line">[] = [],
|
|
305
|
+
): { name: string; line: number; receiver?: string }[] {
|
|
335
306
|
const out = new Map<string, { name: string; line: number; receiver?: string }>();
|
|
307
|
+
const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
|
|
336
308
|
const lines = content.split("\n");
|
|
337
309
|
const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
338
310
|
for (let i = 0; i < lines.length && out.size < 512; i++) {
|
|
@@ -342,14 +314,36 @@ function collectCallsRegex(content: string): { name: string; line: number; recei
|
|
|
342
314
|
// regexes — noise resolves to nothing in the global call pass).
|
|
343
315
|
const trimmed = line.trimStart();
|
|
344
316
|
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
|
|
317
|
+
|
|
318
|
+
// Pass 1: which own-def keys on this line have at least one occurrence
|
|
319
|
+
// DEF_INTRODUCERS already catches? Those are fully handled per-occurrence
|
|
320
|
+
// below — no fallback exclusion needed (or wanted) for them.
|
|
321
|
+
CALL_RE.lastIndex = 0;
|
|
322
|
+
let probe: RegExpExecArray | null;
|
|
323
|
+
const introducerCaught = new Set<string>();
|
|
324
|
+
while ((probe = CALL_RE.exec(line)) !== null) {
|
|
325
|
+
const name = probe[2]!;
|
|
326
|
+
const key = `${name} ${i + 1}`;
|
|
327
|
+
if (ownDefLines.has(key) && DEF_INTRODUCERS.test(line.slice(0, probe.index))) introducerCaught.add(key);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Pass 2: the real collection. Own-def keys with no introducer occurrence
|
|
331
|
+
// fall back to excluding just their first occurrence on the line.
|
|
345
332
|
CALL_RE.lastIndex = 0;
|
|
346
333
|
let m: RegExpExecArray | null;
|
|
334
|
+
const fallbackExcluded = new Set<string>();
|
|
347
335
|
while ((m = CALL_RE.exec(line)) !== null && out.size < 512) {
|
|
348
336
|
const receiver = m[1];
|
|
349
337
|
const name = m[2]!;
|
|
350
338
|
if (name.length < 2 || CALL_KEYWORDS.has(name)) continue;
|
|
351
339
|
if (DEF_INTRODUCERS.test(line.slice(0, m.index))) continue;
|
|
352
340
|
const key = `${name} ${i + 1}`;
|
|
341
|
+
if (ownDefLines.has(key) && !introducerCaught.has(key)) {
|
|
342
|
+
if (!fallbackExcluded.has(key)) {
|
|
343
|
+
fallbackExcluded.add(key);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
353
347
|
if (!out.has(key)) out.set(key, receiver ? { name, line: i + 1, receiver } : { name, line: i + 1 });
|
|
354
348
|
}
|
|
355
349
|
}
|
|
@@ -382,7 +376,9 @@ export function extractCode(rel: string, ext: string, content: string): CodeInfo
|
|
|
382
376
|
idents: ast?.idents,
|
|
383
377
|
// AST call sites when a grammar parsed the file; the conservative regex
|
|
384
378
|
// collector otherwise, so caller indexes exist without the wasm sidecar.
|
|
385
|
-
|
|
379
|
+
// `symbols` (this file's own regex-extracted defs) lets the collector
|
|
380
|
+
// exclude a definition's own name+line from its call candidates.
|
|
381
|
+
calls: ast ? ast.calls : collectCallsRegex(content, symbols),
|
|
386
382
|
importedNames: ast?.importedNames,
|
|
387
383
|
};
|
|
388
384
|
}
|
package/src/lang/common.ts
CHANGED
|
@@ -66,3 +66,67 @@ const EXT_LANG: Record<string, string> = {
|
|
|
66
66
|
export function extToLang(ext: string): string {
|
|
67
67
|
return EXT_LANG[ext] ?? "other";
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
const REEXPORT_EXTS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
71
|
+
|
|
72
|
+
// Barrel re-exports (`export { A, B as C } from './x'`, `export * from './y'`).
|
|
73
|
+
// The line-based lang extractor can't capture multi-name lists, but these ARE
|
|
74
|
+
// the public facade of a module — so list them as exported symbols here.
|
|
75
|
+
//
|
|
76
|
+
// An ALIAS with no `from` clause (`export { b as c }`) renames an in-file
|
|
77
|
+
// declaration — `localSymbols` (already extracted by the AST or regex tier)
|
|
78
|
+
// lets us resolve `b` and mirror ITS kind onto `c` (e.g. "function"), so the
|
|
79
|
+
// alias reads as the real symbol it is rather than the generic "reexport".
|
|
80
|
+
// A true cross-module re-export (`export { b as c } from "./mod"`) has no
|
|
81
|
+
// local `b` to resolve — and an alias the local pass genuinely can't see
|
|
82
|
+
// (destructured/ambient/etc.) falls back the same way — both keep "reexport".
|
|
83
|
+
//
|
|
84
|
+
// Shared by extractCode (extract/code.ts) AND the standalone extractSymbols
|
|
85
|
+
// (lang/registry.ts) — ultradoc and other direct extractSymbols consumers hit
|
|
86
|
+
// the same barrels a repo scan does, so both entry points must agree; this is
|
|
87
|
+
// the one place the alias-mirroring logic lives, reused rather than
|
|
88
|
+
// reimplemented on either side.
|
|
89
|
+
export function extractReexports(rel: string, content: string, localSymbols: CodeSymbol[]): CodeSymbol[] {
|
|
90
|
+
if (!REEXPORT_EXTS.has(rel.slice(rel.lastIndexOf(".")))) return [];
|
|
91
|
+
const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
|
|
92
|
+
const out: CodeSymbol[] = [];
|
|
93
|
+
const seen = new Set<string>();
|
|
94
|
+
const lineAt = (idx: number): number => content.slice(0, idx).split(/\r?\n/).length;
|
|
95
|
+
const localKindOf = new Map<string, string>();
|
|
96
|
+
for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
|
|
97
|
+
|
|
98
|
+
const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
|
|
99
|
+
let m: RegExpExecArray | null;
|
|
100
|
+
while ((m = named.exec(content)) && out.length < 60) {
|
|
101
|
+
const from = m[2];
|
|
102
|
+
for (const part of m[1]!.split(",")) {
|
|
103
|
+
const p = part.trim().replace(/^type\s+/, "");
|
|
104
|
+
const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
|
|
105
|
+
const orig = as ? as[1]! : p;
|
|
106
|
+
const name = as ? as[2]! : p;
|
|
107
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name) || name === "default" || seen.has(name)) continue;
|
|
108
|
+
seen.add(name);
|
|
109
|
+
const mirroredKind = !from ? localKindOf.get(orig) : undefined;
|
|
110
|
+
out.push({
|
|
111
|
+
name, kind: mirroredKind ?? "reexport", file: rel, line: lineAt(m.index),
|
|
112
|
+
signature: from ? `export { ${name} } from "${from}"` : `export { ${name} }`,
|
|
113
|
+
exported: true, lang,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
|
|
119
|
+
while ((m = star.exec(content)) && out.length < 60) {
|
|
120
|
+
const ns = m[1];
|
|
121
|
+
const from = m[2]!;
|
|
122
|
+
const key = "*" + (ns ?? from);
|
|
123
|
+
if (seen.has(key)) continue;
|
|
124
|
+
seen.add(key);
|
|
125
|
+
out.push({
|
|
126
|
+
name: ns ?? `* (${from})`, kind: ns ? "reexport" : "reexport-all", file: rel,
|
|
127
|
+
line: lineAt(m.index), signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
|
|
128
|
+
exported: true, lang,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
package/src/lang/registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CodeSymbol } from "../types.js";
|
|
2
|
-
import { extToLang } from "./common.js";
|
|
2
|
+
import { extToLang, extractReexports } from "./common.js";
|
|
3
3
|
import { jsTs } from "./js-ts.js";
|
|
4
4
|
import { python } from "./python.js";
|
|
5
5
|
import { go } from "./go.js";
|
|
@@ -35,14 +35,29 @@ for (const e of EXTRACTORS) for (const ext of e.exts) BY_EXT.set(ext, e);
|
|
|
35
35
|
|
|
36
36
|
// Extract declared symbols from one file. Returns [] for languages without a
|
|
37
37
|
// dedicated extractor (their content is still fully searchable via ripgrep).
|
|
38
|
+
//
|
|
39
|
+
// Also mirrors extractCode's barrel re-export/alias pass (extractReexports):
|
|
40
|
+
// a direct extractSymbols consumer (ultradoc, or any tool that doesn't go
|
|
41
|
+
// through extractCode) hits the same `export { a, b as c }` barrels a repo
|
|
42
|
+
// scan does, and should see the same alias-mirrors-b's-own-kind symbols —
|
|
43
|
+
// not the bare "reexport" kind extractReexports falls back to when there's
|
|
44
|
+
// no local declaration to mirror. extractCode already runs extractReexports
|
|
45
|
+
// itself and filters by name, so this can't double-add: whichever entry point
|
|
46
|
+
// adds "c" first wins, the other's re-run is a no-op duplicate filtered out.
|
|
38
47
|
export function extractSymbols(rel: string, ext: string, content: string): CodeSymbol[] {
|
|
39
48
|
const extractor = BY_EXT.get(ext);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
let symbols: CodeSymbol[];
|
|
50
|
+
if (!extractor) symbols = [];
|
|
51
|
+
else {
|
|
52
|
+
try {
|
|
53
|
+
symbols = extractor.extract(rel, content);
|
|
54
|
+
} catch {
|
|
55
|
+
symbols = [];
|
|
56
|
+
}
|
|
45
57
|
}
|
|
58
|
+
const known = new Set(symbols.map((s) => s.name));
|
|
59
|
+
const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
|
|
60
|
+
return reexports.length ? [...symbols, ...reexports] : symbols;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
// Human-readable language label for an extension (used for the language
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Single source of truth for the engine version the bundle reports. Kept in
|
|
2
2
|
// lockstep with package.json by the release pipeline. Do not edit by hand
|
|
3
3
|
// outside a release.
|
|
4
|
-
export const ENGINE_VERSION = "2.11.
|
|
4
|
+
export const ENGINE_VERSION = "2.11.1";
|
|
5
5
|
|
|
6
6
|
// Bumped whenever the on-disk artifact shape changes, so a consumer can reject
|
|
7
7
|
// an index written by an incompatible engine instead of misreading it. The
|
|
@@ -27,8 +27,12 @@ export const SCHEMA_VERSION = 4;
|
|
|
27
27
|
// named after the file stem, `export default Foo` marking the declaration);
|
|
28
28
|
// v7 makes an export-alias symbol (`export { b as c }`) mirror the aliased
|
|
29
29
|
// local declaration's own kind (e.g. "function") instead of the generic
|
|
30
|
-
// "reexport" when it resolves in-file.
|
|
31
|
-
|
|
30
|
+
// "reexport" when it resolves in-file. v8 fixes the C/C++ regex tier
|
|
31
|
+
// reporting a function DEFINITION as a call to itself (`void load(void) {`
|
|
32
|
+
// yielding a spurious call `load@<defline>`, found during the ultrasec
|
|
33
|
+
// consumer migration) by excluding call candidates whose name+line match one
|
|
34
|
+
// of the file's own extracted symbols.
|
|
35
|
+
export const EXTRACTOR_VERSION = 8;
|
|
32
36
|
|
|
33
37
|
// How a file is classified. `code` gets symbol/import extraction; `doc` gets
|
|
34
38
|
// link/heading extraction; the rest are catalogued but not deeply parsed.
|