context-slice 1.8.2
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/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/src/cli.js +199 -0
- package/dist/src/indexer/index.js +325 -0
- package/dist/src/languages/adapter.js +23 -0
- package/dist/src/languages/go/index.js +15 -0
- package/dist/src/languages/go/parse.js +443 -0
- package/dist/src/languages/go/resolve.js +313 -0
- package/dist/src/languages/java/enterprise/dependency-injection.js +175 -0
- package/dist/src/languages/java/enterprise/jpa-entity.js +89 -0
- package/dist/src/languages/java/enterprise/registry.js +22 -0
- package/dist/src/languages/java/enterprise/spring-data.js +239 -0
- package/dist/src/languages/java/enterprise/spring-mvc.js +183 -0
- package/dist/src/languages/java/enterprise/transactions.js +110 -0
- package/dist/src/languages/java.js +84 -0
- package/dist/src/languages/javascript/index.js +25 -0
- package/dist/src/languages/python/index.js +29 -0
- package/dist/src/languages/python/parse.js +415 -0
- package/dist/src/languages/python/resolve.js +413 -0
- package/dist/src/languages/rust/calls-resolve.js +1405 -0
- package/dist/src/languages/rust/index.js +12 -0
- package/dist/src/languages/rust/parse.js +545 -0
- package/dist/src/languages/rust/resolve.js +284 -0
- package/dist/src/languages/typescript/index.js +25 -0
- package/dist/src/languages/typescript/parse.js +793 -0
- package/dist/src/languages/typescript/resolve.js +463 -0
- package/dist/src/package-info.js +16 -0
- package/dist/src/parser/java-parser.js +339 -0
- package/dist/src/planner/budget.js +1 -0
- package/dist/src/planner/composition.js +372 -0
- package/dist/src/planner/rank.js +10 -0
- package/dist/src/render/compact-context.js +11 -0
- package/dist/src/server/mcp-server.js +154 -0
- package/dist/src/storage/sqlite.js +105 -0
- package/dist/src/types/enterprise.js +1 -0
- package/dist/src/types/model.js +1 -0
- package/dist/src/workflow/errors.js +10 -0
- package/dist/src/workflow/preview.js +220 -0
- package/dist/src/workflow/repository.js +56 -0
- package/mcp.json +10 -0
- package/package.json +76 -0
- package/plugin.json +15 -0
- package/queries/java/annotations.scm +1 -0
- package/queries/java/calls.scm +1 -0
- package/queries/java/imports.scm +1 -0
- package/queries/java/symbols.scm +2 -0
- package/skills/context-slice/SKILL.md +60 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { leaveUnresolvedOnError, resolveCallsA } from "./calls-resolve.js";
|
|
4
|
+
import { modulePathFor } from "./parse.js";
|
|
5
|
+
function cargoPackageName(tomlText) {
|
|
6
|
+
const section = tomlText.match(/\[package\]([\s\S]*?)(?:\n\[|$)/);
|
|
7
|
+
const body = section ? section[1] : tomlText;
|
|
8
|
+
return body.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
|
|
9
|
+
}
|
|
10
|
+
function cargoWorkspaceMembers(tomlText) {
|
|
11
|
+
const section = tomlText.match(/\[workspace\]([\s\S]*?)(?:\n\[|$)/);
|
|
12
|
+
if (!section)
|
|
13
|
+
return [];
|
|
14
|
+
const members = section[1].match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
15
|
+
if (!members)
|
|
16
|
+
return [];
|
|
17
|
+
return [...members[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
18
|
+
}
|
|
19
|
+
/** Only the simple "dir/*" trailing-glob form is supported — real Cargo globs (nested
|
|
20
|
+
* wildcards, exclude patterns) are rare enough in practice not to special-case here. */
|
|
21
|
+
function expandMemberPattern(root, pattern) {
|
|
22
|
+
const clean = pattern.replace(/\/$/, "");
|
|
23
|
+
if (!clean.endsWith("/*"))
|
|
24
|
+
return [clean];
|
|
25
|
+
const base = clean.slice(0, -2);
|
|
26
|
+
try {
|
|
27
|
+
return readdirSync(join(root, base), { withFileTypes: true })
|
|
28
|
+
.filter((entry) => entry.isDirectory())
|
|
29
|
+
.map((entry) => `${base}/${entry.name}`);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Every crate this project can resolve `use` paths into: a root Cargo.toml's own
|
|
36
|
+
* `[package]` (if any) plus every `[workspace]` member's own `[package]`. No Cargo.toml
|
|
37
|
+
* means no known crates — callers fall back to treating the whole project as one crate,
|
|
38
|
+
* which is the project's existing, correct single-crate behavior. */
|
|
39
|
+
function discoverCrates(root) {
|
|
40
|
+
let rootToml;
|
|
41
|
+
try {
|
|
42
|
+
rootToml = readFileSync(join(root, "Cargo.toml"), "utf8");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const crates = [];
|
|
48
|
+
const rootName = cargoPackageName(rootToml);
|
|
49
|
+
if (rootName)
|
|
50
|
+
crates.push({ name: rootName, dir: "" });
|
|
51
|
+
for (const pattern of cargoWorkspaceMembers(rootToml)) {
|
|
52
|
+
for (const memberDir of expandMemberPattern(root, pattern)) {
|
|
53
|
+
let memberToml;
|
|
54
|
+
try {
|
|
55
|
+
memberToml = readFileSync(join(root, memberDir, "Cargo.toml"), "utf8");
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const name = cargoPackageName(memberToml);
|
|
61
|
+
if (name)
|
|
62
|
+
crates.push({ name, dir: memberDir });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return crates;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Map every indexed .rs file to its crate-relative module path, and back.
|
|
69
|
+
* A module-path key claimed by two different files (e.g. two crates' lib.rs
|
|
70
|
+
* in a workspace) is ambiguous: it is dropped from `byModule` and recorded in
|
|
71
|
+
* `ambiguous`, so it resolves to nothing rather than to whichever came first.
|
|
72
|
+
*/
|
|
73
|
+
export function rustModuleIndex(files) {
|
|
74
|
+
const byModule = new Map();
|
|
75
|
+
const ambiguous = new Set();
|
|
76
|
+
const byFile = new Map();
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const path = modulePathFor(file);
|
|
79
|
+
byFile.set(file, path);
|
|
80
|
+
const key = path.join("::");
|
|
81
|
+
if (ambiguous.has(key))
|
|
82
|
+
continue;
|
|
83
|
+
const existing = byModule.get(key);
|
|
84
|
+
if (existing !== undefined && existing !== file) {
|
|
85
|
+
byModule.delete(key);
|
|
86
|
+
ambiguous.add(key);
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
byModule.set(key, file);
|
|
90
|
+
}
|
|
91
|
+
return { byModule, byFile, ambiguous };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a `use` target's path segments to an indexed file, or report it
|
|
95
|
+
* as external. Deterministic and conservative.
|
|
96
|
+
*
|
|
97
|
+
* Anchored (crate/self/super) targets can only mean "somewhere in this
|
|
98
|
+
* crate", so a miss is `{}`, never external. At file granularity a
|
|
99
|
+
* module-only anchored path whose module has no file of its own (e.g.
|
|
100
|
+
* ["crate","nope"], where `nope` may be an inline `mod` or an item in the
|
|
101
|
+
* crate root) falls back to the containing module's file via `asContaining`
|
|
102
|
+
* (here the crate root); that is the documented file-granularity fallback.
|
|
103
|
+
*
|
|
104
|
+
* Non-anchored targets follow Rust 2018: first a descendant module of the
|
|
105
|
+
* CURRENT module (`[...here, ...segments]`), else an extern crate. Only a
|
|
106
|
+
* match strictly deeper than the current module counts, so a path can never
|
|
107
|
+
* fall back to the current file or an ancestor (e.g. lib.rs).
|
|
108
|
+
*
|
|
109
|
+
* A module-path key claimed by several files is ambiguous and never guessed:
|
|
110
|
+
* the result is `{}`.
|
|
111
|
+
*
|
|
112
|
+
* Accepts EITHER a containing-module path (what `resolveRustCalls` passes,
|
|
113
|
+
* since `ImportRecord.module` is everything but the final segment) OR a full
|
|
114
|
+
* path with the trailing item name (used by the unit tests). Both work via
|
|
115
|
+
* the two-step lookup `asModule` (exact) / `asContaining` (drop last segment).
|
|
116
|
+
*/
|
|
117
|
+
export function resolveRustModule(segments, fromFile, index,
|
|
118
|
+
/** Names bound in `fromFile` (declared items, imports). A non-anchored path starting with one is not an external crate. */
|
|
119
|
+
boundNames,
|
|
120
|
+
/** Other crates in this workspace, by Cargo.toml package name — lets a non-anchored path
|
|
121
|
+
* whose first segment names a sibling crate resolve into THAT crate's own module index,
|
|
122
|
+
* instead of being reported as an external (non-workspace) package. */
|
|
123
|
+
otherCrates) {
|
|
124
|
+
const [anchor, ...rest] = segments;
|
|
125
|
+
const anchored = anchor === "crate" || anchor === "self" || anchor === "super";
|
|
126
|
+
const here = index.byFile.get(fromFile) ?? [];
|
|
127
|
+
let absolute;
|
|
128
|
+
let minLen = 0; // non-anchored: matches must be strictly deeper than `here`
|
|
129
|
+
if (anchor === "crate") {
|
|
130
|
+
absolute = rest;
|
|
131
|
+
}
|
|
132
|
+
else if (anchor === "self" || anchor === "super") {
|
|
133
|
+
if (!index.byFile.has(fromFile))
|
|
134
|
+
return {};
|
|
135
|
+
const base = anchor === "super" ? here.slice(0, Math.max(0, here.length - 1)) : here;
|
|
136
|
+
absolute = [...base, ...rest];
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
absolute = [...here, ...segments];
|
|
140
|
+
minLen = here.length + 1;
|
|
141
|
+
}
|
|
142
|
+
for (const path of [absolute, absolute.slice(0, -1)]) {
|
|
143
|
+
if (path.length < minLen)
|
|
144
|
+
continue;
|
|
145
|
+
const key = path.join("::");
|
|
146
|
+
if (index.ambiguous.has(key))
|
|
147
|
+
return {};
|
|
148
|
+
const file = index.byModule.get(key);
|
|
149
|
+
if (file)
|
|
150
|
+
return { file };
|
|
151
|
+
}
|
|
152
|
+
if (anchored)
|
|
153
|
+
return {};
|
|
154
|
+
if (anchor && boundNames?.has(anchor))
|
|
155
|
+
return {}; // bound locally: unresolved, not external
|
|
156
|
+
const sibling = anchor ? otherCrates?.get(anchor) : undefined;
|
|
157
|
+
if (sibling) {
|
|
158
|
+
for (const path of [rest, rest.slice(0, -1)]) {
|
|
159
|
+
const key = path.join("::");
|
|
160
|
+
if (sibling.ambiguous.has(key))
|
|
161
|
+
return {};
|
|
162
|
+
const file = sibling.byModule.get(key);
|
|
163
|
+
if (file)
|
|
164
|
+
return { file };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { externalPackage: anchor };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Resolves every Rust ImportRecord's `resolvedFile`/`externalPackage` in
|
|
171
|
+
* place, then resolves CallEdges to symbols (structural targets, see
|
|
172
|
+
* calls-resolve.ts).
|
|
173
|
+
*/
|
|
174
|
+
export function resolveRustCalls(context) {
|
|
175
|
+
const files = [...new Set(context.symbols.map((symbol) => symbol.filePath))];
|
|
176
|
+
for (const record of [...context.imports, ...context.exports])
|
|
177
|
+
files.push(record.filePath);
|
|
178
|
+
const allFiles = [...new Set(files)];
|
|
179
|
+
// Partition files by crate (longest directory-prefix match) and build one module index per
|
|
180
|
+
// crate, so two crates' own src/lib.rs (both module path []) never collide into one "ambiguous"
|
|
181
|
+
// key the way a single project-wide index would. No Cargo.toml, or a Cargo.toml with no
|
|
182
|
+
// crates discovered, falls back to exactly one crate covering the whole project — the
|
|
183
|
+
// project's prior, unpartitioned behavior.
|
|
184
|
+
const discovered = discoverCrates(context.root);
|
|
185
|
+
const crateDirs = discovered.length ? discovered : [{ name: "", dir: "" }];
|
|
186
|
+
if (!crateDirs.some((c) => c.dir === ""))
|
|
187
|
+
crateDirs.push({ name: "", dir: "" });
|
|
188
|
+
const dirsByLengthDesc = [...crateDirs].sort((a, b) => b.dir.length - a.dir.length);
|
|
189
|
+
const crateFor = (file) => dirsByLengthDesc.find((c) => c.dir === "" || file === c.dir || file.startsWith(c.dir + "/")) ??
|
|
190
|
+
dirsByLengthDesc[dirsByLengthDesc.length - 1];
|
|
191
|
+
const filesByCrateDir = new Map();
|
|
192
|
+
for (const file of allFiles) {
|
|
193
|
+
const dir = crateFor(file).dir;
|
|
194
|
+
const list = filesByCrateDir.get(dir) ?? [];
|
|
195
|
+
list.push(file);
|
|
196
|
+
filesByCrateDir.set(dir, list);
|
|
197
|
+
}
|
|
198
|
+
const indexByCrateDir = new Map();
|
|
199
|
+
const indexByCrateName = new Map();
|
|
200
|
+
for (const crate of crateDirs) {
|
|
201
|
+
const crateIndex = rustModuleIndex(filesByCrateDir.get(crate.dir) ?? []);
|
|
202
|
+
indexByCrateDir.set(crate.dir, crateIndex);
|
|
203
|
+
if (crate.name)
|
|
204
|
+
indexByCrateName.set(crate.name, crateIndex);
|
|
205
|
+
}
|
|
206
|
+
const indexFor = (file) => indexByCrateDir.get(crateFor(file).dir);
|
|
207
|
+
// Per-file bound names: declared items at any nesting + names bound by imports.
|
|
208
|
+
// A bare unaliased `use foo;` binds the crate name itself, so it does not count.
|
|
209
|
+
const boundByFile = new Map();
|
|
210
|
+
const bind = (file, name) => {
|
|
211
|
+
if (!name)
|
|
212
|
+
return;
|
|
213
|
+
let set = boundByFile.get(file);
|
|
214
|
+
if (!set)
|
|
215
|
+
boundByFile.set(file, (set = new Set()));
|
|
216
|
+
set.add(name);
|
|
217
|
+
};
|
|
218
|
+
// Only type-namespace items (struct/enum/trait/type/mod) can start a `use` path;
|
|
219
|
+
// a fn/const/static with the same name as a crate does not shadow it.
|
|
220
|
+
for (const symbol of context.symbols)
|
|
221
|
+
if (symbol.kind !== "function" && symbol.kind !== "variable")
|
|
222
|
+
bind(symbol.filePath, symbol.name);
|
|
223
|
+
for (const record of context.imports) {
|
|
224
|
+
if (record.wildcard)
|
|
225
|
+
continue;
|
|
226
|
+
const bareCrate = record.module === record.importedName && record.localName === record.importedName;
|
|
227
|
+
if (!bareCrate)
|
|
228
|
+
bind(record.filePath, record.localName);
|
|
229
|
+
}
|
|
230
|
+
for (const record of context.imports) {
|
|
231
|
+
const resolved = resolveRustModule(record.module.split("::"), record.filePath, indexFor(record.filePath), boundByFile.get(record.filePath), indexByCrateName);
|
|
232
|
+
record.resolvedFile = resolved.file;
|
|
233
|
+
record.externalPackage = resolved.externalPackage;
|
|
234
|
+
}
|
|
235
|
+
const exportsByFile = new Map();
|
|
236
|
+
for (const record of context.exports) {
|
|
237
|
+
if (record.fromModule) {
|
|
238
|
+
record.resolvedFile = resolveRustModule(record.fromModule.split("::"), record.filePath, indexFor(record.filePath), undefined, indexByCrateName).file;
|
|
239
|
+
}
|
|
240
|
+
const list = exportsByFile.get(record.filePath) ?? [];
|
|
241
|
+
list.push(record);
|
|
242
|
+
exportsByFile.set(record.filePath, list);
|
|
243
|
+
}
|
|
244
|
+
const symbolsByFileAndName = new Map(); // `${file}#${name}` -> symbolId
|
|
245
|
+
for (const symbol of context.symbols)
|
|
246
|
+
symbolsByFileAndName.set(`${symbol.filePath}#${symbol.name}`, symbol.id);
|
|
247
|
+
/** Follows `pub use` re-export chains; `seen` breaks cycles (mirrors python resolve.ts's `lookup`). */
|
|
248
|
+
const lookup = (file, name, seen) => {
|
|
249
|
+
const key = `${file}#${name}`;
|
|
250
|
+
if (seen.has(key))
|
|
251
|
+
return undefined;
|
|
252
|
+
seen.add(key);
|
|
253
|
+
const direct = symbolsByFileAndName.get(key);
|
|
254
|
+
if (direct)
|
|
255
|
+
return direct;
|
|
256
|
+
const records = exportsByFile.get(file) ?? [];
|
|
257
|
+
const reexport = records.find((record) => record.exportedName === name && record.resolvedFile);
|
|
258
|
+
if (reexport?.resolvedFile)
|
|
259
|
+
return lookup(reexport.resolvedFile, reexport.sourceName ?? name, seen);
|
|
260
|
+
for (const wildcard of records.filter((record) => record.wildcard))
|
|
261
|
+
if (wildcard.resolvedFile) {
|
|
262
|
+
const found = lookup(wildcard.resolvedFile, name, seen);
|
|
263
|
+
if (found)
|
|
264
|
+
return found;
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
};
|
|
268
|
+
for (const record of context.exports) {
|
|
269
|
+
record.symbolId = undefined; // drop any stale id loaded from the cache
|
|
270
|
+
if (record.resolvedFile && record.sourceName)
|
|
271
|
+
record.symbolId = lookup(record.resolvedFile, record.sourceName, new Set());
|
|
272
|
+
}
|
|
273
|
+
// Exact module file for a containing-module path: the dummy trailing segment makes
|
|
274
|
+
// resolveRustModule's "drop last segment" fallback land on `segments` itself, never a parent.
|
|
275
|
+
try {
|
|
276
|
+
resolveCallsA(context, {
|
|
277
|
+
moduleOf: (segments, fromFile) => resolveRustModule([...segments, "\u0000"], fromFile, indexFor(fromFile), boundByFile.get(fromFile), indexByCrateName),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// A throw outside the per-edge guard (impl pre-pass): no Rust edge is trusted, the rebuild continues.
|
|
282
|
+
context.calls.forEach(leaveUnresolvedOnError);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { registerLanguage } from "../adapter.js";
|
|
2
|
+
import { LANGUAGE_ID, parseTypeScript } from "./parse.js";
|
|
3
|
+
import { resolveTypeScriptCalls, typeScriptDiagnostics } from "./resolve.js";
|
|
4
|
+
export const typeScriptAdapter = {
|
|
5
|
+
id: LANGUAGE_ID,
|
|
6
|
+
label: "TypeScript",
|
|
7
|
+
// Longest-suffix matching means ".d.ts" is recognized before ".ts".
|
|
8
|
+
extensions: [".ts", ".tsx", ".mts", ".cts", ".d.ts"],
|
|
9
|
+
ignoredDirectories: [
|
|
10
|
+
"node_modules",
|
|
11
|
+
".next",
|
|
12
|
+
".nuxt",
|
|
13
|
+
".turbo",
|
|
14
|
+
".cache",
|
|
15
|
+
"coverage",
|
|
16
|
+
"storybook-static",
|
|
17
|
+
],
|
|
18
|
+
parse: parseTypeScript,
|
|
19
|
+
resolveCalls: (context) => {
|
|
20
|
+
resolveTypeScriptCalls(context);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
registerLanguage(typeScriptAdapter);
|
|
24
|
+
export { typeScriptDiagnostics };
|
|
25
|
+
export { LANGUAGE_ID as TYPESCRIPT_LANGUAGE_ID };
|