@maxgfr/codeindex 2.7.0 → 2.8.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/README.md +4 -1
- package/package.json +2 -2
- package/scripts/engine.d.mts +7 -2
- package/scripts/engine.mjs +274 -14
- package/src/engine-cli.ts +21 -2
- package/src/engine.ts +2 -0
- package/src/render/scip.ts +369 -0
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,9 @@ ultraeval, reconstruct, construct, ultra11y).
|
|
|
24
24
|
`mention` edges at file and module level, plus Louvain communities, PageRank/
|
|
25
25
|
betweenness centrality, a tests→code map, and surprise-edge detection.
|
|
26
26
|
- **Render** byte-stable `graph.json` / `symbols.json` (two builds of an
|
|
27
|
-
unchanged repo are byte-identical)
|
|
27
|
+
unchanged repo are byte-identical), plus a **SCIP** code-intelligence index
|
|
28
|
+
(`index.scip`) via a hand-rolled zero-dependency protobuf encoder — validated
|
|
29
|
+
by the official `scip` CLI (`stats`/`lint`).
|
|
28
30
|
|
|
29
31
|
## Use as a library (the vendoring model)
|
|
30
32
|
|
|
@@ -68,6 +70,7 @@ brew install maxgfr/tap/codeindex # or: npm i -g @maxgfr/codeindex
|
|
|
68
70
|
|
|
69
71
|
codeindex index --repo . --out .codeindex # graph + symbols + incremental cache
|
|
70
72
|
codeindex graph --repo . > graph.json
|
|
73
|
+
codeindex scip --repo . --out index.scip # SCIP index (--out - for stdout)
|
|
71
74
|
codeindex callers --repo . # per-symbol caller index
|
|
72
75
|
codeindex grep 'pattern' --repo .
|
|
73
76
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxgfr/codeindex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
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",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@semantic-release/git": "^10.0.1",
|
|
60
60
|
"@tree-sitter-grammars/tree-sitter-lua": "0.4.1",
|
|
61
61
|
"@types/node": "^20.14.0",
|
|
62
|
-
"semantic-release": "^
|
|
62
|
+
"semantic-release": "^25.0.8",
|
|
63
63
|
"tree-sitter-bash": "0.25.1",
|
|
64
64
|
"tree-sitter-c": "^0.24.1",
|
|
65
65
|
"tree-sitter-c-sharp": "^0.23.5",
|
package/scripts/engine.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const ENGINE_VERSION = "2.
|
|
1
|
+
declare const ENGINE_VERSION = "2.8.0";
|
|
2
2
|
declare const SCHEMA_VERSION = 4;
|
|
3
3
|
declare const EXTRACTOR_VERSION = 6;
|
|
4
4
|
type FileKind = "code" | "doc" | "config" | "asset" | "other";
|
|
@@ -415,6 +415,11 @@ declare function renderSymbolsJson(index: SymbolIndex): string;
|
|
|
415
415
|
|
|
416
416
|
declare function renderGraphJson(graph: Graph): string;
|
|
417
417
|
|
|
418
|
+
interface RenderScipOptions {
|
|
419
|
+
projectRoot?: string;
|
|
420
|
+
}
|
|
421
|
+
declare function renderScip(scan: RepoScan, opts?: RenderScipOptions): Uint8Array;
|
|
422
|
+
|
|
418
423
|
interface BuildIndexOptions extends ScanOptions {
|
|
419
424
|
meta?: {
|
|
420
425
|
version?: string;
|
|
@@ -615,4 +620,4 @@ declare function rrf<T>(lists: T[][], keyOf: (item: T) => string, k?: number): M
|
|
|
615
620
|
|
|
616
621
|
declare function runCli(argv: string[]): Promise<void>;
|
|
617
622
|
|
|
618
|
-
export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawRef, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type ShResult, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, betweennessOf, buildCallerIndex, buildGraph, buildIndexArtifacts, buildModules, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, detectCommunities, detectWorkspaces, diffFiles, diffHunks, enclosingSymbol, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractMarkdown, extractSymbols, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarReady, grepRepo, have, headCommit, insertAfterSymbol, insertBeforeSymbol, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, pagerankOf, parseGitignore, parseRules, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveImport, resolveUniqueSymbol, riskHotspots, rrf, runCli, runMcpServer, scanRepo, searchIndex, sh, sha1, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, writeMemory };
|
|
623
|
+
export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawRef, type RenderScipOptions, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type ShResult, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, betweennessOf, buildCallerIndex, buildGraph, buildIndexArtifacts, buildModules, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, detectCommunities, detectWorkspaces, diffFiles, diffHunks, enclosingSymbol, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractMarkdown, extractSymbols, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarReady, grepRepo, have, headCommit, insertAfterSymbol, insertBeforeSymbol, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, pagerankOf, parseGitignore, parseRules, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveImport, resolveUniqueSymbol, riskHotspots, rrf, runCli, runMcpServer, scanRepo, searchIndex, sh, sha1, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, writeMemory };
|
package/scripts/engine.mjs
CHANGED
|
@@ -14,7 +14,7 @@ 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.
|
|
17
|
+
ENGINE_VERSION = "2.8.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
19
|
EXTRACTOR_VERSION = 6;
|
|
20
20
|
}
|
|
@@ -9551,7 +9551,7 @@ function findDeadCode(scan2) {
|
|
|
9551
9551
|
const callers = buildCallerIndex(scan2);
|
|
9552
9552
|
const refs = computeSymbolRefs(scan2);
|
|
9553
9553
|
const out2 = [];
|
|
9554
|
-
const consider = (s) => s.exported && !
|
|
9554
|
+
const consider = (s) => s.exported && !REFERENCE_KINDS6.has(s.kind) && !isTestPath(s.file) && !ENTRYPOINT_RE.test(s.file);
|
|
9555
9555
|
for (const f of scan2.files) {
|
|
9556
9556
|
for (const s of f.symbols) {
|
|
9557
9557
|
if (!consider(s)) continue;
|
|
@@ -9564,7 +9564,7 @@ function findDeadCode(scan2) {
|
|
|
9564
9564
|
}
|
|
9565
9565
|
return out2.sort((a, b) => byStr(a.tier, b.tier) || byStr(a.file, b.file) || a.line - b.line);
|
|
9566
9566
|
}
|
|
9567
|
-
var
|
|
9567
|
+
var REFERENCE_KINDS6, ENTRYPOINT_RE;
|
|
9568
9568
|
var init_deadcode = __esm({
|
|
9569
9569
|
"src/deadcode.ts"() {
|
|
9570
9570
|
"use strict";
|
|
@@ -9572,13 +9572,13 @@ var init_deadcode = __esm({
|
|
|
9572
9572
|
init_symbols_json();
|
|
9573
9573
|
init_tests_map();
|
|
9574
9574
|
init_sort();
|
|
9575
|
-
|
|
9575
|
+
REFERENCE_KINDS6 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
|
|
9576
9576
|
ENTRYPOINT_RE = /(^|\/)(index|main|cli|app|server|engine)\.[a-z]+$/;
|
|
9577
9577
|
}
|
|
9578
9578
|
});
|
|
9579
9579
|
|
|
9580
9580
|
// src/complexity.ts
|
|
9581
|
-
import { join as
|
|
9581
|
+
import { join as join10 } from "path";
|
|
9582
9582
|
function complexityOfSource(source) {
|
|
9583
9583
|
return 1 + (source.match(BRANCH_RE) ?? []).length;
|
|
9584
9584
|
}
|
|
@@ -9588,7 +9588,7 @@ function symbolComplexity(scan2, rel, top = 50) {
|
|
|
9588
9588
|
if (f.kind !== "code") continue;
|
|
9589
9589
|
if (rel && f.rel !== rel) continue;
|
|
9590
9590
|
if (!f.symbols.length) continue;
|
|
9591
|
-
const lines = readText(
|
|
9591
|
+
const lines = readText(join10(scan2.root, f.rel)).split("\n");
|
|
9592
9592
|
for (const s of f.symbols) {
|
|
9593
9593
|
if (s.kind === "reexport" || s.kind === "reexport-all") continue;
|
|
9594
9594
|
const end = s.endLine ?? s.line;
|
|
@@ -9603,7 +9603,7 @@ function symbolComplexity(scan2, rel, top = 50) {
|
|
|
9603
9603
|
}
|
|
9604
9604
|
function riskHotspots(scan2, churn, top = 20) {
|
|
9605
9605
|
const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
|
|
9606
|
-
const complexity = complexityOfSource(readText(
|
|
9606
|
+
const complexity = complexityOfSource(readText(join10(scan2.root, f.rel)));
|
|
9607
9607
|
const commits = churn.get(f.rel) ?? 0;
|
|
9608
9608
|
return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
|
|
9609
9609
|
});
|
|
@@ -10285,6 +10285,247 @@ init_tests_map();
|
|
|
10285
10285
|
init_surprise();
|
|
10286
10286
|
init_symbols_json();
|
|
10287
10287
|
init_graph_json();
|
|
10288
|
+
|
|
10289
|
+
// src/render/scip.ts
|
|
10290
|
+
init_types();
|
|
10291
|
+
init_walk();
|
|
10292
|
+
init_sort();
|
|
10293
|
+
import { join as join9 } from "path";
|
|
10294
|
+
var utf8 = new TextEncoder();
|
|
10295
|
+
function pushVarint(out2, n) {
|
|
10296
|
+
if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
|
|
10297
|
+
while (n > 127) {
|
|
10298
|
+
out2.push(n & 127 | 128);
|
|
10299
|
+
n = Math.floor(n / 128);
|
|
10300
|
+
}
|
|
10301
|
+
out2.push(n & 127);
|
|
10302
|
+
}
|
|
10303
|
+
function pushTag(out2, field, wire) {
|
|
10304
|
+
pushVarint(out2, field * 8 + wire);
|
|
10305
|
+
}
|
|
10306
|
+
function pushVarintField(out2, field, n) {
|
|
10307
|
+
pushTag(out2, field, 0);
|
|
10308
|
+
pushVarint(out2, n);
|
|
10309
|
+
}
|
|
10310
|
+
function pushLenDelim(out2, field, payload) {
|
|
10311
|
+
pushTag(out2, field, 2);
|
|
10312
|
+
pushVarint(out2, payload.length);
|
|
10313
|
+
for (let i2 = 0; i2 < payload.length; i2++) out2.push(payload[i2]);
|
|
10314
|
+
}
|
|
10315
|
+
function pushString(out2, field, s) {
|
|
10316
|
+
pushLenDelim(out2, field, utf8.encode(s));
|
|
10317
|
+
}
|
|
10318
|
+
function pushPackedInt32(out2, field, values) {
|
|
10319
|
+
const payload = [];
|
|
10320
|
+
for (const v of values) pushVarint(payload, v);
|
|
10321
|
+
pushLenDelim(out2, field, payload);
|
|
10322
|
+
}
|
|
10323
|
+
var F_INDEX_METADATA = 1;
|
|
10324
|
+
var F_INDEX_DOCUMENTS = 2;
|
|
10325
|
+
var F_META_TOOL_INFO = 2;
|
|
10326
|
+
var F_META_PROJECT_ROOT = 3;
|
|
10327
|
+
var F_META_TEXT_ENCODING = 4;
|
|
10328
|
+
var F_TOOL_NAME = 1;
|
|
10329
|
+
var F_TOOL_VERSION = 2;
|
|
10330
|
+
var F_DOC_RELPATH = 1;
|
|
10331
|
+
var F_DOC_OCCURRENCES = 2;
|
|
10332
|
+
var F_DOC_SYMBOLS = 3;
|
|
10333
|
+
var F_DOC_LANGUAGE = 4;
|
|
10334
|
+
var F_DOC_POSITION_ENCODING = 6;
|
|
10335
|
+
var F_OCC_RANGE = 1;
|
|
10336
|
+
var F_OCC_SYMBOL = 2;
|
|
10337
|
+
var F_OCC_ROLES = 3;
|
|
10338
|
+
var F_SI_SYMBOL = 1;
|
|
10339
|
+
var F_SI_KIND = 5;
|
|
10340
|
+
var F_SI_DISPLAY_NAME = 6;
|
|
10341
|
+
var F_SI_ENCLOSING = 8;
|
|
10342
|
+
var TEXT_ENCODING_UTF8 = 1;
|
|
10343
|
+
var ROLE_DEFINITION = 1;
|
|
10344
|
+
var POSITION_ENCODING_UTF16 = 2;
|
|
10345
|
+
var KIND = {
|
|
10346
|
+
function: 17,
|
|
10347
|
+
// Function
|
|
10348
|
+
method: 26,
|
|
10349
|
+
// Method
|
|
10350
|
+
class: 7,
|
|
10351
|
+
// Class
|
|
10352
|
+
interface: 21,
|
|
10353
|
+
// Interface
|
|
10354
|
+
enum: 11,
|
|
10355
|
+
// Enum
|
|
10356
|
+
struct: 49,
|
|
10357
|
+
// Struct
|
|
10358
|
+
trait: 53,
|
|
10359
|
+
// Trait
|
|
10360
|
+
type: 54,
|
|
10361
|
+
// Type
|
|
10362
|
+
const: 8,
|
|
10363
|
+
// Constant
|
|
10364
|
+
var: 61
|
|
10365
|
+
// Variable
|
|
10366
|
+
};
|
|
10367
|
+
var SYMBOL_PREFIX = "codeindex . . . ";
|
|
10368
|
+
var SIMPLE_ID = /^[A-Za-z0-9_+\-$]+$/;
|
|
10369
|
+
function escapeId(name2) {
|
|
10370
|
+
return SIMPLE_ID.test(name2) ? name2 : "`" + name2.replace(/`/g, "``") + "`";
|
|
10371
|
+
}
|
|
10372
|
+
function fileNamespace(rel) {
|
|
10373
|
+
return "`" + rel.replace(/`/g, "``") + "`/";
|
|
10374
|
+
}
|
|
10375
|
+
function parentDescriptor(parent) {
|
|
10376
|
+
return escapeId(parent) + "#";
|
|
10377
|
+
}
|
|
10378
|
+
var TYPE_KINDS = /* @__PURE__ */ new Set(["class", "interface", "enum", "struct", "trait", "type"]);
|
|
10379
|
+
var METHOD_KINDS = /* @__PURE__ */ new Set(["function", "method", "def"]);
|
|
10380
|
+
function suffixFor(kind) {
|
|
10381
|
+
if (TYPE_KINDS.has(kind)) return "#";
|
|
10382
|
+
if (METHOD_KINDS.has(kind)) return "().";
|
|
10383
|
+
return ".";
|
|
10384
|
+
}
|
|
10385
|
+
function baseSymbol(rel, sym) {
|
|
10386
|
+
let s = SYMBOL_PREFIX + fileNamespace(rel);
|
|
10387
|
+
if (sym.parent) s += parentDescriptor(sym.parent);
|
|
10388
|
+
return s + escapeId(sym.name) + suffixFor(sym.kind);
|
|
10389
|
+
}
|
|
10390
|
+
function enclosingSymbolOf(rel, parent) {
|
|
10391
|
+
return SYMBOL_PREFIX + fileNamespace(rel) + parentDescriptor(parent);
|
|
10392
|
+
}
|
|
10393
|
+
function makeUnique(base, line, used) {
|
|
10394
|
+
if (!used.has(base)) {
|
|
10395
|
+
used.add(base);
|
|
10396
|
+
return base;
|
|
10397
|
+
}
|
|
10398
|
+
for (let n = 0; ; n++) {
|
|
10399
|
+
const disambiguator = n === 0 ? String(line) : `${line}_${n}`;
|
|
10400
|
+
const cand = `${base}(${disambiguator})`;
|
|
10401
|
+
if (!used.has(cand)) {
|
|
10402
|
+
used.add(cand);
|
|
10403
|
+
return cand;
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
function familyOf2(lang) {
|
|
10408
|
+
if (lang === "typescript" || lang === "javascript") return "js";
|
|
10409
|
+
if (lang === "c" || lang === "cpp") return "c";
|
|
10410
|
+
return lang;
|
|
10411
|
+
}
|
|
10412
|
+
var REFERENCE_KINDS5 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
|
|
10413
|
+
function isIdentByte(code) {
|
|
10414
|
+
return code >= 48 && code <= 57 || // 0-9
|
|
10415
|
+
code >= 65 && code <= 90 || // A-Z
|
|
10416
|
+
code >= 97 && code <= 122 || // a-z
|
|
10417
|
+
code === 95 || // _
|
|
10418
|
+
code === 36;
|
|
10419
|
+
}
|
|
10420
|
+
function findWord(line, name2) {
|
|
10421
|
+
if (!name2) return null;
|
|
10422
|
+
const wordy = /^[A-Za-z_$][\w$]*$/.test(name2);
|
|
10423
|
+
let from = 0;
|
|
10424
|
+
for (; ; ) {
|
|
10425
|
+
const idx = line.indexOf(name2, from);
|
|
10426
|
+
if (idx < 0) return null;
|
|
10427
|
+
if (!wordy) return [idx, idx + name2.length];
|
|
10428
|
+
const before = idx > 0 ? line.charCodeAt(idx - 1) : -1;
|
|
10429
|
+
const afterIdx = idx + name2.length;
|
|
10430
|
+
const after = afterIdx < line.length ? line.charCodeAt(afterIdx) : -1;
|
|
10431
|
+
if (!isIdentByte(before) && !isIdentByte(after)) return [idx, idx + name2.length];
|
|
10432
|
+
from = idx + 1;
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
function renderScip(scan2, opts = {}) {
|
|
10436
|
+
const projectRoot = opts.projectRoot ?? "file://" + scan2.root.replace(/\\/g, "/");
|
|
10437
|
+
const docs = scan2.files.filter((f) => f.kind === "code" && f.symbols.length > 0);
|
|
10438
|
+
const docDefs = /* @__PURE__ */ new Map();
|
|
10439
|
+
const defByName = /* @__PURE__ */ new Map();
|
|
10440
|
+
for (const f of docs) {
|
|
10441
|
+
const used = /* @__PURE__ */ new Set();
|
|
10442
|
+
const entries = [];
|
|
10443
|
+
for (const sym of f.symbols) {
|
|
10444
|
+
const symbolString = makeUnique(baseSymbol(f.rel, sym), sym.line, used);
|
|
10445
|
+
entries.push({ sym, symbolString });
|
|
10446
|
+
if (sym.exported && !REFERENCE_KINDS5.has(sym.kind)) {
|
|
10447
|
+
let arr = defByName.get(sym.name);
|
|
10448
|
+
if (!arr) defByName.set(sym.name, arr = []);
|
|
10449
|
+
arr.push({ symbolString, family: familyOf2(sym.lang) });
|
|
10450
|
+
}
|
|
10451
|
+
}
|
|
10452
|
+
docDefs.set(f.rel, entries);
|
|
10453
|
+
}
|
|
10454
|
+
const resolveRef = (name2, callerFamily) => {
|
|
10455
|
+
const cands = defByName.get(name2);
|
|
10456
|
+
if (!cands || cands.length !== 1) return void 0;
|
|
10457
|
+
const only = cands[0];
|
|
10458
|
+
return only.family === callerFamily ? only.symbolString : void 0;
|
|
10459
|
+
};
|
|
10460
|
+
const documents = [];
|
|
10461
|
+
for (const f of docs) {
|
|
10462
|
+
const text = readText(join9(scan2.root, f.rel));
|
|
10463
|
+
const lines = text.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
|
|
10464
|
+
const locate = (lineNo, name2) => {
|
|
10465
|
+
const line = lines[lineNo - 1];
|
|
10466
|
+
if (line === void 0) return [lineNo - 1, 0, 0];
|
|
10467
|
+
const r = findWord(line, name2);
|
|
10468
|
+
return r ? [lineNo - 1, r[0], r[1]] : [lineNo - 1, 0, line.length];
|
|
10469
|
+
};
|
|
10470
|
+
const entries = docDefs.get(f.rel);
|
|
10471
|
+
const occs = [];
|
|
10472
|
+
for (const { sym, symbolString } of entries) {
|
|
10473
|
+
occs.push({ range: locate(sym.line, sym.name), symbol: symbolString, roles: ROLE_DEFINITION });
|
|
10474
|
+
}
|
|
10475
|
+
const callerFamily = familyOf2(f.lang);
|
|
10476
|
+
for (const c2 of f.calls ?? []) {
|
|
10477
|
+
const target = resolveRef(c2.name, callerFamily);
|
|
10478
|
+
if (!target) continue;
|
|
10479
|
+
occs.push({ range: locate(c2.line, c2.name), symbol: target, roles: 0 });
|
|
10480
|
+
}
|
|
10481
|
+
occs.sort(
|
|
10482
|
+
(a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1] || a.range[2] - b.range[2] || a.roles - b.roles || byStr(a.symbol, b.symbol)
|
|
10483
|
+
);
|
|
10484
|
+
const seenOcc = /* @__PURE__ */ new Set();
|
|
10485
|
+
const infos = entries.map(({ sym, symbolString }) => ({
|
|
10486
|
+
symbol: symbolString,
|
|
10487
|
+
displayName: sym.name,
|
|
10488
|
+
kind: KIND[sym.kind],
|
|
10489
|
+
enclosing: sym.parent ? enclosingSymbolOf(f.rel, sym.parent) : void 0
|
|
10490
|
+
})).sort((a, b) => byStr(a.symbol, b.symbol));
|
|
10491
|
+
const doc = [];
|
|
10492
|
+
pushString(doc, F_DOC_RELPATH, f.rel);
|
|
10493
|
+
for (const o of occs) {
|
|
10494
|
+
const key = `${o.range.join(",")} ${o.roles} ${o.symbol}`;
|
|
10495
|
+
if (seenOcc.has(key)) continue;
|
|
10496
|
+
seenOcc.add(key);
|
|
10497
|
+
const ob = [];
|
|
10498
|
+
pushPackedInt32(ob, F_OCC_RANGE, o.range);
|
|
10499
|
+
pushString(ob, F_OCC_SYMBOL, o.symbol);
|
|
10500
|
+
if (o.roles !== 0) pushVarintField(ob, F_OCC_ROLES, o.roles);
|
|
10501
|
+
pushLenDelim(doc, F_DOC_OCCURRENCES, ob);
|
|
10502
|
+
}
|
|
10503
|
+
for (const si of infos) {
|
|
10504
|
+
const sb = [];
|
|
10505
|
+
pushString(sb, F_SI_SYMBOL, si.symbol);
|
|
10506
|
+
if (si.kind !== void 0) pushVarintField(sb, F_SI_KIND, si.kind);
|
|
10507
|
+
pushString(sb, F_SI_DISPLAY_NAME, si.displayName);
|
|
10508
|
+
if (si.enclosing) pushString(sb, F_SI_ENCLOSING, si.enclosing);
|
|
10509
|
+
pushLenDelim(doc, F_DOC_SYMBOLS, sb);
|
|
10510
|
+
}
|
|
10511
|
+
pushString(doc, F_DOC_LANGUAGE, f.lang);
|
|
10512
|
+
pushVarintField(doc, F_DOC_POSITION_ENCODING, POSITION_ENCODING_UTF16);
|
|
10513
|
+
documents.push(doc);
|
|
10514
|
+
}
|
|
10515
|
+
const toolInfo = [];
|
|
10516
|
+
pushString(toolInfo, F_TOOL_NAME, "codeindex");
|
|
10517
|
+
pushString(toolInfo, F_TOOL_VERSION, ENGINE_VERSION);
|
|
10518
|
+
const metadata2 = [];
|
|
10519
|
+
pushLenDelim(metadata2, F_META_TOOL_INFO, toolInfo);
|
|
10520
|
+
pushString(metadata2, F_META_PROJECT_ROOT, projectRoot);
|
|
10521
|
+
pushVarintField(metadata2, F_META_TEXT_ENCODING, TEXT_ENCODING_UTF8);
|
|
10522
|
+
const index = [];
|
|
10523
|
+
pushLenDelim(index, F_INDEX_METADATA, metadata2);
|
|
10524
|
+
for (const d of documents) pushLenDelim(index, F_INDEX_DOCUMENTS, d);
|
|
10525
|
+
return Uint8Array.from(index);
|
|
10526
|
+
}
|
|
10527
|
+
|
|
10528
|
+
// src/engine.ts
|
|
10288
10529
|
init_pipeline();
|
|
10289
10530
|
init_git();
|
|
10290
10531
|
init_grep();
|
|
@@ -10307,6 +10548,8 @@ init_loader();
|
|
|
10307
10548
|
init_pipeline();
|
|
10308
10549
|
init_graph_json();
|
|
10309
10550
|
init_symbols_json();
|
|
10551
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
10552
|
+
import { join as join11, resolve } from "path";
|
|
10310
10553
|
init_scan();
|
|
10311
10554
|
init_callers();
|
|
10312
10555
|
init_workspaces();
|
|
@@ -10319,8 +10562,6 @@ init_complexity();
|
|
|
10319
10562
|
init_viz();
|
|
10320
10563
|
init_bm25();
|
|
10321
10564
|
init_rules();
|
|
10322
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
10323
|
-
import { join as join10, resolve } from "path";
|
|
10324
10565
|
var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
|
|
10325
10566
|
|
|
10326
10567
|
Usage: engine.mjs <command> [flags]
|
|
@@ -10331,6 +10572,8 @@ Commands:
|
|
|
10331
10572
|
scan Scan summary: file count, language histogram, capped flag
|
|
10332
10573
|
graph Full link-graph (graph.json bytes) to stdout or --out
|
|
10333
10574
|
symbols Symbol index (symbols.json bytes) to stdout or --out
|
|
10575
|
+
scip SCIP code-intelligence index (protobuf bytes) into --out
|
|
10576
|
+
(default index.scip; --out - writes to stdout)
|
|
10334
10577
|
callers Per-symbol caller index (JSON)
|
|
10335
10578
|
workspaces Monorepo packages + dependency graph (JSON)
|
|
10336
10579
|
churn Per-file git commit counts (JSON; --since <ref> to bound)
|
|
@@ -10349,7 +10592,10 @@ Commands:
|
|
|
10349
10592
|
|
|
10350
10593
|
Flags:
|
|
10351
10594
|
--repo <dir> Repo root (default: cwd)
|
|
10352
|
-
--out <file> Write output to a file instead of stdout
|
|
10595
|
+
--out <file> Write output to a file instead of stdout (\`scip\`: --out -
|
|
10596
|
+
writes the binary index to stdout)
|
|
10597
|
+
--project-root <uri> \`scip\`: override Metadata.project_root (default
|
|
10598
|
+
file://<repo>); pin it for a byte-reproducible index
|
|
10353
10599
|
--include <glob> Only include matching paths (repeatable)
|
|
10354
10600
|
--exclude <glob> Exclude matching paths (repeatable)
|
|
10355
10601
|
--scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
|
|
@@ -10379,7 +10625,10 @@ function parseFlags(args2) {
|
|
|
10379
10625
|
return n;
|
|
10380
10626
|
};
|
|
10381
10627
|
if (a === "--repo") flags2.repo = resolve(next());
|
|
10382
|
-
else if (a === "--out")
|
|
10628
|
+
else if (a === "--out") {
|
|
10629
|
+
const v = next();
|
|
10630
|
+
flags2.out = v === "-" ? "-" : resolve(v);
|
|
10631
|
+
} else if (a === "--project-root") flags2.projectRoot = next();
|
|
10383
10632
|
else if (a === "--include") flags2.include.push(next());
|
|
10384
10633
|
else if (a === "--exclude") flags2.exclude.push(next());
|
|
10385
10634
|
else if (a === "--scope") flags2.scope = next();
|
|
@@ -10435,7 +10684,7 @@ async function runCli(argv) {
|
|
|
10435
10684
|
if (!flags2.out) throw new Error("index needs --out <dir>");
|
|
10436
10685
|
const outDir = flags2.out;
|
|
10437
10686
|
mkdirSync2(outDir, { recursive: true });
|
|
10438
|
-
const cachePath =
|
|
10687
|
+
const cachePath = join11(outDir, "cache.json");
|
|
10439
10688
|
let cache;
|
|
10440
10689
|
try {
|
|
10441
10690
|
const parsed = JSON.parse(readFileSync5(cachePath, "utf8"));
|
|
@@ -10445,8 +10694,8 @@ async function runCli(argv) {
|
|
|
10445
10694
|
} catch {
|
|
10446
10695
|
}
|
|
10447
10696
|
const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
|
|
10448
|
-
writeFileSync3(
|
|
10449
|
-
writeFileSync3(
|
|
10697
|
+
writeFileSync3(join11(outDir, "graph.json"), renderGraphJson(graph));
|
|
10698
|
+
writeFileSync3(join11(outDir, "symbols.json"), renderSymbolsJson(symbols));
|
|
10450
10699
|
const files = {};
|
|
10451
10700
|
for (const f of scan2.files) {
|
|
10452
10701
|
const entry = { hash: f.hash, record: f, size: f.size };
|
|
@@ -10476,6 +10725,16 @@ async function runCli(argv) {
|
|
|
10476
10725
|
} else if (cmd === "symbols") {
|
|
10477
10726
|
const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
|
|
10478
10727
|
emit(renderSymbolsJson(symbols), flags2.out);
|
|
10728
|
+
} else if (cmd === "scip") {
|
|
10729
|
+
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
10730
|
+
const bytes = renderScip(scan2, { projectRoot: flags2.projectRoot });
|
|
10731
|
+
const out2 = flags2.out ?? resolve("index.scip");
|
|
10732
|
+
if (out2 === "-") process.stdout.write(Buffer.from(bytes));
|
|
10733
|
+
else {
|
|
10734
|
+
writeFileSync3(out2, bytes);
|
|
10735
|
+
process.stderr.write(`codeindex: SCIP index \u2192 ${out2} (${bytes.length} bytes)
|
|
10736
|
+
`);
|
|
10737
|
+
}
|
|
10479
10738
|
} else if (cmd === "callers") {
|
|
10480
10739
|
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
10481
10740
|
const index = buildCallerIndex(scan2, void 0, { recall: flags2.recall });
|
|
@@ -10624,6 +10883,7 @@ export {
|
|
|
10624
10883
|
renderGraphJson,
|
|
10625
10884
|
renderMermaid,
|
|
10626
10885
|
renderRepoMap,
|
|
10886
|
+
renderScip,
|
|
10627
10887
|
renderSymbolsJson,
|
|
10628
10888
|
replaceSymbolBody,
|
|
10629
10889
|
resolveBaseRef,
|
package/src/engine-cli.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { ensureGrammars, allGrammarKeys } from "./ast/loader.js";
|
|
|
6
6
|
import { buildIndexArtifacts, type BuildIndexOptions } from "./pipeline.js";
|
|
7
7
|
import { renderGraphJson } from "./render/graph-json.js";
|
|
8
8
|
import { renderSymbolsJson } from "./render/symbols-json.js";
|
|
9
|
+
import { renderScip } from "./render/scip.js";
|
|
9
10
|
import { scanRepo } from "./scan.js";
|
|
10
11
|
import { buildCallerIndex } from "./callers.js";
|
|
11
12
|
import { detectWorkspaces } from "./workspaces.js";
|
|
@@ -29,6 +30,8 @@ Commands:
|
|
|
29
30
|
scan Scan summary: file count, language histogram, capped flag
|
|
30
31
|
graph Full link-graph (graph.json bytes) to stdout or --out
|
|
31
32
|
symbols Symbol index (symbols.json bytes) to stdout or --out
|
|
33
|
+
scip SCIP code-intelligence index (protobuf bytes) into --out
|
|
34
|
+
(default index.scip; --out - writes to stdout)
|
|
32
35
|
callers Per-symbol caller index (JSON)
|
|
33
36
|
workspaces Monorepo packages + dependency graph (JSON)
|
|
34
37
|
churn Per-file git commit counts (JSON; --since <ref> to bound)
|
|
@@ -47,7 +50,10 @@ Commands:
|
|
|
47
50
|
|
|
48
51
|
Flags:
|
|
49
52
|
--repo <dir> Repo root (default: cwd)
|
|
50
|
-
--out <file> Write output to a file instead of stdout
|
|
53
|
+
--out <file> Write output to a file instead of stdout (\`scip\`: --out -
|
|
54
|
+
writes the binary index to stdout)
|
|
55
|
+
--project-root <uri> \`scip\`: override Metadata.project_root (default
|
|
56
|
+
file://<repo>); pin it for a byte-reproducible index
|
|
51
57
|
--include <glob> Only include matching paths (repeatable)
|
|
52
58
|
--exclude <glob> Exclude matching paths (repeatable)
|
|
53
59
|
--scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
|
|
@@ -79,6 +85,7 @@ interface CliFlags {
|
|
|
79
85
|
config?: string; // rules config path
|
|
80
86
|
limit?: number; // search result cap
|
|
81
87
|
recall?: boolean; // callers: recall-oriented binding
|
|
88
|
+
projectRoot?: string; // scip: override Metadata.project_root
|
|
82
89
|
positional?: string; // e.g. the grep pattern or search query
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -98,7 +105,10 @@ function parseFlags(args: string[]): CliFlags {
|
|
|
98
105
|
return n;
|
|
99
106
|
};
|
|
100
107
|
if (a === "--repo") flags.repo = resolve(next());
|
|
101
|
-
else if (a === "--out")
|
|
108
|
+
else if (a === "--out") {
|
|
109
|
+
const v = next();
|
|
110
|
+
flags.out = v === "-" ? "-" : resolve(v); // "-" = stdout (scip binary)
|
|
111
|
+
} else if (a === "--project-root") flags.projectRoot = next();
|
|
102
112
|
else if (a === "--include") flags.include.push(next());
|
|
103
113
|
else if (a === "--exclude") flags.exclude.push(next());
|
|
104
114
|
else if (a === "--scope") flags.scope = next();
|
|
@@ -206,6 +216,15 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
206
216
|
} else if (cmd === "symbols") {
|
|
207
217
|
const { symbols } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
208
218
|
emit(renderSymbolsJson(symbols), flags.out);
|
|
219
|
+
} else if (cmd === "scip") {
|
|
220
|
+
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
221
|
+
const bytes = renderScip(scan, { projectRoot: flags.projectRoot });
|
|
222
|
+
const out = flags.out ?? resolve("index.scip");
|
|
223
|
+
if (out === "-") process.stdout.write(Buffer.from(bytes));
|
|
224
|
+
else {
|
|
225
|
+
writeFileSync(out, bytes);
|
|
226
|
+
process.stderr.write(`codeindex: SCIP index → ${out} (${bytes.length} bytes)\n`);
|
|
227
|
+
}
|
|
209
228
|
} else if (cmd === "callers") {
|
|
210
229
|
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
211
230
|
const index = buildCallerIndex(scan, undefined, { recall: flags.recall });
|
package/src/engine.ts
CHANGED
|
@@ -75,6 +75,8 @@ export { computeSurprises, isSurprising } from "./surprise.js";
|
|
|
75
75
|
// Symbol index + machine renderers (render-to-string; consumers own persistence).
|
|
76
76
|
export { buildSymbolIndex, computeSymbolRefs, renderSymbolsJson } from "./render/symbols-json.js";
|
|
77
77
|
export { renderGraphJson } from "./render/graph-json.js";
|
|
78
|
+
export { renderScip } from "./render/scip.js";
|
|
79
|
+
export type { RenderScipOptions } from "./render/scip.js";
|
|
78
80
|
|
|
79
81
|
// One-call pipeline.
|
|
80
82
|
export { buildIndexArtifacts } from "./pipeline.js";
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// SCIP index export — a hand-rolled, zero-dependency protobuf wire-format
|
|
2
|
+
// encoder plus the codeindex→SCIP mapping and `renderScip`.
|
|
3
|
+
//
|
|
4
|
+
// Every field number and enum value below is copied VERBATIM from the pinned
|
|
5
|
+
// scip.proto (never guessed):
|
|
6
|
+
// https://raw.githubusercontent.com/sourcegraph/scip/44d39fcfc95486d066a796e2cec8c7ec5d429aae/scip.proto
|
|
7
|
+
// sourcegraph/scip @ 44d39fcfc95486d066a796e2cec8c7ec5d429aae
|
|
8
|
+
//
|
|
9
|
+
// Scope: we emit the classic packed `Occurrence.range` (field 1) that
|
|
10
|
+
// scip-typescript emits and that `scip stats`/`scip lint` read; the typed
|
|
11
|
+
// single/multi-line ranges (fields 8-11) are deliberately out of scope. The
|
|
12
|
+
// symbol grammar (scheme/package/descriptors and the suffixes `/` namespace,
|
|
13
|
+
// `#` type, `.` term, `().` method) is taken from the `Symbol` message comment.
|
|
14
|
+
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { ENGINE_VERSION } from "../types.js";
|
|
17
|
+
import type { CodeSymbol } from "../types.js";
|
|
18
|
+
import type { RepoScan } from "../scan.js";
|
|
19
|
+
import { readText } from "../walk.js";
|
|
20
|
+
import { byStr } from "../sort.js";
|
|
21
|
+
|
|
22
|
+
export interface RenderScipOptions {
|
|
23
|
+
// URI-encoded absolute path to the index root (SCIP `Metadata.project_root`).
|
|
24
|
+
// Overridable so a build is byte-reproducible regardless of the machine's
|
|
25
|
+
// checkout path; defaults to `file://` + the posix repo root.
|
|
26
|
+
projectRoot?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Protobuf wire-format primitives (proto3). Messages are assembled as plain
|
|
31
|
+
// number[] byte buffers and embedded into their parent as length-delimited
|
|
32
|
+
// (wire type 2). Only the two wire types we use are implemented: varint (0)
|
|
33
|
+
// and length-delimited (2).
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
type Bytes = number[];
|
|
36
|
+
const utf8 = new TextEncoder();
|
|
37
|
+
|
|
38
|
+
// Unsigned LEB128. Every value we encode (field tags, enum values, ranges,
|
|
39
|
+
// sub-message byte lengths) is a small non-negative integer well under 2^31.
|
|
40
|
+
function pushVarint(out: Bytes, n: number): void {
|
|
41
|
+
if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
|
|
42
|
+
while (n > 0x7f) {
|
|
43
|
+
out.push((n & 0x7f) | 0x80);
|
|
44
|
+
n = Math.floor(n / 128);
|
|
45
|
+
}
|
|
46
|
+
out.push(n & 0x7f);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pushTag(out: Bytes, field: number, wire: number): void {
|
|
50
|
+
pushVarint(out, field * 8 + wire);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pushVarintField(out: Bytes, field: number, n: number): void {
|
|
54
|
+
pushTag(out, field, 0);
|
|
55
|
+
pushVarint(out, n);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pushLenDelim(out: Bytes, field: number, payload: ArrayLike<number>): void {
|
|
59
|
+
pushTag(out, field, 2);
|
|
60
|
+
pushVarint(out, payload.length);
|
|
61
|
+
for (let i = 0; i < payload.length; i++) out.push(payload[i]!);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pushString(out: Bytes, field: number, s: string): void {
|
|
65
|
+
pushLenDelim(out, field, utf8.encode(s));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A `repeated int32` in packed encoding: a single length-delimited blob holding
|
|
69
|
+
// the concatenated varints of every element (this is the piece the plain
|
|
70
|
+
// per-element encoding would get wrong).
|
|
71
|
+
function pushPackedInt32(out: Bytes, field: number, values: number[]): void {
|
|
72
|
+
const payload: Bytes = [];
|
|
73
|
+
for (const v of values) pushVarint(payload, v);
|
|
74
|
+
pushLenDelim(out, field, payload);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Pinned scip.proto field numbers & enum values (SHA 44d39fc…).
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
const F_INDEX_METADATA = 1;
|
|
81
|
+
const F_INDEX_DOCUMENTS = 2; // Index.external_symbols = 3 (unused — always empty)
|
|
82
|
+
const F_META_TOOL_INFO = 2; // Metadata.version = 1 is UnspecifiedProtocolVersion(0), omitted
|
|
83
|
+
const F_META_PROJECT_ROOT = 3;
|
|
84
|
+
const F_META_TEXT_ENCODING = 4;
|
|
85
|
+
const F_TOOL_NAME = 1;
|
|
86
|
+
const F_TOOL_VERSION = 2;
|
|
87
|
+
const F_DOC_RELPATH = 1;
|
|
88
|
+
const F_DOC_OCCURRENCES = 2;
|
|
89
|
+
const F_DOC_SYMBOLS = 3;
|
|
90
|
+
const F_DOC_LANGUAGE = 4;
|
|
91
|
+
const F_DOC_POSITION_ENCODING = 6; // Document.position_encoding = 6 (Document.text = 5 is unused — we never embed source text)
|
|
92
|
+
const F_OCC_RANGE = 1;
|
|
93
|
+
const F_OCC_SYMBOL = 2;
|
|
94
|
+
const F_OCC_ROLES = 3;
|
|
95
|
+
const F_SI_SYMBOL = 1;
|
|
96
|
+
const F_SI_KIND = 5;
|
|
97
|
+
const F_SI_DISPLAY_NAME = 6;
|
|
98
|
+
const F_SI_ENCLOSING = 8;
|
|
99
|
+
|
|
100
|
+
const TEXT_ENCODING_UTF8 = 1; // TextEncoding.UTF8
|
|
101
|
+
const ROLE_DEFINITION = 0x1; // SymbolRole.Definition (a reference omits the field → 0)
|
|
102
|
+
|
|
103
|
+
// PositionEncoding.UTF16CodeUnitOffsetFromLineStart = 2. `locate`/`findWord`
|
|
104
|
+
// below compute range offsets with JS string `indexOf`/`.length`, i.e. UTF-16
|
|
105
|
+
// code units — this is the encoding that matches that computation (NOT
|
|
106
|
+
// Metadata.text_document_encoding, which is the on-disk file encoding and is
|
|
107
|
+
// unrelated to how `character` offsets in ranges are interpreted).
|
|
108
|
+
const POSITION_ENCODING_UTF16 = 2; // PositionEncoding.UTF16CodeUnitOffsetFromLineStart
|
|
109
|
+
|
|
110
|
+
// SymbolInformation.Kind — only the codeindex kinds we can map; anything else
|
|
111
|
+
// leaves the field unset (UnspecifiedKind = 0).
|
|
112
|
+
const KIND: Record<string, number> = {
|
|
113
|
+
function: 17, // Function
|
|
114
|
+
method: 26, // Method
|
|
115
|
+
class: 7, // Class
|
|
116
|
+
interface: 21, // Interface
|
|
117
|
+
enum: 11, // Enum
|
|
118
|
+
struct: 49, // Struct
|
|
119
|
+
trait: 53, // Trait
|
|
120
|
+
type: 54, // Type
|
|
121
|
+
const: 8, // Constant
|
|
122
|
+
var: 61, // Variable
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// SCIP symbol strings (the `Symbol` grammar). Local, minimal scheme:
|
|
127
|
+
// <scheme:codeindex> ' ' <manager:.> ' ' <package-name:.> ' ' <version:.> ' ' <descriptors>
|
|
128
|
+
// Descriptors: the file path as a backtick-escaped namespace, an optional parent
|
|
129
|
+
// type descriptor, then the symbol with a kind-dependent suffix.
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
const SYMBOL_PREFIX = "codeindex . . . ";
|
|
132
|
+
const SIMPLE_ID = /^[A-Za-z0-9_+\-$]+$/; // <identifier-character> = _ + - $ letters digits
|
|
133
|
+
|
|
134
|
+
function escapeId(name: string): string {
|
|
135
|
+
// A simple-identifier is used as-is; anything else is a backtick-escaped
|
|
136
|
+
// identifier (backticks doubled).
|
|
137
|
+
return SIMPLE_ID.test(name) ? name : "`" + name.replace(/`/g, "``") + "`";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function fileNamespace(rel: string): string {
|
|
141
|
+
// A repo path always contains '/' (and usually '.') so it is never a
|
|
142
|
+
// simple-identifier — always backtick-escape, then the namespace suffix '/'.
|
|
143
|
+
return "`" + rel.replace(/`/g, "``") + "`/";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Enclosing members hang off their parent, which we model as a type descriptor.
|
|
147
|
+
function parentDescriptor(parent: string): string {
|
|
148
|
+
return escapeId(parent) + "#";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const TYPE_KINDS = new Set(["class", "interface", "enum", "struct", "trait", "type"]);
|
|
152
|
+
const METHOD_KINDS = new Set(["function", "method", "def"]);
|
|
153
|
+
|
|
154
|
+
function suffixFor(kind: string): string {
|
|
155
|
+
if (TYPE_KINDS.has(kind)) return "#"; // <type>
|
|
156
|
+
if (METHOD_KINDS.has(kind)) return "()."; // <method>
|
|
157
|
+
return "."; // <term> — const/var/reexport/… default
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function baseSymbol(rel: string, sym: CodeSymbol): string {
|
|
161
|
+
let s = SYMBOL_PREFIX + fileNamespace(rel);
|
|
162
|
+
if (sym.parent) s += parentDescriptor(sym.parent);
|
|
163
|
+
return s + escapeId(sym.name) + suffixFor(sym.kind);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function enclosingSymbolOf(rel: string, parent: string): string {
|
|
167
|
+
return SYMBOL_PREFIX + fileNamespace(rel) + parentDescriptor(parent);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Guarantee a unique symbol string per document. On a same name+kind collision
|
|
171
|
+
// we append a `(<disambiguator>)` parameter descriptor — grammar-valid after any
|
|
172
|
+
// suffix (type/term/method) and deterministic (seeded by the declaration line).
|
|
173
|
+
function makeUnique(base: string, line: number, used: Set<string>): string {
|
|
174
|
+
if (!used.has(base)) {
|
|
175
|
+
used.add(base);
|
|
176
|
+
return base;
|
|
177
|
+
}
|
|
178
|
+
for (let n = 0; ; n++) {
|
|
179
|
+
const disambiguator = n === 0 ? String(line) : `${line}_${n}`;
|
|
180
|
+
const cand = `${base}(${disambiguator})`;
|
|
181
|
+
if (!used.has(cand)) {
|
|
182
|
+
used.add(cand);
|
|
183
|
+
return cand;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Collapse TS/JS and C/C++ into one family so a reference never binds across
|
|
189
|
+
// unrelated languages (mirrors calls.ts `familyOf`).
|
|
190
|
+
function familyOf(lang: string): string {
|
|
191
|
+
if (lang === "typescript" || lang === "javascript") return "js";
|
|
192
|
+
if (lang === "c" || lang === "cpp") return "c";
|
|
193
|
+
return lang;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// A barrel re-export / default alias is a reference to a symbol declared
|
|
197
|
+
// elsewhere — never a call target (mirrors calls.ts REFERENCE_KINDS).
|
|
198
|
+
const REFERENCE_KINDS = new Set(["reexport", "reexport-all", "default"]);
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Range location. CodeSymbol has no columns, so we re-read each file once and
|
|
202
|
+
// locate the name on its declaration line. Deterministic: same content → same
|
|
203
|
+
// bytes.
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
function isIdentByte(code: number): boolean {
|
|
206
|
+
return (
|
|
207
|
+
(code >= 48 && code <= 57) || // 0-9
|
|
208
|
+
(code >= 65 && code <= 90) || // A-Z
|
|
209
|
+
(code >= 97 && code <= 122) || // a-z
|
|
210
|
+
code === 95 || // _
|
|
211
|
+
code === 36 // $
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// [startChar, endChar) of `name` on `line`, preferring a whole-word match for a
|
|
216
|
+
// trivial identifier; null when not found.
|
|
217
|
+
function findWord(line: string, name: string): [number, number] | null {
|
|
218
|
+
if (!name) return null;
|
|
219
|
+
const wordy = /^[A-Za-z_$][\w$]*$/.test(name);
|
|
220
|
+
let from = 0;
|
|
221
|
+
for (;;) {
|
|
222
|
+
const idx = line.indexOf(name, from);
|
|
223
|
+
if (idx < 0) return null;
|
|
224
|
+
if (!wordy) return [idx, idx + name.length];
|
|
225
|
+
const before = idx > 0 ? line.charCodeAt(idx - 1) : -1;
|
|
226
|
+
const afterIdx = idx + name.length;
|
|
227
|
+
const after = afterIdx < line.length ? line.charCodeAt(afterIdx) : -1;
|
|
228
|
+
if (!isIdentByte(before) && !isIdentByte(after)) return [idx, idx + name.length];
|
|
229
|
+
from = idx + 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface Occ {
|
|
234
|
+
range: number[]; // [line0, startChar, endChar]
|
|
235
|
+
symbol: string;
|
|
236
|
+
roles: number; // 0 = reference, ROLE_DEFINITION = definition
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface DefEntry {
|
|
240
|
+
sym: CodeSymbol;
|
|
241
|
+
symbolString: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// renderScip — map a RepoScan to a SCIP Index and encode it to bytes.
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
export function renderScip(scan: RepoScan, opts: RenderScipOptions = {}): Uint8Array {
|
|
248
|
+
const projectRoot = opts.projectRoot ?? "file://" + scan.root.replace(/\\/g, "/");
|
|
249
|
+
|
|
250
|
+
// One Document per `code` file that declares ≥1 symbol, in scan order (already
|
|
251
|
+
// sorted by rel).
|
|
252
|
+
const docs = scan.files.filter((f) => f.kind === "code" && f.symbols.length > 0);
|
|
253
|
+
|
|
254
|
+
// Pass 1 — assign every symbol its final (per-document-unique) symbol string,
|
|
255
|
+
// and index the exported, non-reference-kind defs by name so references can be
|
|
256
|
+
// resolved against globally-unique names.
|
|
257
|
+
const docDefs = new Map<string, DefEntry[]>();
|
|
258
|
+
const defByName = new Map<string, { symbolString: string; family: string }[]>();
|
|
259
|
+
for (const f of docs) {
|
|
260
|
+
const used = new Set<string>();
|
|
261
|
+
const entries: DefEntry[] = [];
|
|
262
|
+
for (const sym of f.symbols) {
|
|
263
|
+
const symbolString = makeUnique(baseSymbol(f.rel, sym), sym.line, used);
|
|
264
|
+
entries.push({ sym, symbolString });
|
|
265
|
+
if (sym.exported && !REFERENCE_KINDS.has(sym.kind)) {
|
|
266
|
+
let arr = defByName.get(sym.name);
|
|
267
|
+
if (!arr) defByName.set(sym.name, (arr = []));
|
|
268
|
+
arr.push({ symbolString, family: familyOf(sym.lang) });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
docDefs.set(f.rel, entries);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// A call resolves to a reference only when the name is defined exactly once in
|
|
275
|
+
// the whole index and in the caller's language family (conservative, like
|
|
276
|
+
// resolveCallEdges — ambiguous names are skipped).
|
|
277
|
+
const resolveRef = (name: string, callerFamily: string): string | undefined => {
|
|
278
|
+
const cands = defByName.get(name);
|
|
279
|
+
if (!cands || cands.length !== 1) return undefined;
|
|
280
|
+
const only = cands[0]!;
|
|
281
|
+
return only.family === callerFamily ? only.symbolString : undefined;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Pass 2 — encode each Document.
|
|
285
|
+
const documents: Bytes[] = [];
|
|
286
|
+
for (const f of docs) {
|
|
287
|
+
const text = readText(join(scan.root, f.rel));
|
|
288
|
+
const lines = text.split("\n").map((l) => (l.endsWith("\r") ? l.slice(0, -1) : l));
|
|
289
|
+
const locate = (lineNo: number, name: string): number[] => {
|
|
290
|
+
const line = lines[lineNo - 1];
|
|
291
|
+
if (line === undefined) return [lineNo - 1, 0, 0];
|
|
292
|
+
const r = findWord(line, name);
|
|
293
|
+
return r ? [lineNo - 1, r[0], r[1]] : [lineNo - 1, 0, line.length];
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const entries = docDefs.get(f.rel)!;
|
|
297
|
+
const occs: Occ[] = [];
|
|
298
|
+
for (const { sym, symbolString } of entries) {
|
|
299
|
+
occs.push({ range: locate(sym.line, sym.name), symbol: symbolString, roles: ROLE_DEFINITION });
|
|
300
|
+
}
|
|
301
|
+
const callerFamily = familyOf(f.lang);
|
|
302
|
+
for (const c of f.calls ?? []) {
|
|
303
|
+
const target = resolveRef(c.name, callerFamily);
|
|
304
|
+
if (!target) continue;
|
|
305
|
+
occs.push({ range: locate(c.line, c.name), symbol: target, roles: 0 });
|
|
306
|
+
}
|
|
307
|
+
// Deterministic occurrence order + dedupe of exact duplicates.
|
|
308
|
+
occs.sort(
|
|
309
|
+
(a, b) =>
|
|
310
|
+
a.range[0]! - b.range[0]! ||
|
|
311
|
+
a.range[1]! - b.range[1]! ||
|
|
312
|
+
a.range[2]! - b.range[2]! ||
|
|
313
|
+
a.roles - b.roles ||
|
|
314
|
+
byStr(a.symbol, b.symbol),
|
|
315
|
+
);
|
|
316
|
+
const seenOcc = new Set<string>();
|
|
317
|
+
|
|
318
|
+
// One SymbolInformation per definition, sorted by symbol string.
|
|
319
|
+
const infos = entries
|
|
320
|
+
.map(({ sym, symbolString }) => ({
|
|
321
|
+
symbol: symbolString,
|
|
322
|
+
displayName: sym.name,
|
|
323
|
+
kind: KIND[sym.kind],
|
|
324
|
+
enclosing: sym.parent ? enclosingSymbolOf(f.rel, sym.parent) : undefined,
|
|
325
|
+
}))
|
|
326
|
+
.sort((a, b) => byStr(a.symbol, b.symbol));
|
|
327
|
+
|
|
328
|
+
const doc: Bytes = [];
|
|
329
|
+
pushString(doc, F_DOC_RELPATH, f.rel);
|
|
330
|
+
for (const o of occs) {
|
|
331
|
+
const key = `${o.range.join(",")} ${o.roles} ${o.symbol}`;
|
|
332
|
+
if (seenOcc.has(key)) continue;
|
|
333
|
+
seenOcc.add(key);
|
|
334
|
+
const ob: Bytes = [];
|
|
335
|
+
pushPackedInt32(ob, F_OCC_RANGE, o.range);
|
|
336
|
+
pushString(ob, F_OCC_SYMBOL, o.symbol);
|
|
337
|
+
if (o.roles !== 0) pushVarintField(ob, F_OCC_ROLES, o.roles);
|
|
338
|
+
pushLenDelim(doc, F_DOC_OCCURRENCES, ob);
|
|
339
|
+
}
|
|
340
|
+
for (const si of infos) {
|
|
341
|
+
const sb: Bytes = [];
|
|
342
|
+
pushString(sb, F_SI_SYMBOL, si.symbol);
|
|
343
|
+
if (si.kind !== undefined) pushVarintField(sb, F_SI_KIND, si.kind);
|
|
344
|
+
pushString(sb, F_SI_DISPLAY_NAME, si.displayName);
|
|
345
|
+
if (si.enclosing) pushString(sb, F_SI_ENCLOSING, si.enclosing);
|
|
346
|
+
pushLenDelim(doc, F_DOC_SYMBOLS, sb);
|
|
347
|
+
}
|
|
348
|
+
pushString(doc, F_DOC_LANGUAGE, f.lang);
|
|
349
|
+
pushVarintField(doc, F_DOC_POSITION_ENCODING, POSITION_ENCODING_UTF16);
|
|
350
|
+
documents.push(doc);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Metadata { tool_info, project_root, text_document_encoding }.
|
|
354
|
+
const toolInfo: Bytes = [];
|
|
355
|
+
pushString(toolInfo, F_TOOL_NAME, "codeindex");
|
|
356
|
+
pushString(toolInfo, F_TOOL_VERSION, ENGINE_VERSION);
|
|
357
|
+
|
|
358
|
+
const metadata: Bytes = [];
|
|
359
|
+
pushLenDelim(metadata, F_META_TOOL_INFO, toolInfo);
|
|
360
|
+
pushString(metadata, F_META_PROJECT_ROOT, projectRoot);
|
|
361
|
+
pushVarintField(metadata, F_META_TEXT_ENCODING, TEXT_ENCODING_UTF8);
|
|
362
|
+
|
|
363
|
+
// Index { metadata, documents }.
|
|
364
|
+
const index: Bytes = [];
|
|
365
|
+
pushLenDelim(index, F_INDEX_METADATA, metadata);
|
|
366
|
+
for (const d of documents) pushLenDelim(index, F_INDEX_DOCUMENTS, d);
|
|
367
|
+
|
|
368
|
+
return Uint8Array.from(index);
|
|
369
|
+
}
|
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.
|
|
4
|
+
export const ENGINE_VERSION = "2.8.0";
|
|
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
|