graph-indexer 2.1.0 → 3.0.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/CHANGELOG.md +260 -1
- package/README.md +232 -312
- package/SECURITY.md +37 -59
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +124 -0
- package/package.json +31 -105
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +356 -0
- package/src/cli/init.mjs +255 -0
- package/src/cli/resident.mjs +175 -0
- package/src/index/discover.mjs +130 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +331 -0
- package/src/index/resolver.mjs +954 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +973 -0
- package/src/parse/arity.mjs +152 -0
- package/src/parse/extract.mjs +1031 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +343 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +680 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +119 -0
- package/src/query/textsearch.mjs +160 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/paths.mjs +24 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- package/watch-daemon.mjs +0 -289
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module resolution: turns an import's source string into repository files (or a package
|
|
3
|
+
* directory). Everything is computed from the file list plus a few manifest files
|
|
4
|
+
* (package.json workspaces, tsconfig paths, go.mod) — no build, no network.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const ECMA_EXTS = ['.ts', '.tsx', '.d.ts', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
|
|
10
|
+
|
|
11
|
+
function readJson(p) {
|
|
12
|
+
try {
|
|
13
|
+
const text = fs.readFileSync(p, 'utf8').replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
|
|
14
|
+
return JSON.parse(text);
|
|
15
|
+
} catch { return null; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ModuleResolver {
|
|
19
|
+
constructor(root, filePaths) {
|
|
20
|
+
this.root = root;
|
|
21
|
+
this.setFiles(filePaths);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setFiles(filePaths) {
|
|
25
|
+
this.files = new Set(filePaths);
|
|
26
|
+
this.bySuffix = null; // lazy
|
|
27
|
+
this.dirs = new Map(); // dir -> [paths]
|
|
28
|
+
for (const f of filePaths) {
|
|
29
|
+
const d = path.posix.dirname(f);
|
|
30
|
+
(this.dirs.get(d) ?? this.dirs.set(d, []).get(d)).push(f);
|
|
31
|
+
}
|
|
32
|
+
this.#loadManifests();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
addFile(f) {
|
|
36
|
+
if (this.files.has(f)) return;
|
|
37
|
+
this.files.add(f);
|
|
38
|
+
const d = path.posix.dirname(f);
|
|
39
|
+
(this.dirs.get(d) ?? this.dirs.set(d, []).get(d)).push(f);
|
|
40
|
+
this.bySuffix = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
removeFile(f) {
|
|
44
|
+
if (!this.files.delete(f)) return;
|
|
45
|
+
const d = path.posix.dirname(f);
|
|
46
|
+
const list = this.dirs.get(d);
|
|
47
|
+
if (list) { const i = list.indexOf(f); if (i >= 0) list.splice(i, 1); }
|
|
48
|
+
this.bySuffix = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#loadManifests() {
|
|
52
|
+
// JS/TS workspace packages: package name -> directory
|
|
53
|
+
this.packages = new Map();
|
|
54
|
+
const pkgJsons = [...this.files].length ? this.#findManifests('package.json') : [];
|
|
55
|
+
for (const rel of pkgJsons) {
|
|
56
|
+
const j = readJson(path.join(this.root, rel));
|
|
57
|
+
if (j?.name) this.packages.set(j.name, { dir: path.posix.dirname(rel), json: j });
|
|
58
|
+
}
|
|
59
|
+
// tsconfig paths and baseUrl, per project: a file resolves through the nearest tsconfig.json
|
|
60
|
+
// above it (in a monorepo each package maps its own aliases, often the same `@/*` to a
|
|
61
|
+
// different directory), then through the root's
|
|
62
|
+
this.tsScopes = new Map();
|
|
63
|
+
const rootCfg = fs.existsSync(path.join(this.root, 'tsconfig.json')) ? 'tsconfig.json' : 'tsconfig.base.json';
|
|
64
|
+
const rootScope = this.#tsScope(rootCfg);
|
|
65
|
+
if (rootScope) this.tsScopes.set('.', rootScope);
|
|
66
|
+
for (const rel of this.#findManifests('tsconfig.json')) {
|
|
67
|
+
const dir = path.posix.dirname(rel);
|
|
68
|
+
if (dir === '.') continue;
|
|
69
|
+
const scope = this.#tsScope(rel);
|
|
70
|
+
if (scope) this.tsScopes.set(dir, scope);
|
|
71
|
+
}
|
|
72
|
+
// Go module path
|
|
73
|
+
this.goModules = [];
|
|
74
|
+
for (const rel of this.#findManifests('go.mod')) {
|
|
75
|
+
try {
|
|
76
|
+
const m = /^module\s+(\S+)/m.exec(fs.readFileSync(path.join(this.root, rel), 'utf8'));
|
|
77
|
+
if (m) this.goModules.push({ module: m[1], dir: path.posix.dirname(rel) });
|
|
78
|
+
} catch { /* unreadable */ }
|
|
79
|
+
}
|
|
80
|
+
this.goModules.sort((a, b) => b.module.length - a.module.length);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** compilerOptions of a tsconfig file, following relative `extends` (a child's paths replace its parent's). */
|
|
84
|
+
#tsOptions(rel, depth = 0) {
|
|
85
|
+
const j = readJson(path.join(this.root, rel));
|
|
86
|
+
if (!j || depth > 5) return null;
|
|
87
|
+
const dir = path.posix.dirname(rel);
|
|
88
|
+
let out = { paths: null, pathsDir: null, baseUrl: null };
|
|
89
|
+
for (const ext of [j.extends ?? []].flat()) {
|
|
90
|
+
if (typeof ext !== 'string' || !ext.startsWith('.')) continue;
|
|
91
|
+
const parent = this.#tsOptions(path.posix.normalize(path.posix.join(dir, ext.endsWith('.json') ? ext : ext + '.json')), depth + 1);
|
|
92
|
+
if (parent) out = { paths: parent.paths ?? out.paths, pathsDir: parent.paths ? parent.pathsDir : out.pathsDir, baseUrl: parent.baseUrl ?? out.baseUrl };
|
|
93
|
+
}
|
|
94
|
+
const co = j.compilerOptions ?? {};
|
|
95
|
+
if (co.baseUrl != null) out.baseUrl = path.posix.normalize(path.posix.join(dir, co.baseUrl));
|
|
96
|
+
if (co.paths) { out.paths = co.paths; out.pathsDir = dir; }
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The aliases a tsconfig (or, when it has none, the app/lib configs next to it) declares. */
|
|
101
|
+
#tsScope(rel) {
|
|
102
|
+
let o = this.#tsOptions(rel);
|
|
103
|
+
const dir = path.posix.dirname(rel);
|
|
104
|
+
if (o && !o.paths && !o.baseUrl) {
|
|
105
|
+
for (const alt of ['tsconfig.app.json', 'tsconfig.lib.json', 'tsconfig.build.json']) {
|
|
106
|
+
const a = this.#tsOptions(dir === '.' ? alt : `${dir}/${alt}`);
|
|
107
|
+
if (a?.paths || a?.baseUrl) { o = a; break; }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!o || (!o.paths && !o.baseUrl)) return null;
|
|
111
|
+
// targets are relative to baseUrl, or to the config that declares them when there is none
|
|
112
|
+
const base = o.baseUrl ?? o.pathsDir;
|
|
113
|
+
const paths = Object.entries(o.paths ?? {}).map(([pat, targets]) => ({ pat, targets: [targets].flat().map(t => path.posix.normalize(path.posix.join(base, t))) }));
|
|
114
|
+
return { paths, baseUrl: o.baseUrl };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#tsScopesFor(fromDir) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (let d = fromDir; ; d = path.posix.dirname(d)) {
|
|
120
|
+
const s = this.tsScopes.get(d);
|
|
121
|
+
if (s && d !== '.') { out.push(s); break; }
|
|
122
|
+
if (d === '.' || d === '/' || !d) break;
|
|
123
|
+
}
|
|
124
|
+
const root = this.tsScopes.get('.');
|
|
125
|
+
if (root) out.push(root);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#findManifests(name) {
|
|
130
|
+
// manifests are not source files, so look for them next to source directories (bounded)
|
|
131
|
+
const out = [];
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const consider = (dir) => {
|
|
134
|
+
if (seen.has(dir)) return; seen.add(dir);
|
|
135
|
+
const rel = dir === '.' ? name : `${dir}/${name}`;
|
|
136
|
+
if (fs.existsSync(path.join(this.root, rel))) out.push(rel);
|
|
137
|
+
};
|
|
138
|
+
consider('.');
|
|
139
|
+
for (const d of this.dirs.keys()) {
|
|
140
|
+
let cur = d;
|
|
141
|
+
for (let i = 0; i < 6 && cur && cur !== '.'; i++) {
|
|
142
|
+
if (/(^|\/)node_modules(\/|$)/.test(cur)) break;
|
|
143
|
+
consider(cur);
|
|
144
|
+
cur = path.posix.dirname(cur);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#suffixIndex() {
|
|
151
|
+
if (!this.bySuffix) {
|
|
152
|
+
this.bySuffix = new Map();
|
|
153
|
+
for (const f of this.files) {
|
|
154
|
+
const parts = f.split('/');
|
|
155
|
+
for (let i = Math.max(0, parts.length - 6); i < parts.length; i++) {
|
|
156
|
+
const suf = parts.slice(i).join('/');
|
|
157
|
+
(this.bySuffix.get(suf) ?? this.bySuffix.set(suf, []).get(suf)).push(f);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return this.bySuffix;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#ecmaFile(base) {
|
|
165
|
+
if (this.files.has(base)) return base;
|
|
166
|
+
const noJs = base.replace(/\.(m|c)?js$/, '');
|
|
167
|
+
for (const ext of ECMA_EXTS) if (this.files.has(noJs + ext)) return noJs + ext;
|
|
168
|
+
for (const ext of ECMA_EXTS) if (this.files.has(`${noJs}/index${ext}`)) return `${noJs}/index${ext}`;
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve an import from `fromFile`.
|
|
174
|
+
* @returns {{ file?: string, dir?: string } | null} null → external dependency
|
|
175
|
+
*/
|
|
176
|
+
resolve(lang, fromFile, source) {
|
|
177
|
+
if (!source) return null;
|
|
178
|
+
const fromDir = path.posix.dirname(fromFile);
|
|
179
|
+
switch (lang) {
|
|
180
|
+
case 'javascript': case 'typescript': case 'tsx': return this.#resolveEcma(fromDir, source);
|
|
181
|
+
case 'python': return this.#resolvePython(fromDir, source);
|
|
182
|
+
case 'go': return this.#resolveGo(source);
|
|
183
|
+
case 'c': case 'cpp': return this.#resolveInclude(fromDir, source);
|
|
184
|
+
case 'ruby': case 'bash': case 'css': return this.#resolveRelativeOrSuffix(fromDir, source, lang);
|
|
185
|
+
case 'rust': return this.#resolveRust(fromFile, source);
|
|
186
|
+
default: return null; // package-based languages resolve through the package index
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#resolveEcma(fromDir, source) {
|
|
191
|
+
if (source.startsWith('.')) {
|
|
192
|
+
const f = this.#ecmaFile(path.posix.normalize(path.posix.join(fromDir, source)));
|
|
193
|
+
return f ? { file: f } : null;
|
|
194
|
+
}
|
|
195
|
+
if (source.startsWith('/')) return null;
|
|
196
|
+
const scopes = this.#tsScopesFor(fromDir);
|
|
197
|
+
for (const { pat, targets } of scopes.flatMap(s => s.paths)) {
|
|
198
|
+
const star = pat.indexOf('*');
|
|
199
|
+
if (star < 0 ? pat === source : (source.startsWith(pat.slice(0, star)) && source.endsWith(pat.slice(star + 1)))) {
|
|
200
|
+
const mid = star < 0 ? '' : source.slice(star, source.length - (pat.length - star - 1));
|
|
201
|
+
for (const t of targets) {
|
|
202
|
+
const f = this.#ecmaFile(t.replace('*', mid));
|
|
203
|
+
if (f) return { file: f };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// workspace package (monorepo): '@scope/pkg' or '@scope/pkg/sub/path'
|
|
208
|
+
const parts = source.split('/');
|
|
209
|
+
const pkgName = source.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
210
|
+
const sub = source.startsWith('@') ? parts.slice(2).join('/') : parts.slice(1).join('/');
|
|
211
|
+
const pkg = this.packages.get(pkgName);
|
|
212
|
+
if (pkg) {
|
|
213
|
+
const base = pkg.dir === '.' ? '' : pkg.dir + '/';
|
|
214
|
+
if (sub) {
|
|
215
|
+
const f = this.#ecmaFile(base + sub) ?? this.#ecmaFile(base + 'src/' + sub) ?? this.#ecmaFile(base + 'lib/' + sub);
|
|
216
|
+
if (f) return { file: f };
|
|
217
|
+
}
|
|
218
|
+
const j = pkg.json;
|
|
219
|
+
const entries = [j.source, j.types, j.typings, j.module, j.main, 'index', 'src/index', 'lib/index'].filter(Boolean);
|
|
220
|
+
for (const e of entries) {
|
|
221
|
+
const f = this.#ecmaFile(path.posix.normalize(base + String(e).replace(/^\.\//, '')).replace(/\.d\.ts$/, '')) ?? this.#ecmaFile(path.posix.normalize(base + String(e).replace(/^\.\//, '')));
|
|
222
|
+
if (f) return { file: f };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const s of scopes) if (s.baseUrl) {
|
|
226
|
+
const f = this.#ecmaFile(path.posix.normalize(path.posix.join(s.baseUrl, source)));
|
|
227
|
+
if (f) return { file: f };
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#resolvePython(fromDir, source) {
|
|
233
|
+
let base;
|
|
234
|
+
let mod = source;
|
|
235
|
+
if (source.startsWith('.')) {
|
|
236
|
+
const dots = /^\.+/.exec(source)[0].length;
|
|
237
|
+
base = fromDir;
|
|
238
|
+
for (let i = 1; i < dots; i++) base = path.posix.dirname(base);
|
|
239
|
+
mod = source.slice(dots);
|
|
240
|
+
const p = mod ? path.posix.join(base, mod.replace(/\./g, '/')) : base;
|
|
241
|
+
if (this.files.has(p + '.py')) return { file: p + '.py' };
|
|
242
|
+
if (this.files.has(p + '/__init__.py')) return { file: p + '/__init__.py', dir: p };
|
|
243
|
+
if (this.dirs.has(p)) return { dir: p };
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const rel = mod.replace(/\./g, '/');
|
|
247
|
+
const idx = this.#suffixIndex();
|
|
248
|
+
const cands = [...(idx.get(rel + '.py') ?? []), ...(idx.get(rel + '/__init__.py') ?? [])]
|
|
249
|
+
.filter(f => f === rel + '.py' || f === rel + '/__init__.py' || f.endsWith('/' + rel + '.py') || f.endsWith('/' + rel + '/__init__.py'));
|
|
250
|
+
if (cands.length) {
|
|
251
|
+
cands.sort((a, b) => a.length - b.length);
|
|
252
|
+
const f = cands[0];
|
|
253
|
+
return f.endsWith('/__init__.py') ? { file: f, dir: path.posix.dirname(f) } : { file: f };
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#resolveGo(source) {
|
|
259
|
+
for (const { module, dir } of this.goModules) {
|
|
260
|
+
if (source === module || source.startsWith(module + '/')) {
|
|
261
|
+
const sub = source.slice(module.length).replace(/^\//, '');
|
|
262
|
+
const d = path.posix.normalize(dir === '.' ? (sub || '.') : (sub ? `${dir}/${sub}` : dir));
|
|
263
|
+
return this.dirs.has(d) ? { dir: d } : null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#resolveInclude(fromDir, source) {
|
|
270
|
+
const direct = path.posix.normalize(path.posix.join(fromDir, source));
|
|
271
|
+
if (this.files.has(direct)) return { file: direct };
|
|
272
|
+
const idx = this.#suffixIndex();
|
|
273
|
+
const cands = (idx.get(source) ?? []).filter(f => f === source || f.endsWith('/' + source));
|
|
274
|
+
if (cands.length) {
|
|
275
|
+
cands.sort((a, b) => a.length - b.length);
|
|
276
|
+
return { file: cands[0] };
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#resolveRelativeOrSuffix(fromDir, source, lang) {
|
|
282
|
+
const exts = lang === 'ruby' ? ['', '.rb'] : lang === 'css' ? ['', '.css', '.scss'] : ['', '.sh', '.bash'];
|
|
283
|
+
if (source.startsWith('.') || lang === 'bash') {
|
|
284
|
+
for (const e of exts) {
|
|
285
|
+
const p = path.posix.normalize(path.posix.join(fromDir, source + e));
|
|
286
|
+
if (this.files.has(p)) return { file: p };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const idx = this.#suffixIndex();
|
|
290
|
+
for (const e of exts) {
|
|
291
|
+
const s = source.replace(/^\.\//, '') + e;
|
|
292
|
+
const cands = (idx.get(s) ?? []).filter(f => f === s || f.endsWith('/' + s));
|
|
293
|
+
if (cands.length) { cands.sort((a, b) => a.length - b.length); return { file: cands[0] }; }
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#resolveRust(fromFile, source) {
|
|
299
|
+
// crate::a::b / self::a / super::a → src/a/b.rs | src/a/b/mod.rs | src/a.rs (item b inside)
|
|
300
|
+
const segs = source.split('::').filter(Boolean);
|
|
301
|
+
if (!segs.length) return null;
|
|
302
|
+
let baseDir;
|
|
303
|
+
const crateRoot = this.#rustCrateRoot(fromFile);
|
|
304
|
+
if (segs[0] === 'crate') { baseDir = crateRoot; segs.shift(); }
|
|
305
|
+
else if (segs[0] === 'self') { baseDir = this.#rustModDir(fromFile); segs.shift(); }
|
|
306
|
+
else if (segs[0] === 'super') { baseDir = path.posix.dirname(this.#rustModDir(fromFile)); segs.shift(); while (segs[0] === 'super') { baseDir = path.posix.dirname(baseDir); segs.shift(); } }
|
|
307
|
+
else return null; // external crate
|
|
308
|
+
for (let n = segs.length; n > 0; n--) {
|
|
309
|
+
const p = path.posix.join(baseDir, ...segs.slice(0, n));
|
|
310
|
+
if (this.files.has(p + '.rs')) return { file: p + '.rs' };
|
|
311
|
+
if (this.files.has(p + '/mod.rs')) return { file: p + '/mod.rs' };
|
|
312
|
+
}
|
|
313
|
+
const lib = ['lib.rs', 'main.rs', 'mod.rs'].map(f => path.posix.join(baseDir, f)).find(f => this.files.has(f));
|
|
314
|
+
return lib ? { file: lib } : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#rustCrateRoot(fromFile) {
|
|
318
|
+
let d = path.posix.dirname(fromFile);
|
|
319
|
+
while (d && d !== '.') {
|
|
320
|
+
if (this.files.has(d + '/lib.rs') || this.files.has(d + '/main.rs')) return d;
|
|
321
|
+
d = path.posix.dirname(d);
|
|
322
|
+
}
|
|
323
|
+
return 'src';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#rustModDir(fromFile) {
|
|
327
|
+
const base = path.posix.basename(fromFile);
|
|
328
|
+
const dir = path.posix.dirname(fromFile);
|
|
329
|
+
return (base === 'mod.rs' || base === 'lib.rs' || base === 'main.rs') ? dir : path.posix.join(dir, base.replace(/\.rs$/, ''));
|
|
330
|
+
}
|
|
331
|
+
}
|