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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -0
  3. package/dist/src/cli.js +199 -0
  4. package/dist/src/indexer/index.js +325 -0
  5. package/dist/src/languages/adapter.js +23 -0
  6. package/dist/src/languages/go/index.js +15 -0
  7. package/dist/src/languages/go/parse.js +443 -0
  8. package/dist/src/languages/go/resolve.js +313 -0
  9. package/dist/src/languages/java/enterprise/dependency-injection.js +175 -0
  10. package/dist/src/languages/java/enterprise/jpa-entity.js +89 -0
  11. package/dist/src/languages/java/enterprise/registry.js +22 -0
  12. package/dist/src/languages/java/enterprise/spring-data.js +239 -0
  13. package/dist/src/languages/java/enterprise/spring-mvc.js +183 -0
  14. package/dist/src/languages/java/enterprise/transactions.js +110 -0
  15. package/dist/src/languages/java.js +84 -0
  16. package/dist/src/languages/javascript/index.js +25 -0
  17. package/dist/src/languages/python/index.js +29 -0
  18. package/dist/src/languages/python/parse.js +415 -0
  19. package/dist/src/languages/python/resolve.js +413 -0
  20. package/dist/src/languages/rust/calls-resolve.js +1405 -0
  21. package/dist/src/languages/rust/index.js +12 -0
  22. package/dist/src/languages/rust/parse.js +545 -0
  23. package/dist/src/languages/rust/resolve.js +284 -0
  24. package/dist/src/languages/typescript/index.js +25 -0
  25. package/dist/src/languages/typescript/parse.js +793 -0
  26. package/dist/src/languages/typescript/resolve.js +463 -0
  27. package/dist/src/package-info.js +16 -0
  28. package/dist/src/parser/java-parser.js +339 -0
  29. package/dist/src/planner/budget.js +1 -0
  30. package/dist/src/planner/composition.js +372 -0
  31. package/dist/src/planner/rank.js +10 -0
  32. package/dist/src/render/compact-context.js +11 -0
  33. package/dist/src/server/mcp-server.js +154 -0
  34. package/dist/src/storage/sqlite.js +105 -0
  35. package/dist/src/types/enterprise.js +1 -0
  36. package/dist/src/types/model.js +1 -0
  37. package/dist/src/workflow/errors.js +10 -0
  38. package/dist/src/workflow/preview.js +220 -0
  39. package/dist/src/workflow/repository.js +56 -0
  40. package/mcp.json +10 -0
  41. package/package.json +76 -0
  42. package/plugin.json +15 -0
  43. package/queries/java/annotations.scm +1 -0
  44. package/queries/java/calls.scm +1 -0
  45. package/queries/java/imports.scm +1 -0
  46. package/queries/java/symbols.scm +2 -0
  47. package/skills/context-slice/SKILL.md +60 -0
@@ -0,0 +1,793 @@
1
+ import Parser from "tree-sitter";
2
+ import TypeScript from "tree-sitter-typescript";
3
+ export const LANGUAGE_ID = "typescript";
4
+ export const JAVASCRIPT_LANGUAGE_ID = "javascript";
5
+ // tree-sitter-typescript's "typescript" grammar can't parse JSX (ambiguous with type
6
+ // assertions), so a .jsx/.js-with-jsx file needs the "tsx" grammar regardless of language id.
7
+ const jsxDialect = (filePath) => [".tsx", ".jsx"].some((ext) => filePath.toLowerCase().endsWith(ext));
8
+ const isJavaScriptFile = (filePath) => [".jsx", ".mjs", ".cjs", ".js"].some((ext) => filePath.toLowerCase().endsWith(ext));
9
+ export const isDeclarationFile = (filePath) => filePath.toLowerCase().endsWith(".d.ts");
10
+ const parsers = new Map();
11
+ function parserFor(filePath) {
12
+ const dialect = jsxDialect(filePath) ? "tsx" : "typescript";
13
+ let parser = parsers.get(dialect);
14
+ if (!parser) {
15
+ parser = new Parser();
16
+ parser.setLanguage((dialect === "tsx"
17
+ ? TypeScript.tsx
18
+ : TypeScript.typescript));
19
+ parsers.set(dialect, parser);
20
+ }
21
+ return parser;
22
+ }
23
+ const range = (node) => ({
24
+ startLine: node.startPosition.row + 1,
25
+ startColumn: node.startPosition.column,
26
+ endLine: node.endPosition.row + 1,
27
+ endColumn: node.endPosition.column,
28
+ });
29
+ const field = (node, name) => node.childForFieldName(name);
30
+ const text = (node) => node?.text ?? "";
31
+ const isCallable = (node) => node?.type === "arrow_function" ||
32
+ node?.type === "function_expression" ||
33
+ node?.type === "function";
34
+ /** `src/order.ts::OrderService::method::create(OrderInput)` — stable across edits. */
35
+ function canonicalId(filePath, chain, kind, name, parameters) {
36
+ return [
37
+ filePath,
38
+ ...chain,
39
+ kind,
40
+ parameters === undefined ? name : `${name}(${parameters})`,
41
+ ].join("::");
42
+ }
43
+ function parameterSignature(parameters) {
44
+ if (!parameters)
45
+ return "";
46
+ return parameters.namedChildren
47
+ .map((parameter) => {
48
+ const type = field(parameter, "type");
49
+ const pattern = field(parameter, "pattern") ?? parameter.namedChild(0);
50
+ const typeText = text(type).replace(/^:\s*/, "").trim();
51
+ return typeText || text(pattern).trim();
52
+ })
53
+ .filter(Boolean)
54
+ .join(", ");
55
+ }
56
+ function returnType(node) {
57
+ const declared = field(node, "return_type");
58
+ return declared ? text(declared).replace(/^:\s*/, "").trim() : undefined;
59
+ }
60
+ const looksLikeComponent = (name, node, filePath) => jsxDialect(filePath) && /^[A-Z]/.test(name) && containsJsx(node);
61
+ function containsJsx(node) {
62
+ const stack = [node];
63
+ while (stack.length) {
64
+ const current = stack.pop();
65
+ if (current.type === "jsx_element" ||
66
+ current.type === "jsx_self_closing_element" ||
67
+ current.type === "jsx_fragment")
68
+ return true;
69
+ stack.push(...current.namedChildren);
70
+ }
71
+ return false;
72
+ }
73
+ export function parseTypeScript(filePath, source) {
74
+ const symbols = [];
75
+ const calls = [];
76
+ const imports = [];
77
+ const exports = [];
78
+ const languageId = isJavaScriptFile(filePath)
79
+ ? JAVASCRIPT_LANGUAGE_ID
80
+ : LANGUAGE_ID;
81
+ const declarationOnly = isDeclarationFile(filePath);
82
+ let parseError = false;
83
+ let tree;
84
+ try {
85
+ // The node binding rejects inputs of 32KB or more, so always feed it in small chunks.
86
+ tree = parserFor(filePath).parse((index) => source.slice(index, index + 4_096));
87
+ }
88
+ catch {
89
+ return { symbols, calls, imports, exports, parseError: true };
90
+ }
91
+ if (tree.rootNode.hasError)
92
+ parseError = true;
93
+ const scopes = [{ types: new Map() }];
94
+ const declaredTypeOf = (name) => {
95
+ for (let at = scopes.length - 1; at >= 0; at--) {
96
+ const found = scopes[at].types.get(name);
97
+ if (found)
98
+ return found;
99
+ }
100
+ return undefined;
101
+ };
102
+ const noteBinding = (name, type) => {
103
+ if (name && type)
104
+ scopes.at(-1).types.set(name, type);
105
+ };
106
+ let moduleSymbol;
107
+ /** Top-level statements need an owner so their calls are not dropped. */
108
+ const moduleOwner = () => {
109
+ if (!moduleSymbol) {
110
+ // Named after the file so it can never shadow a declared symbol.
111
+ const name = filePath;
112
+ moduleSymbol = {
113
+ id: `${filePath}::module::${name}`,
114
+ language: languageId,
115
+ kind: "namespace",
116
+ name,
117
+ qualifiedName: name,
118
+ canonicalIdentity: `${filePath}::module::${name}`,
119
+ signature: `module ${name}`,
120
+ filePath,
121
+ range: range(tree.rootNode),
122
+ annotations: [],
123
+ modifiers: [],
124
+ metadata: { moduleScope: true },
125
+ source: "",
126
+ };
127
+ symbols.push(moduleSymbol);
128
+ }
129
+ return moduleSymbol;
130
+ };
131
+ const addSymbol = (node, kind, name, chain, options = {}) => {
132
+ const id = canonicalId(filePath, chain, kind, name, options.parameters);
133
+ const body = options.bodyNode ?? undefined;
134
+ const symbol = {
135
+ id,
136
+ language: languageId,
137
+ kind,
138
+ name,
139
+ qualifiedName: [...chain, name].join("."),
140
+ canonicalIdentity: id,
141
+ signature: options.signature ?? `${kind} ${name}`,
142
+ filePath,
143
+ range: range(node),
144
+ bodyRange: body ? range(body) : undefined,
145
+ parentId: options.parentId,
146
+ supertypes: options.supertypes,
147
+ annotations: [],
148
+ modifiers: [],
149
+ metadata: {
150
+ ...options.metadata,
151
+ ...(declarationOnly ? { declarationOnly: true } : {}),
152
+ },
153
+ source: node.text,
154
+ body: body?.text,
155
+ };
156
+ symbols.push(symbol);
157
+ return symbol;
158
+ };
159
+ // ---- imports -------------------------------------------------------------
160
+ const importStatement = (node) => {
161
+ const module = text(field(node, "source")).replace(/^["'`]|["'`]$/g, "");
162
+ const clause = node.namedChildren.find((child) => child.type === "import_clause");
163
+ const typeOnly = node.text.startsWith("import type");
164
+ const base = {
165
+ filePath,
166
+ language: languageId,
167
+ module,
168
+ typeOnly,
169
+ range: range(node),
170
+ };
171
+ if (!clause) {
172
+ imports.push({ ...base, kind: "side-effect" });
173
+ return;
174
+ }
175
+ for (const child of clause.namedChildren) {
176
+ if (child.type === "identifier")
177
+ imports.push({
178
+ ...base,
179
+ kind: "default",
180
+ importedName: "default",
181
+ localName: child.text,
182
+ });
183
+ else if (child.type === "namespace_import")
184
+ imports.push({
185
+ ...base,
186
+ kind: "namespace",
187
+ localName: text(child.namedChild(0)),
188
+ });
189
+ else if (child.type === "named_imports")
190
+ for (const specifier of child.namedChildren.filter((item) => item.type === "import_specifier")) {
191
+ const name = text(field(specifier, "name"));
192
+ const alias = text(field(specifier, "alias"));
193
+ imports.push({
194
+ ...base,
195
+ kind: "named",
196
+ importedName: name,
197
+ localName: alias || name,
198
+ typeOnly: typeOnly || specifier.text.startsWith("type "),
199
+ });
200
+ }
201
+ }
202
+ };
203
+ // ---- exports -------------------------------------------------------------
204
+ const declaredNames = (declaration) => {
205
+ if (declaration.type === "lexical_declaration" ||
206
+ declaration.type === "variable_declaration")
207
+ return declaration.namedChildren
208
+ .filter((child) => child.type === "variable_declarator")
209
+ .map((child) => text(field(child, "name")))
210
+ .filter(Boolean);
211
+ const name = text(field(declaration, "name"));
212
+ return name ? [name] : [];
213
+ };
214
+ const exportStatement = (node) => {
215
+ const source = field(node, "source");
216
+ const module = source
217
+ ? text(source).replace(/^["'`]|["'`]$/g, "")
218
+ : undefined;
219
+ const typeOnly = node.text.startsWith("export type");
220
+ const base = {
221
+ filePath,
222
+ language: languageId,
223
+ typeOnly,
224
+ range: range(node),
225
+ };
226
+ const declaration = field(node, "declaration");
227
+ const isDefault = node.children.some((child) => child.type === "default");
228
+ if (declaration) {
229
+ if (isDefault) {
230
+ const name = text(field(declaration, "name")) || "default";
231
+ exports.push({
232
+ ...base,
233
+ exportedName: "default",
234
+ localName: name || undefined,
235
+ });
236
+ }
237
+ else
238
+ for (const name of declaredNames(declaration))
239
+ exports.push({ ...base, exportedName: name, localName: name });
240
+ return;
241
+ }
242
+ if (isDefault) {
243
+ // `export default handler;` or `export default () => {}`
244
+ const value = node.namedChildren.find((child) => child.type !== "export_clause");
245
+ exports.push({
246
+ ...base,
247
+ exportedName: "default",
248
+ localName: value?.type === "identifier" ? value.text : undefined,
249
+ });
250
+ return;
251
+ }
252
+ const clause = node.namedChildren.find((child) => child.type === "export_clause");
253
+ if (clause) {
254
+ for (const specifier of clause.namedChildren.filter((child) => child.type === "export_specifier")) {
255
+ const name = text(field(specifier, "name"));
256
+ const alias = text(field(specifier, "alias"));
257
+ exports.push({
258
+ ...base,
259
+ exportedName: alias || name,
260
+ localName: module ? undefined : name,
261
+ fromModule: module,
262
+ sourceName: module ? name : undefined,
263
+ });
264
+ }
265
+ return;
266
+ }
267
+ if (module) {
268
+ const namespaceAlias = node.namedChildren.find((child) => child.type === "namespace_export");
269
+ exports.push({
270
+ ...base,
271
+ exportedName: namespaceAlias ? text(namespaceAlias.namedChild(0)) : "*",
272
+ fromModule: module,
273
+ wildcard: !namespaceAlias,
274
+ });
275
+ }
276
+ };
277
+ // ---- calls ---------------------------------------------------------------
278
+ const addCall = (node, owner) => {
279
+ const callee = field(node, "function");
280
+ if (!callee)
281
+ return;
282
+ const argumentsNode = field(node, "arguments");
283
+ const argumentCount = argumentsNode
284
+ ? argumentsNode.namedChildren.length
285
+ : 0;
286
+ const shared = {
287
+ callerId: owner.id,
288
+ argumentCount,
289
+ filePath,
290
+ language: languageId,
291
+ range: range(node),
292
+ confidence: "unresolved",
293
+ evidence: [],
294
+ };
295
+ if (callee.type === "identifier") {
296
+ calls.push({
297
+ ...shared,
298
+ calleeName: callee.text,
299
+ resolutionKind: "unresolved",
300
+ });
301
+ return;
302
+ }
303
+ if (callee.type === "member_expression" ||
304
+ callee.type === "subscript_expression") {
305
+ const object = field(callee, "object");
306
+ const property = field(callee, "property");
307
+ if (!property || property.type !== "property_identifier")
308
+ return;
309
+ const thisProperty = object?.type === "member_expression" &&
310
+ field(object, "object")?.type === "this" &&
311
+ field(object, "property")?.type === "property_identifier"
312
+ ? `this.${text(field(object, "property"))}`
313
+ : undefined;
314
+ const receiverText = object?.type === "identifier" || object?.type === "this"
315
+ ? object.text
316
+ : thisProperty;
317
+ calls.push({
318
+ ...shared,
319
+ calleeName: property.text,
320
+ receiverText,
321
+ receiverType: receiverText && receiverText !== "this"
322
+ ? declaredTypeOf(receiverText)
323
+ : undefined,
324
+ optionalChaining: callee.text.includes("?."),
325
+ resolutionKind: "unresolved",
326
+ });
327
+ }
328
+ };
329
+ const addNew = (node, owner) => {
330
+ const constructor = field(node, "constructor");
331
+ if (!constructor || constructor.type !== "identifier")
332
+ return;
333
+ const argumentsNode = field(node, "arguments");
334
+ calls.push({
335
+ callerId: owner.id,
336
+ calleeName: constructor.text,
337
+ argumentCount: argumentsNode ? argumentsNode.namedChildren.length : 0,
338
+ filePath,
339
+ language: languageId,
340
+ range: range(node),
341
+ confidence: "unresolved",
342
+ resolutionKind: "constructor",
343
+ evidence: ["syntactic new expression"],
344
+ });
345
+ };
346
+ const addJsxReference = (node, owner) => {
347
+ const name = field(node, "name");
348
+ if (!name || !/^[A-Z]/.test(name.text))
349
+ return;
350
+ calls.push({
351
+ callerId: owner.id,
352
+ calleeName: name.text,
353
+ filePath,
354
+ language: languageId,
355
+ range: range(node),
356
+ confidence: "unresolved",
357
+ resolutionKind: "jsx-reference",
358
+ evidence: ["JSX element reference"],
359
+ });
360
+ };
361
+ // ---- traversal -----------------------------------------------------------
362
+ /** Walk a subtree, attributing calls to `owner` and declaring nested callables. */
363
+ const walk = (node, owner, chain) => {
364
+ for (const child of node.namedChildren)
365
+ visit(child, owner, chain);
366
+ };
367
+ function bindingType(declarator) {
368
+ const type = field(declarator, "type");
369
+ if (type) {
370
+ const name = text(type).replace(/^:\s*/, "").replace(/<.*$/s, "").trim();
371
+ if (/^[A-Za-z_$][\w$]*$/.test(name))
372
+ return name;
373
+ }
374
+ const value = field(declarator, "value");
375
+ if (value?.type === "new_expression") {
376
+ const constructor = field(value, "constructor");
377
+ if (constructor?.type === "identifier")
378
+ return constructor.text;
379
+ }
380
+ if (value?.type === "await_expression") {
381
+ const inner = value.namedChild(0);
382
+ if (inner?.type === "new_expression") {
383
+ const constructor = field(inner, "constructor");
384
+ if (constructor?.type === "identifier")
385
+ return constructor.text;
386
+ }
387
+ }
388
+ return undefined;
389
+ }
390
+ /** `this.x = new T()` in any member body tells us the property's type. */
391
+ function assignedPropertyTypes(body) {
392
+ const found = new Map();
393
+ if (!body)
394
+ return found;
395
+ const stack = [body];
396
+ while (stack.length) {
397
+ const node = stack.pop();
398
+ if (node.type === "assignment_expression") {
399
+ const left = field(node, "left");
400
+ const right = field(node, "right");
401
+ const value = right?.type === "await_expression" ? right.namedChild(0) : right;
402
+ if (left?.type === "member_expression" &&
403
+ field(left, "object")?.type === "this" &&
404
+ value?.type === "new_expression") {
405
+ const constructor = field(value, "constructor");
406
+ if (constructor?.type === "identifier")
407
+ found.set(text(field(left, "property")), constructor.text);
408
+ }
409
+ }
410
+ stack.push(...node.namedChildren);
411
+ }
412
+ return found;
413
+ }
414
+ function classMembers(body, chain, parent) {
415
+ if (!body)
416
+ return;
417
+ const assigned = assignedPropertyTypes(body);
418
+ for (const member of body.namedChildren) {
419
+ if (member.type === "method_definition") {
420
+ const name = text(field(member, "name"));
421
+ if (name === "constructor")
422
+ for (const parameter of field(member, "parameters")?.namedChildren ??
423
+ [])
424
+ if (parameter.type === "required_parameter") {
425
+ const accessibility = parameter.children.find((child) => ["accessibility_modifier", "readonly"].includes(child.type));
426
+ const propertyName = text(field(parameter, "pattern") ?? parameter.namedChild(0));
427
+ const annotation = text(field(parameter, "type"))
428
+ .replace(/^:\s*/, "")
429
+ .replace(/<.*$/s, "")
430
+ .trim();
431
+ if (accessibility && propertyName)
432
+ addSymbol(parameter, "property", propertyName, chain, {
433
+ parentId: parent.id,
434
+ signature: `${propertyName}: ${annotation}`,
435
+ metadata: /^[A-Za-z_$][\w$]*$/.test(annotation)
436
+ ? { declaredType: annotation }
437
+ : undefined,
438
+ });
439
+ }
440
+ const parameters = parameterSignature(field(member, "parameters"));
441
+ const kindToken = member.children.find((child) => ["get", "set"].includes(child.type))?.type;
442
+ const kind = name === "constructor"
443
+ ? "constructor"
444
+ : kindToken === "get"
445
+ ? "getter"
446
+ : kindToken === "set"
447
+ ? "setter"
448
+ : "method";
449
+ const body = field(member, "body");
450
+ const symbol = addSymbol(member, kind, name, chain, {
451
+ parameters,
452
+ signature: `${name}(${parameters})${returnType(member) ? `: ${returnType(member)}` : ""}`,
453
+ parentId: parent.id,
454
+ bodyNode: body,
455
+ metadata: { async: member.text.startsWith("async ") },
456
+ });
457
+ scopes.push({ types: new Map() });
458
+ for (const parameter of field(member, "parameters")?.namedChildren ??
459
+ [])
460
+ noteBinding(text(field(parameter, "pattern") ?? parameter.namedChild(0)), text(field(parameter, "type"))
461
+ .replace(/^:\s*/, "")
462
+ .replace(/<.*$/s, "")
463
+ .trim() || undefined);
464
+ if (body)
465
+ walk(body, symbol, [...chain, name]);
466
+ scopes.pop();
467
+ continue;
468
+ }
469
+ if (member.type === "public_field_definition" ||
470
+ member.type === "property_signature") {
471
+ const name = text(field(member, "name"));
472
+ const value = field(member, "value");
473
+ if (isCallable(value)) {
474
+ const parameters = parameterSignature(field(value, "parameters"));
475
+ const symbol = addSymbol(member, "method", name, chain, {
476
+ parameters,
477
+ signature: `${name}(${parameters})`,
478
+ parentId: parent.id,
479
+ bodyNode: field(value, "body"),
480
+ metadata: { async: value.text.startsWith("async ") },
481
+ });
482
+ scopes.push({ types: new Map() });
483
+ const body = field(value, "body");
484
+ if (body)
485
+ walk(body, symbol, [...chain, name]);
486
+ scopes.pop();
487
+ }
488
+ else if (name) {
489
+ const annotation = text(field(member, "type"))
490
+ .replace(/^:\s*/, "")
491
+ .replace(/<.*$/s, "")
492
+ .trim();
493
+ addSymbol(member, "property", name, chain, {
494
+ parentId: parent.id,
495
+ signature: `${name}${text(field(member, "type"))}`,
496
+ metadata: /^[A-Za-z_$][\w$]*$/.test(annotation)
497
+ ? { declaredType: annotation }
498
+ : undefined,
499
+ });
500
+ }
501
+ continue;
502
+ }
503
+ if (member.type === "method_signature") {
504
+ const name = text(field(member, "name"));
505
+ const parameters = parameterSignature(field(member, "parameters"));
506
+ addSymbol(member, "method", name, chain, {
507
+ parameters,
508
+ signature: `${name}(${parameters})${returnType(member) ? `: ${returnType(member)}` : ""}`,
509
+ parentId: parent.id,
510
+ });
511
+ }
512
+ }
513
+ }
514
+ function applyAssignedTypes(parent, assigned) {
515
+ for (const [name, type] of assigned) {
516
+ const property = symbols.find((candidate) => candidate.parentId === parent.id &&
517
+ candidate.kind === "property" &&
518
+ candidate.name === name);
519
+ if (property && !property.metadata?.declaredType)
520
+ property.metadata = { ...property.metadata, declaredType: type };
521
+ }
522
+ }
523
+ function visit(node, owner, chain) {
524
+ switch (node.type) {
525
+ case "import_statement":
526
+ importStatement(node);
527
+ return;
528
+ case "export_statement": {
529
+ exportStatement(node);
530
+ const declaration = field(node, "declaration");
531
+ if (declaration)
532
+ visit(declaration, owner, chain);
533
+ else
534
+ for (const child of node.namedChildren)
535
+ if (child.type !== "export_clause" &&
536
+ child.type !== "string" &&
537
+ child.type !== "namespace_export")
538
+ visit(child, owner, chain);
539
+ return;
540
+ }
541
+ case "class_declaration":
542
+ case "abstract_class_declaration": {
543
+ const name = text(field(node, "name"));
544
+ const heritage = node.namedChildren.find((child) => child.type === "class_heritage");
545
+ const supertypes = heritage
546
+ ? heritage.namedChildren.flatMap((clause) => clause.namedChildren.map((item) => text(item).replace(/<.*$/s, "").trim()))
547
+ : [];
548
+ const symbol = addSymbol(node, "class", name, chain, {
549
+ signature: `class ${name}`,
550
+ supertypes: supertypes.filter(Boolean),
551
+ bodyNode: field(node, "body"),
552
+ });
553
+ classMembers(field(node, "body"), [...chain, name], symbol);
554
+ applyAssignedTypes(symbol, assignedPropertyTypes(field(node, "body")));
555
+ return;
556
+ }
557
+ case "interface_declaration": {
558
+ const name = text(field(node, "name"));
559
+ const symbol = addSymbol(node, "interface", name, chain, {
560
+ signature: `interface ${name}`,
561
+ bodyNode: field(node, "body"),
562
+ });
563
+ classMembers(field(node, "body"), [...chain, name], symbol);
564
+ return;
565
+ }
566
+ case "type_alias_declaration":
567
+ addSymbol(node, "type", text(field(node, "name")), chain, {
568
+ signature: `type ${text(field(node, "name"))}`,
569
+ });
570
+ return;
571
+ case "enum_declaration":
572
+ addSymbol(node, "enum", text(field(node, "name")), chain, {
573
+ signature: `enum ${text(field(node, "name"))}`,
574
+ bodyNode: field(node, "body"),
575
+ });
576
+ return;
577
+ case "internal_module":
578
+ case "module": {
579
+ const name = text(field(node, "name")).replace(/^["'`]|["'`]$/g, "");
580
+ const symbol = addSymbol(node, "namespace", name, chain, {
581
+ signature: `namespace ${name}`,
582
+ bodyNode: field(node, "body"),
583
+ });
584
+ const body = field(node, "body");
585
+ if (body)
586
+ walk(body, symbol, [...chain, name]);
587
+ return;
588
+ }
589
+ case "function_signature": {
590
+ // Overload signature or ambient declaration: callable API, no body.
591
+ const name = text(field(node, "name"));
592
+ const parameters = parameterSignature(field(node, "parameters"));
593
+ addSymbol(node, "function", name, chain, {
594
+ parameters,
595
+ signature: `${name}(${parameters})${returnType(node) ? `: ${returnType(node)}` : ""}`,
596
+ metadata: { overloadSignature: !declarationOnly },
597
+ });
598
+ return;
599
+ }
600
+ case "function_declaration":
601
+ case "generator_function_declaration": {
602
+ const name = text(field(node, "name"));
603
+ const parameters = parameterSignature(field(node, "parameters"));
604
+ const body = field(node, "body");
605
+ const symbol = addSymbol(node, "function", name, chain, {
606
+ parameters,
607
+ signature: `${name}(${parameters})${returnType(node) ? `: ${returnType(node)}` : ""}`,
608
+ bodyNode: body,
609
+ parentId: owner?.id,
610
+ metadata: {
611
+ async: node.text.startsWith("async "),
612
+ reactComponent: looksLikeComponent(name, node, filePath),
613
+ },
614
+ });
615
+ scopes.push({ types: new Map() });
616
+ for (const parameter of field(node, "parameters")?.namedChildren ?? [])
617
+ noteBinding(text(field(parameter, "pattern") ?? parameter.namedChild(0)), text(field(parameter, "type"))
618
+ .replace(/^:\s*/, "")
619
+ .replace(/<.*$/s, "")
620
+ .trim() || undefined);
621
+ if (body)
622
+ walk(body, symbol, [...chain, name]);
623
+ scopes.pop();
624
+ return;
625
+ }
626
+ case "assignment_expression": {
627
+ // `obj.method = function(){}` / `Foo.prototype.method = () => {}` at module level:
628
+ // mirrors the lexical_declaration callable case below, keyed off the property name
629
+ // instead of a binding name. `module.exports`/`exports` targets are deliberately left
630
+ // alone — that's CommonJS module semantics, a separate, untouched limitation.
631
+ const left = field(node, "left");
632
+ const right = field(node, "right");
633
+ const objectText = left?.type === "member_expression" ? text(field(left, "object")) : undefined;
634
+ const propertyName = left?.type === "member_expression" ? text(field(left, "property")) : undefined;
635
+ if (chain.length === 0 &&
636
+ owner === undefined &&
637
+ objectText &&
638
+ propertyName &&
639
+ isCallable(right) &&
640
+ !/^(module\.exports|exports)(\.|$)/.test(objectText)) {
641
+ const parameters = parameterSignature(field(right, "parameters"));
642
+ const body = field(right, "body");
643
+ const symbol = addSymbol(node, "function", propertyName, [...chain, objectText], {
644
+ parameters,
645
+ signature: `${propertyName}(${parameters})${returnType(right) ? `: ${returnType(right)}` : ""}`,
646
+ bodyNode: body,
647
+ metadata: {
648
+ async: right.text.startsWith("async "),
649
+ reactComponent: looksLikeComponent(propertyName, right, filePath),
650
+ },
651
+ });
652
+ scopes.push({ types: new Map() });
653
+ for (const parameter of field(right, "parameters")?.namedChildren ?? [])
654
+ noteBinding(text(field(parameter, "pattern") ?? parameter.namedChild(0)), text(field(parameter, "type"))
655
+ .replace(/^:\s*/, "")
656
+ .replace(/<.*$/s, "")
657
+ .trim() || undefined);
658
+ if (body)
659
+ walk(body, symbol, [...chain, objectText, propertyName]);
660
+ scopes.pop();
661
+ return;
662
+ }
663
+ walk(node, owner, chain);
664
+ return;
665
+ }
666
+ case "lexical_declaration":
667
+ case "variable_declaration": {
668
+ for (const declarator of node.namedChildren.filter((child) => child.type === "variable_declarator")) {
669
+ const name = text(field(declarator, "name"));
670
+ const value = field(declarator, "value");
671
+ noteBinding(name, bindingType(declarator));
672
+ if (isCallable(value)) {
673
+ const parameters = parameterSignature(field(value, "parameters"));
674
+ const body = field(value, "body");
675
+ const symbol = addSymbol(declarator, "function", name, chain, {
676
+ parameters,
677
+ signature: `${name}(${parameters})${returnType(value) ? `: ${returnType(value)}` : ""}`,
678
+ bodyNode: body,
679
+ parentId: owner?.id,
680
+ metadata: {
681
+ async: value.text.startsWith("async "),
682
+ reactComponent: looksLikeComponent(name, value, filePath),
683
+ },
684
+ });
685
+ scopes.push({ types: new Map() });
686
+ for (const parameter of field(value, "parameters")
687
+ ?.namedChildren ?? [])
688
+ noteBinding(text(field(parameter, "pattern") ?? parameter.namedChild(0)), text(field(parameter, "type"))
689
+ .replace(/^:\s*/, "")
690
+ .replace(/<.*$/s, "")
691
+ .trim() || undefined);
692
+ if (body)
693
+ walk(body, symbol, [...chain, name]);
694
+ scopes.pop();
695
+ continue;
696
+ }
697
+ // Only module-level values are searchable symbols; locals are not.
698
+ if (name && chain.length === 0 && owner === undefined)
699
+ addSymbol(declarator, "variable", name, chain, {
700
+ signature: `${name}${text(field(declarator, "type"))}`,
701
+ });
702
+ if (value)
703
+ visit(value, owner, chain);
704
+ }
705
+ return;
706
+ }
707
+ case "call_expression":
708
+ addCall(node, owner ?? moduleOwner());
709
+ walk(node, owner, chain);
710
+ return;
711
+ case "new_expression":
712
+ addNew(node, owner ?? moduleOwner());
713
+ walk(node, owner, chain);
714
+ return;
715
+ case "jsx_opening_element":
716
+ case "jsx_self_closing_element":
717
+ addJsxReference(node, owner ?? moduleOwner());
718
+ walk(node, owner, chain);
719
+ return;
720
+ case "statement_block":
721
+ scopes.push({ types: new Map() });
722
+ walk(node, owner, chain);
723
+ scopes.pop();
724
+ return;
725
+ default:
726
+ walk(node, owner, chain);
727
+ }
728
+ }
729
+ walk(tree.rootNode, undefined, []);
730
+ if (moduleSymbol) {
731
+ // Represent the file's top-level code: imports, configuration and side effects.
732
+ const declarationTypes = new Set([
733
+ "class_declaration",
734
+ "abstract_class_declaration",
735
+ "interface_declaration",
736
+ "type_alias_declaration",
737
+ "enum_declaration",
738
+ "function_declaration",
739
+ "generator_function_declaration",
740
+ "function_signature",
741
+ "internal_module",
742
+ "module",
743
+ ]);
744
+ const isDeclaration = (node) => {
745
+ const inner = node.type === "export_statement"
746
+ ? (field(node, "declaration") ?? node)
747
+ : node;
748
+ return declarationTypes.has(inner.type);
749
+ };
750
+ moduleSymbol.source = tree.rootNode.namedChildren
751
+ .filter((child) => !isDeclaration(child))
752
+ .map((child) => child.text)
753
+ .join("\n");
754
+ }
755
+ // Two declarations can share a canonical identity (overloads, sibling scopes).
756
+ const identityCounts = new Map();
757
+ for (const symbol of symbols) {
758
+ const identity = symbol.canonicalIdentity ?? symbol.id;
759
+ const count = identityCounts.get(identity) ?? 0;
760
+ if (count > 0) {
761
+ const previousId = symbol.id;
762
+ symbol.id = `${identity}#${count + 1}`;
763
+ for (const call of calls)
764
+ if (call.callerId === previousId)
765
+ call.callerId = symbol.id;
766
+ }
767
+ identityCounts.set(identity, count + 1);
768
+ }
769
+ // Declaration files describe APIs; they never contribute executable edges.
770
+ const executableCalls = declarationOnly ? [] : calls;
771
+ // Link exports to the symbols they expose.
772
+ for (const record of exports) {
773
+ if (!record.localName)
774
+ continue;
775
+ const symbol = symbols.find((candidate) => candidate.name === record.localName &&
776
+ !candidate.parentId &&
777
+ !candidate.metadata?.overloadSignature);
778
+ if (symbol) {
779
+ record.symbolId = symbol.id;
780
+ symbol.metadata = {
781
+ ...symbol.metadata,
782
+ exported: record.exportedName === "default" ? "default" : "named",
783
+ };
784
+ }
785
+ }
786
+ return {
787
+ symbols,
788
+ calls: executableCalls,
789
+ imports,
790
+ exports,
791
+ parseError,
792
+ };
793
+ }