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,12 @@
|
|
|
1
|
+
import { registerLanguage } from "../adapter.js";
|
|
2
|
+
import { LANGUAGE_ID, parseRust } from "./parse.js";
|
|
3
|
+
import { resolveRustCalls } from "./resolve.js";
|
|
4
|
+
export const rustAdapter = {
|
|
5
|
+
id: LANGUAGE_ID,
|
|
6
|
+
label: "Rust",
|
|
7
|
+
extensions: [".rs"],
|
|
8
|
+
ignoredDirectories: ["target"],
|
|
9
|
+
parse: parseRust,
|
|
10
|
+
resolveCalls: resolveRustCalls,
|
|
11
|
+
};
|
|
12
|
+
registerLanguage(rustAdapter);
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import Parser from "tree-sitter";
|
|
2
|
+
import Rust from "tree-sitter-rust";
|
|
3
|
+
export const LANGUAGE_ID = "rust";
|
|
4
|
+
let parser;
|
|
5
|
+
export function rustParser() {
|
|
6
|
+
if (!parser) {
|
|
7
|
+
parser = new Parser();
|
|
8
|
+
parser.setLanguage(Rust);
|
|
9
|
+
}
|
|
10
|
+
return parser;
|
|
11
|
+
}
|
|
12
|
+
export const range = (node) => ({
|
|
13
|
+
startLine: node.startPosition.row + 1,
|
|
14
|
+
startColumn: node.startPosition.column,
|
|
15
|
+
endLine: node.endPosition.row + 1,
|
|
16
|
+
endColumn: node.endPosition.column,
|
|
17
|
+
});
|
|
18
|
+
export const field = (node, name) => node.childForFieldName(name);
|
|
19
|
+
export const text = (node) => node?.text ?? "";
|
|
20
|
+
const ITEM_TYPES = new Set([
|
|
21
|
+
"function_item",
|
|
22
|
+
"function_signature_item",
|
|
23
|
+
"struct_item",
|
|
24
|
+
"enum_item",
|
|
25
|
+
"trait_item",
|
|
26
|
+
"impl_item",
|
|
27
|
+
"mod_item",
|
|
28
|
+
"const_item",
|
|
29
|
+
"static_item",
|
|
30
|
+
"type_item",
|
|
31
|
+
]);
|
|
32
|
+
// SymbolKind (src/types/model.ts) has no "struct" or "trait" member —
|
|
33
|
+
// verified directly against the live type before writing this plan, do not
|
|
34
|
+
// add "struct"/"trait" as kind values, tsc will reject them.
|
|
35
|
+
const KIND_BY_NODE_TYPE = {
|
|
36
|
+
function_item: "function",
|
|
37
|
+
function_signature_item: "function",
|
|
38
|
+
struct_item: "class", // closest existing kind for a data-bearing named type
|
|
39
|
+
enum_item: "enum",
|
|
40
|
+
trait_item: "interface", // closest existing kind: a trait is a behavioral contract
|
|
41
|
+
impl_item: "type", // impl blocks are containers, not themselves a named declaration; shares "type" with type aliases deliberately, revisit if this causes ambiguity in a later phase
|
|
42
|
+
mod_item: "namespace",
|
|
43
|
+
const_item: "variable",
|
|
44
|
+
static_item: "variable",
|
|
45
|
+
type_item: "type",
|
|
46
|
+
};
|
|
47
|
+
const isPub = (node) => node.children.some((child) => child.type === "visibility_modifier");
|
|
48
|
+
// `async` is not a direct child of function_item — tree-sitter-rust 0.21 nests
|
|
49
|
+
// it inside a `function_modifiers` child (verified empirically; the brief's
|
|
50
|
+
// "direct child" assumption didn't hold here, unlike visibility_modifier).
|
|
51
|
+
const isAsync = (node) => node.children.some((child) => child.type === "async" ||
|
|
52
|
+
(child.type === "function_modifiers" &&
|
|
53
|
+
child.children.some((c) => c.type === "async")));
|
|
54
|
+
function canonicalId(filePath, chain, kind, name) {
|
|
55
|
+
return [filePath, ...chain, kind, name].join("::");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The crate-relative module path a `use` statement would need to name this
|
|
59
|
+
* file, per the standard `src/lib.rs`/`src/main.rs`/`src/foo.rs`/`src/foo/mod.rs`
|
|
60
|
+
* layout (spec §10). Computed purely from this file's own path — no
|
|
61
|
+
* repository-wide listing is needed, since the convention is per-file.
|
|
62
|
+
* Multi-crate workspaces and `src/bin/*.rs` binaries are not modeled (§41-46,
|
|
63
|
+
* §60, deferred).
|
|
64
|
+
*/
|
|
65
|
+
export function modulePathFor(filePath) {
|
|
66
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
67
|
+
const srcMatch = /(^|\/)src\//.exec(normalized); // segment-safe: not `mysrc/`
|
|
68
|
+
const relative = srcMatch ? normalized.slice(srcMatch.index + srcMatch[0].length) : normalized;
|
|
69
|
+
const parts = relative.split("/").filter(Boolean);
|
|
70
|
+
const last = parts.pop() ?? "";
|
|
71
|
+
const base = last.replace(/\.rs$/, "");
|
|
72
|
+
if (base === "lib" || base === "main" || base === "mod")
|
|
73
|
+
return parts;
|
|
74
|
+
return [...parts, base];
|
|
75
|
+
}
|
|
76
|
+
/** impl blocks have no `name` field — label them by their Self type (+ trait, if any). */
|
|
77
|
+
function implLabel(node) {
|
|
78
|
+
const selfType = text(field(node, "type"));
|
|
79
|
+
const traitNode = field(node, "trait");
|
|
80
|
+
return traitNode ? `impl ${text(traitNode)} for ${selfType}` : `impl ${selfType}`;
|
|
81
|
+
}
|
|
82
|
+
function itemName(node) {
|
|
83
|
+
if (node.type === "impl_item")
|
|
84
|
+
return implLabel(node);
|
|
85
|
+
return text(field(node, "name"));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Flattens a `scoped_identifier` chain (or a bare `crate`/`self`/`super`/
|
|
89
|
+
* `identifier` leaf) into its segments in source order, e.g.
|
|
90
|
+
* `crate::service::create_order` -> ["crate", "service", "create_order"].
|
|
91
|
+
*/
|
|
92
|
+
function pathSegments(node) {
|
|
93
|
+
if (node.type === "scoped_identifier") {
|
|
94
|
+
const path = field(node, "path");
|
|
95
|
+
const name = text(field(node, "name"));
|
|
96
|
+
return path ? [...pathSegments(path), name] : [name];
|
|
97
|
+
}
|
|
98
|
+
return [text(node)];
|
|
99
|
+
}
|
|
100
|
+
/** One `use` statement can bind several names (a grouped list) — always returns an array, even for the common single-binding case. */
|
|
101
|
+
function parseUseDeclaration(node, filePath) {
|
|
102
|
+
const argument = field(node, "argument");
|
|
103
|
+
if (!argument)
|
|
104
|
+
return [];
|
|
105
|
+
const baseRecord = {
|
|
106
|
+
filePath,
|
|
107
|
+
language: LANGUAGE_ID,
|
|
108
|
+
kind: "named",
|
|
109
|
+
typeOnly: false,
|
|
110
|
+
range: range(node),
|
|
111
|
+
};
|
|
112
|
+
const records = [];
|
|
113
|
+
const leaf = (full, localName) => {
|
|
114
|
+
const importedName = full.at(-1);
|
|
115
|
+
if (!importedName)
|
|
116
|
+
return;
|
|
117
|
+
const module = full.length > 1 ? full.slice(0, -1).join("::") : importedName;
|
|
118
|
+
records.push({ ...baseRecord, module, importedName, localName: localName ?? importedName });
|
|
119
|
+
};
|
|
120
|
+
const wildcard = (full) => {
|
|
121
|
+
if (full.length)
|
|
122
|
+
records.push({ ...baseRecord, module: full.join("::"), wildcard: true });
|
|
123
|
+
};
|
|
124
|
+
// `self` inside `m::{self}` imports module `m` itself (module = m's parent path).
|
|
125
|
+
const selfLeaf = (prefix, alias) => {
|
|
126
|
+
const name = prefix.at(-1);
|
|
127
|
+
if (!name)
|
|
128
|
+
return;
|
|
129
|
+
records.push({
|
|
130
|
+
...baseRecord,
|
|
131
|
+
module: prefix.length > 1 ? prefix.slice(0, -1).join("::") : name,
|
|
132
|
+
importedName: name,
|
|
133
|
+
localName: alias ?? name,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
// Recursive over the tree-sitter-rust 0.21 use shapes: scoped_use_list
|
|
137
|
+
// (path + use_list), use_list, use_wildcard, use_as_clause, scoped_identifier,
|
|
138
|
+
// identifier/self/crate/super. Any other node type emits nothing.
|
|
139
|
+
const useTree = (item, prefix) => {
|
|
140
|
+
switch (item.type) {
|
|
141
|
+
case "use_list":
|
|
142
|
+
for (const child of item.namedChildren)
|
|
143
|
+
useTree(child, prefix);
|
|
144
|
+
return;
|
|
145
|
+
case "scoped_use_list": {
|
|
146
|
+
const path = field(item, "path");
|
|
147
|
+
const list = field(item, "list");
|
|
148
|
+
if (list)
|
|
149
|
+
useTree(list, [...prefix, ...(path ? pathSegments(path) : [])]);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
case "use_wildcard": {
|
|
153
|
+
const path = item.namedChild(0);
|
|
154
|
+
wildcard([...prefix, ...(path ? pathSegments(path) : [])]);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case "use_as_clause": {
|
|
158
|
+
const path = field(item, "path");
|
|
159
|
+
const alias = text(field(item, "alias"));
|
|
160
|
+
if (!path || !alias)
|
|
161
|
+
return;
|
|
162
|
+
if (path.type === "self")
|
|
163
|
+
selfLeaf(prefix, alias);
|
|
164
|
+
else if (isPathNode(path))
|
|
165
|
+
leaf([...prefix, ...pathSegments(path)], alias);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
case "self":
|
|
169
|
+
selfLeaf(prefix);
|
|
170
|
+
return;
|
|
171
|
+
default:
|
|
172
|
+
if (isPathNode(item))
|
|
173
|
+
leaf([...prefix, ...pathSegments(item)]);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
useTree(argument, []);
|
|
177
|
+
return records;
|
|
178
|
+
}
|
|
179
|
+
const isPathNode = (node) => ["scoped_identifier", "identifier", "self", "crate", "super"].includes(node.type);
|
|
180
|
+
const isComment = (n) => n.type.endsWith("comment");
|
|
181
|
+
const namedNoComments = (n) => (n?.namedChildren ?? []).filter((c) => !isComment(c));
|
|
182
|
+
/** Names of an impl's own generic type parameters (not lifetimes/consts). */
|
|
183
|
+
export function typeParamNames(impl) {
|
|
184
|
+
const names = new Set();
|
|
185
|
+
for (const p of field(impl, "type_parameters")?.namedChildren ?? []) {
|
|
186
|
+
const n = p.type === "type_identifier" ? p : (field(p, "left") ?? field(p, "name"));
|
|
187
|
+
if (n?.type === "type_identifier")
|
|
188
|
+
names.add(n.text);
|
|
189
|
+
}
|
|
190
|
+
return names;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Base type name: strips generics, `&`/`&mut`, `dyn`, lifetimes and path prefixes.
|
|
194
|
+
* Undefined for shapes that are not one named type (slices, arrays, tuples, fn pointers,
|
|
195
|
+
* multi-bound `dyn`, `impl Trait`, `!`, `_`) and for names in `generics`.
|
|
196
|
+
*/
|
|
197
|
+
function baseTypeName(node, generics) {
|
|
198
|
+
if (!node)
|
|
199
|
+
return undefined;
|
|
200
|
+
let name;
|
|
201
|
+
switch (node.type) {
|
|
202
|
+
case "generic_type":
|
|
203
|
+
case "reference_type":
|
|
204
|
+
case "pointer_type":
|
|
205
|
+
return baseTypeName(field(node, "type"), generics);
|
|
206
|
+
case "dynamic_type":
|
|
207
|
+
return baseTypeName(field(node, "trait"), generics);
|
|
208
|
+
case "scoped_type_identifier":
|
|
209
|
+
name = text(field(node, "name"));
|
|
210
|
+
break;
|
|
211
|
+
case "type_identifier":
|
|
212
|
+
name = node.text;
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
return generics?.has(name) ? undefined : name;
|
|
218
|
+
}
|
|
219
|
+
function symbolMetadata(node) {
|
|
220
|
+
const meta = {};
|
|
221
|
+
if (node.type === "function_item" && isAsync(node))
|
|
222
|
+
meta.async = true;
|
|
223
|
+
if (node.type === "impl_item") {
|
|
224
|
+
const generics = typeParamNames(node);
|
|
225
|
+
meta.implSelfType = baseTypeName(field(node, "type"), generics);
|
|
226
|
+
const trait = baseTypeName(field(node, "trait"), generics);
|
|
227
|
+
if (trait)
|
|
228
|
+
meta.implTrait = trait;
|
|
229
|
+
}
|
|
230
|
+
else if (node.type === "struct_item") {
|
|
231
|
+
const types = {};
|
|
232
|
+
const body = field(node, "body");
|
|
233
|
+
if (body?.type === "field_declaration_list") {
|
|
234
|
+
for (const f of body.namedChildren)
|
|
235
|
+
if (f.type === "field_declaration")
|
|
236
|
+
types[text(field(f, "name"))] = text(field(f, "type"));
|
|
237
|
+
}
|
|
238
|
+
else if (body) {
|
|
239
|
+
body.childrenForFieldName("type").forEach((t, i) => (types[String(i)] = t.text));
|
|
240
|
+
}
|
|
241
|
+
if (Object.keys(types).length)
|
|
242
|
+
meta.declaredTypes = types;
|
|
243
|
+
}
|
|
244
|
+
else if (node.type === "function_item" || node.type === "function_signature_item") {
|
|
245
|
+
const types = {};
|
|
246
|
+
// declaredTypes.self is "&self" | "&mut self" | "self" for plain receivers, or the raw type
|
|
247
|
+
// text (e.g. "Box<Self>") for a typed `self: T`. Pattern params (`(a, b): T`, `_: T`) are skipped.
|
|
248
|
+
for (const p of namedNoComments(field(node, "parameters"))) {
|
|
249
|
+
if (p.type === "self_parameter") {
|
|
250
|
+
const ref = p.children.some((c) => c.type === "&");
|
|
251
|
+
types.self = ref ? (p.children.some((c) => c.type === "mutable_specifier") ? "&mut self" : "&self") : "self";
|
|
252
|
+
}
|
|
253
|
+
else if (p.type === "parameter") {
|
|
254
|
+
const pattern = field(p, "pattern");
|
|
255
|
+
if (pattern?.type === "identifier" || pattern?.type === "self")
|
|
256
|
+
types[pattern.text] = text(field(p, "type"));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (Object.keys(types).length)
|
|
260
|
+
meta.declaredTypes = types;
|
|
261
|
+
}
|
|
262
|
+
return Object.keys(meta).length ? meta : undefined;
|
|
263
|
+
}
|
|
264
|
+
/** Dot count of a pure ident/self field chain, or undefined if it contains anything else. */
|
|
265
|
+
const chainDots = (node) => {
|
|
266
|
+
if (node.type === "self" || node.type === "identifier")
|
|
267
|
+
return 0;
|
|
268
|
+
if (node.type !== "field_expression")
|
|
269
|
+
return undefined;
|
|
270
|
+
const inner = chainDots(field(node, "value"));
|
|
271
|
+
return inner === undefined ? undefined : inner + 1;
|
|
272
|
+
};
|
|
273
|
+
/** Chains of <= 3 dots: source text. `<field>` = chain too long; `<expr>` = chain contains a call/index/etc. */
|
|
274
|
+
function receiverOf(node) {
|
|
275
|
+
const dots = chainDots(node);
|
|
276
|
+
if (dots !== undefined)
|
|
277
|
+
return dots <= 3 ? node.text : "<field>";
|
|
278
|
+
return node.type === "field_expression" ? "<expr>" : `<${node.type.replace(/_expression$/, "")}>`;
|
|
279
|
+
}
|
|
280
|
+
/** Path prefix text without generic arguments (`Vec::<u8>` -> `Vec`). */
|
|
281
|
+
const prefixText = (node) => node.type === "generic_type" ? text(field(node, "type")) : node.text;
|
|
282
|
+
/** Turns a call_expression / macro_invocation into an unresolved edge. */
|
|
283
|
+
export function callEdge(node, filePath, callerId) {
|
|
284
|
+
const edge = {
|
|
285
|
+
callerId,
|
|
286
|
+
calleeName: "",
|
|
287
|
+
filePath,
|
|
288
|
+
language: LANGUAGE_ID,
|
|
289
|
+
range: range(node),
|
|
290
|
+
confidence: "unresolved",
|
|
291
|
+
resolutionKind: "unresolved",
|
|
292
|
+
evidence: [],
|
|
293
|
+
};
|
|
294
|
+
if (node.type === "macro_invocation") {
|
|
295
|
+
const macro = field(node, "macro");
|
|
296
|
+
edge.calleeName = macro.type === "scoped_identifier" ? text(field(macro, "name")) : macro.text;
|
|
297
|
+
if (macro.type === "scoped_identifier")
|
|
298
|
+
edge.receiverText = prefixText(field(macro, "path"));
|
|
299
|
+
edge.argumentCount = 0;
|
|
300
|
+
edge.evidence = [`macro:${edge.calleeName}`];
|
|
301
|
+
return edge;
|
|
302
|
+
}
|
|
303
|
+
edge.argumentCount = namedNoComments(field(node, "arguments")).length;
|
|
304
|
+
let fn = field(node, "function");
|
|
305
|
+
if (fn.type === "generic_function")
|
|
306
|
+
fn = field(fn, "function");
|
|
307
|
+
if (fn.type === "field_expression") {
|
|
308
|
+
edge.calleeName = text(field(fn, "field"));
|
|
309
|
+
edge.receiverText = receiverOf(field(fn, "value"));
|
|
310
|
+
}
|
|
311
|
+
else if (fn.type === "scoped_identifier") {
|
|
312
|
+
edge.calleeName = text(field(fn, "name"));
|
|
313
|
+
const path = field(fn, "path");
|
|
314
|
+
if (path)
|
|
315
|
+
edge.receiverText = prefixText(path);
|
|
316
|
+
const qualified = path?.type === "bracketed_type" ? path.namedChild(0) : null;
|
|
317
|
+
if (qualified?.type === "qualified_type")
|
|
318
|
+
edge.evidence = [`qualified:${baseTypeName(field(qualified, "alias"))}`];
|
|
319
|
+
}
|
|
320
|
+
else if (fn.type === "identifier") {
|
|
321
|
+
edge.calleeName = fn.text;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const inner = fn.type === "parenthesized_expression" ? fn.namedChild(0) : null;
|
|
325
|
+
edge.calleeName =
|
|
326
|
+
inner?.type === "field_expression"
|
|
327
|
+
? text(field(inner, "field"))
|
|
328
|
+
: inner?.type === "identifier"
|
|
329
|
+
? inner.text
|
|
330
|
+
: "<expr>";
|
|
331
|
+
edge.evidence = ["no-type:callee-expression"];
|
|
332
|
+
}
|
|
333
|
+
return edge;
|
|
334
|
+
}
|
|
335
|
+
// Macro arguments (spec §36: never expanded). A macro's token tree is re-parsed, best effort, as the body of a
|
|
336
|
+
// dummy fn so ordinary expressions inside it (`tokio::select!` branches, `stream!` bodies) yield call nodes.
|
|
337
|
+
// DENYLIST, for precision: format / print / write / assert / panic / vec style macros and logging macros (a
|
|
338
|
+
// `log::` or `tracing::` path, or the bare log-level names) are never looked into; their arguments are
|
|
339
|
+
// format strings and values, not control flow a developer navigates. anyhow!/bail! are the same
|
|
340
|
+
// format-string-plus-values shape (error construction), so they join this group. matches! is denylisted for
|
|
341
|
+
// a different, sharper reason: its second argument is a PATTERN, not an expression — a tuple-variant
|
|
342
|
+
// pattern like `Opt::Foo(_)` re-parses as a call_expression to `Foo`, a confirmed false positive, not a
|
|
343
|
+
// style call. ensure!/dbg! stay off the denylist: ensure!'s first argument is a genuine boolean condition
|
|
344
|
+
// (real control flow) and dbg!'s sole argument is the real expression being inspected — both verified to
|
|
345
|
+
// recover only real, correctly-named calls, with no pattern-confusion risk.
|
|
346
|
+
const OPAQUE_MACROS = new Set([
|
|
347
|
+
"println", "print", "eprintln", "eprint", "format", "format_args", "write", "writeln", "vec", "panic",
|
|
348
|
+
"unreachable", "todo", "unimplemented", "trace", "debug", "info", "warn", "error", "log", "event", "span",
|
|
349
|
+
"anyhow", "bail", "matches",
|
|
350
|
+
]);
|
|
351
|
+
const OPAQUE_ROOTS = new Set(["log", "tracing", "std", "core", "alloc"]);
|
|
352
|
+
const MACRO_PREFIX = "fn __m() {";
|
|
353
|
+
/** Re-parsed argument tree of a macro invocation, or undefined for denylisted / argument-less macros. */
|
|
354
|
+
export function macroArgs(mac) {
|
|
355
|
+
const m = field(mac, "macro");
|
|
356
|
+
if (!m)
|
|
357
|
+
return undefined;
|
|
358
|
+
const name = m.type === "scoped_identifier" ? text(field(m, "name")) : m.text;
|
|
359
|
+
if (m.type === "scoped_identifier" && OPAQUE_ROOTS.has(m.text.split("::")[0].trim()))
|
|
360
|
+
return undefined;
|
|
361
|
+
if (OPAQUE_MACROS.has(name) || /^(debug_)?assert/.test(name))
|
|
362
|
+
return undefined;
|
|
363
|
+
const tt = mac.namedChildren.find((c) => c.type === "token_tree");
|
|
364
|
+
if (!tt || tt.text.length < 3)
|
|
365
|
+
return undefined;
|
|
366
|
+
const src = `${MACRO_PREFIX}${tt.text.slice(1, -1)}}`;
|
|
367
|
+
try {
|
|
368
|
+
return { root: rustParser().parse((i) => src.slice(i, i + 4_096)).rootNode, tt, macro: name };
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/** Index / position in the token tree's own tree of a node of the re-parsed argument tree. */
|
|
375
|
+
export const macroIndex = (a, i) => a.tt.startIndex + 1 + i - MACRO_PREFIX.length;
|
|
376
|
+
const macroPos = (a, p) => p.row === 0
|
|
377
|
+
? { row: a.tt.startPosition.row, column: a.tt.startPosition.column + 1 + p.column - MACRO_PREFIX.length }
|
|
378
|
+
: { row: a.tt.startPosition.row + p.row, column: p.column };
|
|
379
|
+
/**
|
|
380
|
+
* Calls inside a macro's arguments, recursively through nested non-denylisted macros. `pos` maps a position
|
|
381
|
+
* of `a`'s own tree to the file. Each edge carries `macro:arg <macro>` (resolution caps it at `probable`).
|
|
382
|
+
*/
|
|
383
|
+
function macroCalls(a, pos, filePath, callerId, out) {
|
|
384
|
+
const here = (p) => pos(macroPos(a, p));
|
|
385
|
+
const walk = (n) => {
|
|
386
|
+
for (const c of n.namedChildren) {
|
|
387
|
+
if (c.type === "call_expression") {
|
|
388
|
+
const edge = callEdge(c, filePath, callerId);
|
|
389
|
+
const s = here(c.startPosition);
|
|
390
|
+
const e = here(c.endPosition);
|
|
391
|
+
edge.range = { startLine: s.row + 1, startColumn: s.column, endLine: e.row + 1, endColumn: e.column };
|
|
392
|
+
edge.evidence = [...edge.evidence, `macro:arg ${a.macro}`];
|
|
393
|
+
out.push(edge);
|
|
394
|
+
}
|
|
395
|
+
else if (c.type === "macro_invocation") {
|
|
396
|
+
const inner = macroArgs(c);
|
|
397
|
+
if (inner)
|
|
398
|
+
macroCalls(inner, here, filePath, callerId, out);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
walk(c);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
walk(a.root);
|
|
405
|
+
}
|
|
406
|
+
export function parseRust(filePath, source) {
|
|
407
|
+
const symbols = [];
|
|
408
|
+
const seenIds = new Set();
|
|
409
|
+
let parseError = false;
|
|
410
|
+
let tree;
|
|
411
|
+
try {
|
|
412
|
+
// The node binding rejects string inputs of ~32KB or more, so feed it in small chunks.
|
|
413
|
+
tree = rustParser().parse((index) => source.slice(index, index + 4_096));
|
|
414
|
+
parseError = tree.rootNode.hasError;
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return { symbols: [], calls: [], imports: [], exports: [], parseError: true };
|
|
418
|
+
}
|
|
419
|
+
const modulePath = modulePathFor(filePath);
|
|
420
|
+
const fnByNode = new Map();
|
|
421
|
+
function walk(node, parent, chain) {
|
|
422
|
+
// `#[...]` attributes are siblings that precede their item: collect them as annotations.
|
|
423
|
+
let attributes = [];
|
|
424
|
+
for (const child of node.namedChildren) {
|
|
425
|
+
if (child.type === "attribute_item") {
|
|
426
|
+
attributes.push(child.text);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (!ITEM_TYPES.has(child.type)) {
|
|
430
|
+
if (!child.type.endsWith("comment"))
|
|
431
|
+
attributes = [];
|
|
432
|
+
// Not a top-level item itself, but it may contain one (e.g. a source_file
|
|
433
|
+
// wraps everything; an impl/trait/mod body wraps its members directly, so
|
|
434
|
+
// this branch mainly matters for source_file's implicit top level).
|
|
435
|
+
walk(child, parent, chain);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const name = itemName(child);
|
|
439
|
+
if (!name)
|
|
440
|
+
continue;
|
|
441
|
+
const kind = KIND_BY_NODE_TYPE[child.type];
|
|
442
|
+
const canonicalIdentity = canonicalId(filePath, chain, kind, name);
|
|
443
|
+
// Two items at the same nesting level can share a name (e.g. multiple
|
|
444
|
+
// `impl Foo` blocks, or #[cfg(...)]-gated fn overloads), which would
|
|
445
|
+
// otherwise collide on `id` — a SQLite primary key — and crash the
|
|
446
|
+
// whole project's indexing. Disambiguate with the node's own start
|
|
447
|
+
// line: deterministic and human-readable, unlike an incrementing
|
|
448
|
+
// counter that could shift under fixture reordering.
|
|
449
|
+
let id = canonicalIdentity;
|
|
450
|
+
if (seenIds.has(id))
|
|
451
|
+
id = `${canonicalIdentity}#${child.startPosition.row + 1}`;
|
|
452
|
+
seenIds.add(id);
|
|
453
|
+
const modifiers = isPub(child) ? ["pub"] : [];
|
|
454
|
+
const symbol = {
|
|
455
|
+
id,
|
|
456
|
+
language: LANGUAGE_ID,
|
|
457
|
+
kind,
|
|
458
|
+
name,
|
|
459
|
+
qualifiedName: [...modulePath, ...chain, name].join("::"),
|
|
460
|
+
canonicalIdentity,
|
|
461
|
+
signature: child.text.split("\n")[0].trim(),
|
|
462
|
+
filePath,
|
|
463
|
+
range: range(child),
|
|
464
|
+
bodyRange: field(child, "body") ? range(field(child, "body")) : undefined,
|
|
465
|
+
parentId: parent?.id,
|
|
466
|
+
annotations: attributes,
|
|
467
|
+
modifiers,
|
|
468
|
+
metadata: symbolMetadata(child),
|
|
469
|
+
source: child.text,
|
|
470
|
+
body: field(child, "body")?.text,
|
|
471
|
+
};
|
|
472
|
+
attributes = [];
|
|
473
|
+
symbols.push(symbol);
|
|
474
|
+
if (child.type === "function_item")
|
|
475
|
+
fnByNode.set(child.id, symbol);
|
|
476
|
+
const body = field(child, "body");
|
|
477
|
+
if (body)
|
|
478
|
+
walk(body, symbol, [...chain, name]);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
walk(tree.rootNode, undefined, []);
|
|
482
|
+
const calls = [];
|
|
483
|
+
// Caller = innermost enclosing fn (closures/async blocks belong to it); token trees are only re-parsed via macroArgs.
|
|
484
|
+
const collectCalls = (node, caller) => {
|
|
485
|
+
for (const child of node.namedChildren) {
|
|
486
|
+
if (child.type === "token_tree")
|
|
487
|
+
continue;
|
|
488
|
+
const owner = fnByNode.get(child.id) ?? caller;
|
|
489
|
+
if (owner && (child.type === "call_expression" || child.type === "macro_invocation"))
|
|
490
|
+
calls.push(callEdge(child, filePath, owner.id));
|
|
491
|
+
if (owner && child.type === "macro_invocation") {
|
|
492
|
+
const args = macroArgs(child);
|
|
493
|
+
if (args)
|
|
494
|
+
macroCalls(args, (p) => p, filePath, owner.id, calls);
|
|
495
|
+
}
|
|
496
|
+
collectCalls(child, owner);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
collectCalls(tree.rootNode, undefined);
|
|
500
|
+
const imports = [];
|
|
501
|
+
const exports = [];
|
|
502
|
+
const collectUses = (node) => {
|
|
503
|
+
if (node.type === "use_declaration") {
|
|
504
|
+
const records = parseUseDeclaration(node, filePath);
|
|
505
|
+
imports.push(...records);
|
|
506
|
+
if (isPub(node)) {
|
|
507
|
+
for (const record of records)
|
|
508
|
+
exports.push({
|
|
509
|
+
filePath,
|
|
510
|
+
language: LANGUAGE_ID,
|
|
511
|
+
exportedName: record.localName ?? record.importedName ?? "",
|
|
512
|
+
fromModule: record.module,
|
|
513
|
+
sourceName: record.importedName,
|
|
514
|
+
wildcard: record.wildcard,
|
|
515
|
+
typeOnly: false,
|
|
516
|
+
range: record.range,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return; // a use_declaration has no nested items worth descending into
|
|
520
|
+
}
|
|
521
|
+
for (const child of node.namedChildren)
|
|
522
|
+
collectUses(child);
|
|
523
|
+
};
|
|
524
|
+
collectUses(tree.rootNode);
|
|
525
|
+
const topLevelUses = tree.rootNode.namedChildren.filter((child) => child.type === "use_declaration");
|
|
526
|
+
if (topLevelUses.length) {
|
|
527
|
+
const moduleId = `${filePath}::module::${filePath}`;
|
|
528
|
+
symbols.push({
|
|
529
|
+
id: moduleId,
|
|
530
|
+
language: LANGUAGE_ID,
|
|
531
|
+
kind: "namespace",
|
|
532
|
+
name: filePath,
|
|
533
|
+
qualifiedName: filePath,
|
|
534
|
+
canonicalIdentity: moduleId,
|
|
535
|
+
signature: `module ${filePath}`,
|
|
536
|
+
filePath,
|
|
537
|
+
range: range(tree.rootNode),
|
|
538
|
+
annotations: [],
|
|
539
|
+
modifiers: [],
|
|
540
|
+
metadata: { moduleScope: true },
|
|
541
|
+
source: topLevelUses.map((node) => node.text).join("\n"),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return { symbols, calls, imports, exports, parseError };
|
|
545
|
+
}
|