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,339 @@
|
|
|
1
|
+
import Parser from "tree-sitter";
|
|
2
|
+
import Java from "tree-sitter-java";
|
|
3
|
+
// Java grammar node type -> SymbolKind, matching the old regex parser's exact kind values.
|
|
4
|
+
const TYPE_DECL_NODE_TYPES = {
|
|
5
|
+
class_declaration: "class",
|
|
6
|
+
interface_declaration: "interface",
|
|
7
|
+
enum_declaration: "enum",
|
|
8
|
+
record_declaration: "record",
|
|
9
|
+
};
|
|
10
|
+
function point(source, offset) {
|
|
11
|
+
const lines = source.slice(0, offset).split("\n");
|
|
12
|
+
return { line: lines.length, column: lines.at(-1)?.length ?? 0 };
|
|
13
|
+
}
|
|
14
|
+
function sourceRange(source, start, end) {
|
|
15
|
+
const a = point(source, start);
|
|
16
|
+
const b = point(source, end);
|
|
17
|
+
return {
|
|
18
|
+
startLine: a.line,
|
|
19
|
+
startColumn: a.column,
|
|
20
|
+
endLine: b.line,
|
|
21
|
+
endColumn: b.column,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function canonicalId(filePath, packageName, typeChain, kind, name, parameters = "") {
|
|
25
|
+
return `${filePath}::${packageName || "<default>"}::${typeChain.join(".") || "<file>"}::${kind}::${name}${parameters ? `(${parameters})` : ""}`;
|
|
26
|
+
}
|
|
27
|
+
function parameterSignature(parameters) {
|
|
28
|
+
return parameters
|
|
29
|
+
.replace(/@[A-Za-z_$][\w$]*(?:\([^)]*\))?\s*/g, "")
|
|
30
|
+
.replace(/\b(final|volatile|transient)\s+/g, "")
|
|
31
|
+
.replace(/\s+/g, " ")
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
function callArguments(source, open) {
|
|
35
|
+
let depth = 0;
|
|
36
|
+
for (let i = open; i < source.length; i++) {
|
|
37
|
+
if (source[i] === "(")
|
|
38
|
+
depth++;
|
|
39
|
+
else if (source[i] === ")" && --depth === 0)
|
|
40
|
+
return source.slice(open + 1, i);
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
/** Annotation names + modifier keywords out of a `modifiers` node, in source order (bare annotation names only, no arguments). */
|
|
45
|
+
function modifiersNodeParts(modifiersNode) {
|
|
46
|
+
const annotations = [];
|
|
47
|
+
const modifiers = [];
|
|
48
|
+
for (const child of modifiersNode?.children ?? []) {
|
|
49
|
+
if (child.type === "marker_annotation" || child.type === "annotation") {
|
|
50
|
+
annotations.push(`@${child.childForFieldName("name").text}`);
|
|
51
|
+
}
|
|
52
|
+
else if (!child.isNamed) {
|
|
53
|
+
modifiers.push(child.type);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { annotations, modifiers };
|
|
57
|
+
}
|
|
58
|
+
/** Strip generic type arguments (e.g. `JpaRepository<User, Long>` -> `JpaRepository`), matching the old regex parser's bare-identifier-only behavior. */
|
|
59
|
+
function stripGenericArgs(name) {
|
|
60
|
+
const idx = name.indexOf("<");
|
|
61
|
+
return (idx === -1 ? name : name.slice(0, idx)).trim();
|
|
62
|
+
}
|
|
63
|
+
/** class/interface supertypes: class uses `superclass`/`interfaces` fields, interface uses a direct `extends_interfaces` child (no matching field). */
|
|
64
|
+
function supertypesOf(node) {
|
|
65
|
+
if (node.type === "class_declaration") {
|
|
66
|
+
const superclass = node.childForFieldName("superclass");
|
|
67
|
+
const interfaces = node.childForFieldName("interfaces");
|
|
68
|
+
return [
|
|
69
|
+
...(superclass
|
|
70
|
+
? [stripGenericArgs(superclass.text.replace(/^extends\s+/, ""))]
|
|
71
|
+
: []),
|
|
72
|
+
...(interfaces
|
|
73
|
+
? interfaces
|
|
74
|
+
.text.replace(/^implements\s+/, "")
|
|
75
|
+
.split(/\s*,\s*/)
|
|
76
|
+
.map(stripGenericArgs)
|
|
77
|
+
: []),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
if (node.type === "interface_declaration") {
|
|
81
|
+
const extendsNode = node.children.find((c) => c.type === "extends_interfaces");
|
|
82
|
+
const typeList = extendsNode?.namedChildren.find((c) => c.type === "type_list");
|
|
83
|
+
return typeList?.namedChildren.map((t) => stripGenericArgs(t.text)) ?? [];
|
|
84
|
+
}
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The nodes to scan for direct method/constructor members of a type's body. For class/interface/record
|
|
89
|
+
* bodies these are the body node's own namedChildren. For an enum, the grammar nests them one level
|
|
90
|
+
* deeper: `enum_body`'s real members (methods, constructors, fields declared after the constant list)
|
|
91
|
+
* live inside a child `enum_body_declarations` node, not as direct children of `enum_body` itself
|
|
92
|
+
* (an enum with no such members after its constants has no `enum_body_declarations` child at all).
|
|
93
|
+
*/
|
|
94
|
+
function memberNodesOf(bodyNode) {
|
|
95
|
+
if (bodyNode.type === "enum_body") {
|
|
96
|
+
const decls = bodyNode.namedChildren.find((c) => c.type === "enum_body_declarations");
|
|
97
|
+
return decls ? decls.namedChildren : [];
|
|
98
|
+
}
|
|
99
|
+
return bodyNode.namedChildren;
|
|
100
|
+
}
|
|
101
|
+
/** Recursive walk over top-level/nested type declarations (class/interface/enum/record), mirroring src/languages/rust/parse.ts's walk(node, parent, chain) shape. */
|
|
102
|
+
function walkTypes(node, parent, chain, filePath, source, packageName, symbols, types) {
|
|
103
|
+
for (const child of node.namedChildren) {
|
|
104
|
+
const kind = TYPE_DECL_NODE_TYPES[child.type];
|
|
105
|
+
if (!kind) {
|
|
106
|
+
walkTypes(child, parent, chain, filePath, source, packageName, symbols, types);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const name = child.childForFieldName("name").text;
|
|
110
|
+
const bodyNode = child.childForFieldName("body");
|
|
111
|
+
const typeChain = [...chain, name];
|
|
112
|
+
const canonicalIdentity = canonicalId(filePath, packageName, typeChain, kind, name);
|
|
113
|
+
const modifiersNode = child.namedChildren.find((c) => c.type === "modifiers");
|
|
114
|
+
const { annotations, modifiers } = modifiersNodeParts(modifiersNode);
|
|
115
|
+
const symbol = {
|
|
116
|
+
id: canonicalIdentity,
|
|
117
|
+
language: "java",
|
|
118
|
+
kind,
|
|
119
|
+
name,
|
|
120
|
+
packageName,
|
|
121
|
+
qualifiedName: `${packageName ? `${packageName}.` : ""}${typeChain.join(".")}`,
|
|
122
|
+
canonicalIdentity,
|
|
123
|
+
signature: `${kind} ${name}`,
|
|
124
|
+
filePath,
|
|
125
|
+
range: sourceRange(source, child.startIndex, child.endIndex),
|
|
126
|
+
bodyRange: bodyNode
|
|
127
|
+
? sourceRange(source, bodyNode.startIndex, bodyNode.endIndex)
|
|
128
|
+
: undefined,
|
|
129
|
+
parentId: parent?.id,
|
|
130
|
+
supertypes: supertypesOf(child),
|
|
131
|
+
annotations,
|
|
132
|
+
modifiers,
|
|
133
|
+
source: source.slice(child.startIndex, child.endIndex),
|
|
134
|
+
body: bodyNode
|
|
135
|
+
? source.slice(bodyNode.startIndex, bodyNode.endIndex)
|
|
136
|
+
: undefined,
|
|
137
|
+
};
|
|
138
|
+
symbols.push(symbol);
|
|
139
|
+
types.push(symbol);
|
|
140
|
+
if (bodyNode) {
|
|
141
|
+
for (const member of memberNodesOf(bodyNode)) {
|
|
142
|
+
if (member.type === "method_declaration") {
|
|
143
|
+
symbols.push(methodSymbol(member, symbol, typeChain, filePath, source, packageName));
|
|
144
|
+
}
|
|
145
|
+
else if (member.type === "constructor_declaration") {
|
|
146
|
+
symbols.push(constructorSymbol(member, symbol, typeChain, filePath, source, packageName));
|
|
147
|
+
}
|
|
148
|
+
else if (member.type === "field_declaration" || member.type === "constant_declaration") {
|
|
149
|
+
symbols.push(...fieldSymbols(member, symbol, typeChain, filePath, source, packageName));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
walkTypes(bodyNode, symbol, typeChain, filePath, source, packageName, symbols, types);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** `formal_parameters` node text includes its own parens; parameterSignature() expects the bare inner text (matching the old regex's capture group, which never included the parens). */
|
|
157
|
+
function parametersText(parametersNode) {
|
|
158
|
+
const text = parametersNode.text;
|
|
159
|
+
return text.startsWith("(") && text.endsWith(")")
|
|
160
|
+
? text.slice(1, -1)
|
|
161
|
+
: text;
|
|
162
|
+
}
|
|
163
|
+
function methodSymbol(node, parent, typeChain, filePath, source, packageName) {
|
|
164
|
+
const name = node.childForFieldName("name").text;
|
|
165
|
+
const type = node.childForFieldName("type").text;
|
|
166
|
+
const parametersNode = node.childForFieldName("parameters");
|
|
167
|
+
const bodyNode = node.childForFieldName("body");
|
|
168
|
+
const modifiersNode = node.namedChildren.find((c) => c.type === "modifiers");
|
|
169
|
+
const { annotations, modifiers } = modifiersNodeParts(modifiersNode);
|
|
170
|
+
const parameters = parameterSignature(parametersText(parametersNode));
|
|
171
|
+
const signature = `${name}(${parameters}): ${type}`;
|
|
172
|
+
const canonicalIdentity = canonicalId(filePath, packageName, typeChain, "method", name, parameters);
|
|
173
|
+
return {
|
|
174
|
+
id: canonicalIdentity,
|
|
175
|
+
language: "java",
|
|
176
|
+
kind: "method",
|
|
177
|
+
name,
|
|
178
|
+
packageName,
|
|
179
|
+
qualifiedName: `${packageName ? `${packageName}.` : ""}${[...typeChain, name].join(".")}`,
|
|
180
|
+
canonicalIdentity,
|
|
181
|
+
signature,
|
|
182
|
+
filePath,
|
|
183
|
+
range: sourceRange(source, node.startIndex, node.endIndex),
|
|
184
|
+
bodyRange: bodyNode
|
|
185
|
+
? sourceRange(source, bodyNode.startIndex, bodyNode.endIndex)
|
|
186
|
+
: undefined,
|
|
187
|
+
parentId: parent.id,
|
|
188
|
+
annotations,
|
|
189
|
+
modifiers,
|
|
190
|
+
source: source.slice(node.startIndex, node.endIndex),
|
|
191
|
+
body: bodyNode ? source.slice(bodyNode.startIndex, bodyNode.endIndex) : undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function constructorSymbol(node, parent, typeChain, filePath, source, packageName) {
|
|
195
|
+
const name = node.childForFieldName("name").text;
|
|
196
|
+
const parametersNode = node.childForFieldName("parameters");
|
|
197
|
+
const bodyNode = node.childForFieldName("body");
|
|
198
|
+
const modifiersNode = node.namedChildren.find((c) => c.type === "modifiers");
|
|
199
|
+
const { annotations, modifiers } = modifiersNodeParts(modifiersNode);
|
|
200
|
+
const parameters = parameterSignature(parametersText(parametersNode));
|
|
201
|
+
const signature = `${name}(${parameters}): ${name}`;
|
|
202
|
+
const canonicalIdentity = canonicalId(filePath, packageName, typeChain, "constructor", name, parameters);
|
|
203
|
+
return {
|
|
204
|
+
id: canonicalIdentity,
|
|
205
|
+
language: "java",
|
|
206
|
+
kind: "constructor",
|
|
207
|
+
name,
|
|
208
|
+
packageName,
|
|
209
|
+
qualifiedName: `${packageName ? `${packageName}.` : ""}${[...typeChain, name].join(".")}`,
|
|
210
|
+
canonicalIdentity,
|
|
211
|
+
signature,
|
|
212
|
+
filePath,
|
|
213
|
+
range: sourceRange(source, node.startIndex, node.endIndex),
|
|
214
|
+
bodyRange: sourceRange(source, bodyNode.startIndex, bodyNode.endIndex),
|
|
215
|
+
parentId: parent.id,
|
|
216
|
+
annotations,
|
|
217
|
+
modifiers,
|
|
218
|
+
source: source.slice(node.startIndex, node.endIndex),
|
|
219
|
+
body: source.slice(bodyNode.startIndex, bodyNode.endIndex),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A field_declaration (class/enum/record) or constant_declaration (interface — a
|
|
224
|
+
* different node type with an identical internal shape) can carry multiple
|
|
225
|
+
* variable_declarator children (`private int a, b;`), so this returns one SymbolRecord
|
|
226
|
+
* per declarator, all sharing the same declared type/annotations/modifiers. A field has
|
|
227
|
+
* no top-level `type` property on SymbolRecord (only methods put a type-like thing in
|
|
228
|
+
* `.signature`); the declared type goes in `metadata.declaredType`, the same convention
|
|
229
|
+
* the TypeScript and Python adapters already use.
|
|
230
|
+
*/
|
|
231
|
+
function fieldSymbols(node, parent, typeChain, filePath, source, packageName) {
|
|
232
|
+
const declaredType = node.childForFieldName("type").text;
|
|
233
|
+
const modifiersNode = node.namedChildren.find((c) => c.type === "modifiers");
|
|
234
|
+
const { annotations, modifiers } = modifiersNodeParts(modifiersNode);
|
|
235
|
+
const declarators = node.namedChildren.filter((c) => c.type === "variable_declarator");
|
|
236
|
+
const single = declarators.length === 1;
|
|
237
|
+
return declarators.map((declarator) => {
|
|
238
|
+
const name = declarator.childForFieldName("name").text;
|
|
239
|
+
const start = single ? node.startIndex : declarator.startIndex;
|
|
240
|
+
const end = single ? node.endIndex : declarator.endIndex;
|
|
241
|
+
const canonicalIdentity = canonicalId(filePath, packageName, typeChain, "field", name);
|
|
242
|
+
return {
|
|
243
|
+
id: canonicalIdentity,
|
|
244
|
+
language: "java",
|
|
245
|
+
kind: "field",
|
|
246
|
+
name,
|
|
247
|
+
packageName,
|
|
248
|
+
qualifiedName: `${packageName ? `${packageName}.` : ""}${[...typeChain, name].join(".")}`,
|
|
249
|
+
canonicalIdentity,
|
|
250
|
+
signature: `${name}: ${declaredType}`,
|
|
251
|
+
filePath,
|
|
252
|
+
range: sourceRange(source, start, end),
|
|
253
|
+
bodyRange: undefined,
|
|
254
|
+
parentId: parent.id,
|
|
255
|
+
annotations,
|
|
256
|
+
modifiers,
|
|
257
|
+
metadata: { declaredType },
|
|
258
|
+
source: source.slice(start, end),
|
|
259
|
+
body: undefined,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function byRange(a, b) {
|
|
264
|
+
return (a.range.startLine - b.range.startLine ||
|
|
265
|
+
a.range.startColumn - b.range.startColumn);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Mutates each symbol's `id`, appending `#N` for the Nth+ symbol sharing a `canonicalIdentity`
|
|
269
|
+
* within this file. The old regex parser built `symbols` in a fixed global order — all types
|
|
270
|
+
* (via the type walk), then ALL methods across the whole file in document order (one regex pass),
|
|
271
|
+
* then ALL constructors across the whole file in document order (another pass) — and dedup
|
|
272
|
+
* suffixes were assigned by iterating that order. The new AST walk instead pushes each type's
|
|
273
|
+
* own methods/constructors immediately after the type itself (per-type nested order), which can
|
|
274
|
+
* differ from the old flat order when the file has multiple types. To keep dedup suffixes
|
|
275
|
+
* byte-identical to the old parser regardless of that ordering change, this recomputes the
|
|
276
|
+
* old global order (types, then methods sorted by position, then constructors sorted by
|
|
277
|
+
* position) purely for suffix assignment; the returned `symbols` array itself keeps its
|
|
278
|
+
* (unrelated) per-type nested order. Fields (a new symbol kind with no old-parser precedent to match) are included in this same ordering, inserted after types and before methods — the exact position has no behavioral significance since there is no legacy ordering to preserve for fields, but must stay consistent.
|
|
279
|
+
*/
|
|
280
|
+
function assignDedupIds(symbols, types) {
|
|
281
|
+
const fields = symbols.filter((s) => s.kind === "field").sort(byRange);
|
|
282
|
+
const methods = symbols.filter((s) => s.kind === "method").sort(byRange);
|
|
283
|
+
const constructors = symbols
|
|
284
|
+
.filter((s) => s.kind === "constructor")
|
|
285
|
+
.sort(byRange);
|
|
286
|
+
const dedupOrder = [...types, ...fields, ...methods, ...constructors];
|
|
287
|
+
const identityCounts = new Map();
|
|
288
|
+
for (const symbol of dedupOrder) {
|
|
289
|
+
const count = identityCounts.get(symbol.canonicalIdentity ?? symbol.id) ?? 0;
|
|
290
|
+
if (count > 0)
|
|
291
|
+
symbol.id = `${symbol.canonicalIdentity}#${count + 1}`;
|
|
292
|
+
identityCounts.set(symbol.canonicalIdentity ?? symbol.id, count + 1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
export function parseJava(filePath, source) {
|
|
296
|
+
const parser = new Parser();
|
|
297
|
+
parser.setLanguage(Java);
|
|
298
|
+
let parseError = false;
|
|
299
|
+
let tree;
|
|
300
|
+
try {
|
|
301
|
+
tree = parser.parse(source);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
parseError = true;
|
|
305
|
+
}
|
|
306
|
+
const symbols = [];
|
|
307
|
+
const types = [];
|
|
308
|
+
const packageDecl = tree?.rootNode.namedChildren.find((c) => c.type === "package_declaration");
|
|
309
|
+
const packageName = packageDecl?.namedChildren[0]?.text ?? "";
|
|
310
|
+
if (tree)
|
|
311
|
+
walkTypes(tree.rootNode, undefined, [], filePath, source, packageName, symbols, types);
|
|
312
|
+
assignDedupIds(symbols, types);
|
|
313
|
+
const calls = [];
|
|
314
|
+
const callable = symbols.filter((s) => s.kind === "method" || s.kind === "constructor");
|
|
315
|
+
for (const caller of callable)
|
|
316
|
+
for (const match of (caller.body ?? "").matchAll(/(?:(\w+)\.)?([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
317
|
+
const calleeName = match[2];
|
|
318
|
+
const offset = match.index ?? 0;
|
|
319
|
+
const prefix = (caller.body ?? "").slice(Math.max(0, offset - 8), offset);
|
|
320
|
+
if (["if", "for", "while", "switch", "catch", "return"].includes(calleeName))
|
|
321
|
+
continue;
|
|
322
|
+
const argumentsText = callArguments(caller.body ?? "", offset + match[0].lastIndexOf("("));
|
|
323
|
+
const argumentCount = argumentsText.trim()
|
|
324
|
+
? argumentsText.split(",").length
|
|
325
|
+
: 0;
|
|
326
|
+
calls.push({
|
|
327
|
+
callerId: caller.id,
|
|
328
|
+
receiverText: match[1],
|
|
329
|
+
calleeName,
|
|
330
|
+
argumentCount,
|
|
331
|
+
filePath,
|
|
332
|
+
range: caller.range,
|
|
333
|
+
confidence: "unresolved",
|
|
334
|
+
resolutionKind: prefix.includes("new") ? "constructor" : "unresolved",
|
|
335
|
+
evidence: prefix.includes("new") ? ["syntactic new expression"] : [],
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return { symbols, calls, parseError };
|
|
339
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const estimateTokens = (text) => Math.ceil(text.length / 4);
|