@mrclrchtr/supi-tree-sitter 6.0.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +6 -18
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  4. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  5. package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  7. package/package.json +3 -5
  8. package/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  9. package/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  10. package/src/api.ts +1 -11
  11. package/src/coordinates.ts +19 -0
  12. package/src/language.ts +5 -23
  13. package/src/operation-support.ts +3 -21
  14. package/src/provider/tree-sitter-provider.ts +5 -9
  15. package/src/session/runtime-controller.ts +10 -20
  16. package/src/session/session.ts +7 -1
  17. package/src/session/structural-worker-client.ts +9 -5
  18. package/src/session/structural-worker-protocol.ts +134 -9
  19. package/src/tool/call-name.ts +96 -0
  20. package/src/tool/call-sites.ts +11 -12
  21. package/src/tool/callees.ts +39 -56
  22. package/src/tool/exports.ts +9 -7
  23. package/src/tool/js-binding-pattern.ts +26 -0
  24. package/src/tool/node-at.ts +2 -21
  25. package/src/tool/outline-html-sql.ts +144 -12
  26. package/src/tool/outline.ts +50 -37
  27. package/src/tool/scope.ts +138 -0
  28. package/src/types.ts +0 -7
  29. package/src/worker/parsed-file-store.ts +1 -14
  30. package/src/worker/runtime.ts +0 -10
  31. package/src/worker/service.ts +6 -8
  32. package/src/worker/worker-main.ts +34 -10
  33. package/scripts/generate-kotlin-wasm.mjs +0 -132
  34. package/scripts/generate-sql-wasm.mjs +0 -158
  35. package/scripts/vendor-wasm.mjs +0 -167
  36. package/src/index.ts +0 -42
  37. package/src/session/runtime-registration.ts +0 -37
  38. package/src/session/service-registry.ts +0 -30
  39. package/src/tool/structure.ts +0 -8
@@ -1,5 +1,4 @@
1
1
  // biome-ignore-all lint/style/noExcessiveLinesPerFile: one private protocol module defines and validates the full closed contract
2
- import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
3
2
  import { isDebugOperationId } from "@mrclrchtr/supi-core/debug";
4
3
  import type { TreeSitterResult } from "../types.ts";
5
4
  import type { StructuralTimingEvent } from "./structural-timing.ts";
@@ -120,6 +119,45 @@ export type StructuralProtocolValidation<T> =
120
119
  | { readonly kind: "valid"; readonly message: T }
121
120
  | { readonly kind: "invalid"; readonly reason: string };
122
121
 
122
+ /** Validate one operation before it enters the Structural Worker mailbox. */
123
+ export function validateStructuralWorkerOperation(
124
+ value: unknown,
125
+ ): StructuralProtocolValidation<StructuralWorkerOperation> {
126
+ if (!isRecord(value) || typeof value.operation !== "string") {
127
+ return invalid("Invalid structural operation");
128
+ }
129
+
130
+ switch (value.operation) {
131
+ case "canParse":
132
+ case "outline":
133
+ case "imports":
134
+ case "exports":
135
+ case "callSites":
136
+ return hasExactKeys(value, ["operation", "file"]) && isFileValue(value.file)
137
+ ? valid(value as unknown as StructuralWorkerOperation)
138
+ : invalid("Invalid structural operation");
139
+ case "query":
140
+ return hasExactKeys(value, ["operation", "file", "query"]) &&
141
+ isFileValue(value.file) &&
142
+ isQueryValue(value.query)
143
+ ? valid(value as unknown as StructuralWorkerOperation)
144
+ : invalid("Invalid structural operation");
145
+ case "nodeAt":
146
+ return hasExactKeys(value, ["operation", "file", "line", "character"]) &&
147
+ isFileValue(value.file) &&
148
+ isPositiveInteger(value.line) &&
149
+ isPositiveInteger(value.character)
150
+ ? valid(value as unknown as StructuralWorkerOperation)
151
+ : invalid("Invalid structural operation");
152
+ case "calleesAt":
153
+ return isCalleesAtOperation(value)
154
+ ? valid(value as unknown as StructuralWorkerOperation)
155
+ : invalid("Invalid structural operation");
156
+ default:
157
+ return invalid("Invalid structural operation");
158
+ }
159
+ }
160
+
123
161
  /** Validate an untrusted request before the Structural Worker executes it. */
124
162
  export function validateStructuralWorkerRequest(
125
163
  value: unknown,
@@ -128,7 +166,7 @@ export function validateStructuralWorkerRequest(
128
166
  const required = ["kind", "version", "generation", "requestId", "input", "cancellationFlag"];
129
167
  const allowed = new Set([...required, "operationId", "deadline"]);
130
168
  if (
131
- required.some((key) => !(key in value)) ||
169
+ required.some((key) => !Object.hasOwn(value, key)) ||
132
170
  Object.keys(value).some((key) => !allowed.has(key))
133
171
  ) {
134
172
  return invalid("Invalid structural request envelope");
@@ -138,7 +176,6 @@ export function validateStructuralWorkerRequest(
138
176
  value.version !== STRUCTURAL_WORKER_PROTOCOL_VERSION ||
139
177
  !isPositiveInteger(value.generation) ||
140
178
  !isRequestId(value.requestId) ||
141
- !isRecord(value.input) ||
142
179
  !(value.cancellationFlag instanceof SharedArrayBuffer) ||
143
180
  value.cancellationFlag.byteLength !== Int32Array.BYTES_PER_ELEMENT ||
144
181
  (value.deadline !== undefined &&
@@ -147,7 +184,71 @@ export function validateStructuralWorkerRequest(
147
184
  ) {
148
185
  return invalid("Invalid structural request");
149
186
  }
150
- return valid(value as unknown as StructuralWorkerRequestMessage);
187
+ const operation = validateStructuralWorkerOperation(value.input);
188
+ return operation.kind === "valid"
189
+ ? valid(value as unknown as StructuralWorkerRequestMessage)
190
+ : invalid(operation.reason);
191
+ }
192
+
193
+ /** Validate an untrusted cancellation envelope before it reaches the Worker state machine. */
194
+ export function validateStructuralWorkerCancel(
195
+ value: unknown,
196
+ ): StructuralProtocolValidation<StructuralWorkerCancelMessage> {
197
+ if (!isRecord(value)) return invalid("Structural Worker cancel must be an object");
198
+ if (!hasExactKeys(value, ["kind", "version", "generation", "requestId"])) {
199
+ return invalid("Invalid structural cancel envelope");
200
+ }
201
+ if (
202
+ value.kind !== "cancel" ||
203
+ value.version !== STRUCTURAL_WORKER_PROTOCOL_VERSION ||
204
+ !isPositiveInteger(value.generation) ||
205
+ !isRequestId(value.requestId)
206
+ ) {
207
+ return invalid("Invalid structural cancel");
208
+ }
209
+ return valid(value as unknown as StructuralWorkerCancelMessage);
210
+ }
211
+
212
+ /** Validate an untrusted chunk acknowledgement before it reaches the Worker state machine. */
213
+ export function validateStructuralWorkerChunkAck(
214
+ value: unknown,
215
+ ): StructuralProtocolValidation<StructuralWorkerChunkAckMessage> {
216
+ if (!isRecord(value)) return invalid("Structural Worker chunk acknowledgement must be an object");
217
+ if (!hasExactKeys(value, ["kind", "version", "generation", "requestId", "sequence"])) {
218
+ return invalid("Invalid structural chunk acknowledgement envelope");
219
+ }
220
+ if (
221
+ value.kind !== "chunk-ack" ||
222
+ value.version !== STRUCTURAL_WORKER_PROTOCOL_VERSION ||
223
+ !isPositiveInteger(value.generation) ||
224
+ !isRequestId(value.requestId) ||
225
+ !isNonNegativeInteger(value.sequence)
226
+ ) {
227
+ return invalid("Invalid structural chunk acknowledgement");
228
+ }
229
+ return valid(value as unknown as StructuralWorkerChunkAckMessage);
230
+ }
231
+
232
+ /** Validate any untrusted message sent from the parent to the Structural Worker. */
233
+ export function validateParentToStructuralWorkerMessage(
234
+ value: unknown,
235
+ ): StructuralProtocolValidation<ParentToStructuralWorkerMessage> {
236
+ if (!isRecord(value)) return invalid("Structural Worker parent message must be an object");
237
+ switch (value.kind) {
238
+ case "request":
239
+ return validateStructuralWorkerRequest(value);
240
+ case "cancel":
241
+ return validateStructuralWorkerCancel(value);
242
+ case "chunk-ack":
243
+ return validateStructuralWorkerChunkAck(value);
244
+ default:
245
+ return invalid("Unknown Structural Worker parent message kind");
246
+ }
247
+ }
248
+
249
+ /** Read a generation only when the parent message has a safe positive integer shape. */
250
+ export function getStructuralWorkerMessageGeneration(value: unknown): number | undefined {
251
+ return isRecord(value) && isPositiveInteger(value.generation) ? value.generation : undefined;
151
252
  }
152
253
 
153
254
  /** Encode one complete structural result into fixed-size binary chunks. */
@@ -219,11 +320,6 @@ export function validateWorkerToParentMessage(
219
320
  }
220
321
  }
221
322
 
222
- /** Build the exact request control represented by one Worker request. */
223
- export interface StructuralWorkerRequestControl extends CodeRequestControl {
224
- readonly cancellationFlag: Int32Array;
225
- }
226
-
227
323
  function validateChunk(
228
324
  value: Record<string, unknown>,
229
325
  ): StructuralProtocolValidation<StructuralWorkerChunkMessage> {
@@ -439,10 +535,39 @@ function isPositiveInteger(value: unknown): value is number {
439
535
  return Number.isSafeInteger(value) && Number(value) > 0;
440
536
  }
441
537
 
538
+ function isNonNegativeInteger(value: unknown): value is number {
539
+ return Number.isSafeInteger(value) && Number(value) >= 0;
540
+ }
541
+
442
542
  function isRequestId(value: unknown): value is string {
443
543
  return typeof value === "string" && value.length > 0 && value.length <= 128;
444
544
  }
445
545
 
546
+ function isCalleesAtOperation(value: Record<string, unknown>): boolean {
547
+ const keysValid =
548
+ hasExactKeys(value, ["operation", "file", "line", "character"]) ||
549
+ hasExactKeys(value, ["operation", "file", "line", "character", "depth"]);
550
+ return (
551
+ keysValid &&
552
+ isFileValue(value.file) &&
553
+ isPositiveInteger(value.line) &&
554
+ isPositiveInteger(value.character) &&
555
+ (!("depth" in value) || value.depth === "direct" || value.depth === "deep")
556
+ );
557
+ }
558
+
559
+ function isFileValue(value: unknown): value is string {
560
+ return isNonBlankString(value, 16_384);
561
+ }
562
+
563
+ function isQueryValue(value: unknown): value is string {
564
+ return isNonBlankString(value, 10_000);
565
+ }
566
+
567
+ function isNonBlankString(value: unknown, maxLength: number): value is string {
568
+ return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength;
569
+ }
570
+
446
571
  function valid<T>(message: T): StructuralProtocolValidation<T> {
447
572
  return { kind: "valid", message };
448
573
  }
@@ -0,0 +1,96 @@
1
+ import type { GrammarId } from "../types.ts";
2
+
3
+ /** Normalize one grammar node that represents a call callee. */
4
+ export function normalizeCallName(text: string, grammarId: GrammarId, nodeType: string): string {
5
+ const normalized = text.trim().replace(/\s+/g, " ");
6
+ switch (grammarId) {
7
+ case "java":
8
+ return nodeType === "method_invocation"
9
+ ? stripFinalInvocationArguments(normalized)
10
+ : normalized;
11
+ case "kotlin":
12
+ return nodeType === "call_expression" ? stripKotlinCallSuffix(normalized) : normalized;
13
+ case "ruby":
14
+ return nodeType === "call" ? stripRubyCallSuffix(normalized) : normalized;
15
+ default:
16
+ return normalized;
17
+ }
18
+ }
19
+
20
+ function stripKotlinCallSuffix(text: string): string {
21
+ const lambda = topLevelIndex(text, "{");
22
+ const withoutLambda = lambda >= 0 ? text.slice(0, lambda).trim() : text;
23
+ return stripFinalInvocationArguments(withoutLambda);
24
+ }
25
+
26
+ function stripRubyCallSuffix(text: string): string {
27
+ const block = topLevelKeywordIndex(text, "do");
28
+ const withoutBlock = block >= 0 ? text.slice(0, block).trim() : text;
29
+ const bracedBlock = topLevelIndex(withoutBlock, "{");
30
+ const withoutSuffix = bracedBlock >= 0 ? withoutBlock.slice(0, bracedBlock).trim() : withoutBlock;
31
+ const withoutArguments = stripFinalInvocationArguments(withoutSuffix);
32
+ const whitespace = topLevelWhitespaceIndex(withoutArguments);
33
+ return whitespace >= 0 ? withoutArguments.slice(0, whitespace).trim() : withoutArguments;
34
+ }
35
+
36
+ function stripFinalInvocationArguments(text: string): string {
37
+ if (!text.endsWith(")")) return text;
38
+ const openingIndex = findLastTopLevelOpening(text);
39
+ return openingIndex >= 0 ? text.slice(0, openingIndex).trim() : text;
40
+ }
41
+
42
+ function findLastTopLevelOpening(text: string): number {
43
+ let openingIndex = -1;
44
+ scanTopLevel(text, (character, index, depth) => {
45
+ if (character === "(" && depth === 0) openingIndex = index;
46
+ return false;
47
+ });
48
+ return openingIndex;
49
+ }
50
+
51
+ function topLevelKeywordIndex(text: string, keyword: string): number {
52
+ const marker = ` ${keyword}`;
53
+ const index = topLevelIndex(text, marker);
54
+ return index >= 0 ? index + 1 : -1;
55
+ }
56
+
57
+ function topLevelIndex(text: string, marker: string): number {
58
+ return scanTopLevel(
59
+ text,
60
+ (_character, index, depth) => depth === 0 && text.startsWith(marker, index),
61
+ );
62
+ }
63
+
64
+ function topLevelWhitespaceIndex(text: string): number {
65
+ return scanTopLevel(text, (character, _index, depth) => depth === 0 && /\s/.test(character));
66
+ }
67
+
68
+ function scanTopLevel(
69
+ text: string,
70
+ visit: (character: string, index: number, depth: number) => boolean,
71
+ ): number {
72
+ let depth = 0;
73
+ let quote: string | null = null;
74
+ for (let index = 0; index < text.length; index += 1) {
75
+ const character = text[index] ?? "";
76
+ if (quote !== null) {
77
+ if (character === quote && text[index - 1] !== "\\") quote = null;
78
+ continue;
79
+ }
80
+ if (character === '"' || character === "'") {
81
+ quote = character;
82
+ continue;
83
+ }
84
+ if (visit(character, index, depth)) return index;
85
+ depth = updateDepth(character, depth);
86
+ }
87
+ return -1;
88
+ }
89
+
90
+ function updateDepth(character: string, depth: number): number {
91
+ if (character === "(" || character === "[" || character === "{") return depth + 1;
92
+ if (character === ")" || character === "]" || character === "}") {
93
+ return Math.max(0, depth - 1);
94
+ }
95
+ return depth;
96
+ }
@@ -4,6 +4,7 @@ import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
4
  import { detectGrammar } from "../language.ts";
5
5
  import type { CallSiteMatch, GrammarId, TreeSitterResult } from "../types.ts";
6
6
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
7
+ import { normalizeCallName } from "./call-name.ts";
7
8
 
8
9
  // ── Per-grammar call-site queries ─────────────────────────────────────
9
10
 
@@ -21,18 +22,16 @@ const CALL_SITE_QUERIES: Partial<Record<GrammarId, string>> = {
21
22
  javascript: JS_TS_CALL_SITE_QUERY,
22
23
  typescript: JS_TS_CALL_SITE_QUERY,
23
24
  tsx: JS_TS_CALL_SITE_QUERY,
24
- python:
25
- "(call function: (identifier) @call) (call function: (attribute attribute: (identifier) @call))",
26
- rust: "(call_expression function: (identifier) @call) (call_expression function: (field_expression field: (field_identifier) @call)) (macro_invocation macro: (identifier) @call)",
27
- go: "(call_expression function: (identifier) @call) (call_expression function: (selector_expression field: (field_identifier) @call))",
28
- c: "(call_expression function: (identifier) @call)",
29
- cpp: "(call_expression function: (identifier) @call) (call_expression function: (field_expression field: (field_identifier) @call))",
30
- java: "(method_invocation name: (identifier) @call) (object_creation_expression type: (type_identifier) @call)",
31
- kotlin:
32
- "(call_expression . (simple_identifier) @call) (call_expression . (navigation_expression . (simple_identifier) @call))",
33
- ruby: "(call method: (identifier) @call)",
25
+ python: "(call function: (_) @call)",
26
+ rust: "(call_expression function: (_) @call) (macro_invocation macro: (_) @call)",
27
+ go: "(call_expression function: (_) @call)",
28
+ c: "(call_expression function: (_) @call)",
29
+ cpp: "(call_expression function: (_) @call)",
30
+ java: "(method_invocation) @call (object_creation_expression type: (_) @call)",
31
+ kotlin: "(call_expression) @call",
32
+ ruby: "(call) @call",
34
33
  bash: "(command name: (command_name) @call)",
35
- r: "(call function: (identifier) @call)",
34
+ r: "(call function: (_) @call)",
36
35
  };
37
36
 
38
37
  /** Return whether the call-site extractor has a query for one grammar. */
@@ -78,7 +77,7 @@ export async function extractCallSites(
78
77
 
79
78
  for (const capture of captures) {
80
79
  // Normalize: trim whitespace and collapse internal whitespace to single space
81
- const name = capture.text.trim().replace(/\s+/g, " ");
80
+ const name = normalizeCallName(capture.text, grammarId, capture.nodeType);
82
81
  if (name.length === 0) continue;
83
82
 
84
83
  // Deduplicate by name + line
@@ -1,9 +1,12 @@
1
1
  // Structural callee extraction — enclosing-scope lookup with per-language queries.
2
2
 
3
3
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
+ import { validatePublicPositionBounds } from "../coordinates.ts";
4
5
  import { detectGrammar } from "../language.ts";
5
6
  import type { GrammarId, SourceRange, TreeSitterResult } from "../types.ts";
6
7
  import { queryParsedFile, type TreeSitterRuntime } from "../worker/runtime.ts";
8
+ import { normalizeCallName } from "./call-name.ts";
9
+ import { extractScopeName } from "./scope.ts";
7
10
 
8
11
  /** Result shape returned by lookupCalleesAt. */
9
12
  export interface CalleesAtResult {
@@ -25,13 +28,13 @@ const CALLEE_QUERIES: Partial<Record<GrammarId, string>> = {
25
28
  typescript: "(call_expression function: (_) @callee) (new_expression constructor: (_) @callee)",
26
29
  tsx: "(call_expression function: (_) @callee) (new_expression constructor: (_) @callee)",
27
30
  python: "(call function: (_) @callee)",
28
- rust: "(call_expression function: (_) @callee) (macro_invocation) @callee",
31
+ rust: "(call_expression function: (_) @callee) (macro_invocation macro: (_) @callee)",
29
32
  go: "(call_expression function: (_) @callee)",
30
33
  c: "(call_expression function: (_) @callee)",
31
34
  cpp: "(call_expression function: (_) @callee)",
32
- java: "(method_invocation name: (_) @callee) (object_creation_expression type: (_) @callee)",
33
- kotlin: "(call_expression . (_) @callee)",
34
- ruby: "(call method: (_) @callee)",
35
+ java: "(method_invocation) @callee (object_creation_expression type: (_) @callee)",
36
+ kotlin: "(call_expression) @callee",
37
+ ruby: "(call) @callee",
35
38
  bash: "(command . (_) @callee)",
36
39
  r: "(call function: (_) @callee)",
37
40
  };
@@ -57,63 +60,22 @@ const ENCLOSING_SCOPE_TYPES: Record<GrammarId, ReadonlySet<string>> = {
57
60
  "arrow_function",
58
61
  "function_expression",
59
62
  ]),
60
- python: new Set(["function_definition"]),
61
- rust: new Set(["function_item"]),
62
- go: new Set(["function_declaration"]),
63
+ python: new Set(["function_definition", "lambda"]),
64
+ rust: new Set(["function_item", "closure_expression"]),
65
+ go: new Set(["function_declaration", "method_declaration", "func_literal"]),
63
66
  c: new Set(["function_definition"]),
64
- cpp: new Set(["function_definition"]),
65
- java: new Set(["method_declaration"]),
66
- kotlin: new Set(["function_declaration"]),
67
- ruby: new Set(["method"]),
67
+ cpp: new Set(["function_definition", "lambda_expression"]),
68
+ java: new Set(["method_declaration", "constructor_declaration", "lambda_expression"]),
69
+ kotlin: new Set(["function_declaration", "lambda_literal", "anonymous_function"]),
70
+ ruby: new Set(["method", "block", "do_block", "lambda"]),
68
71
  bash: new Set(["function_definition"]),
69
72
  r: new Set(["function_definition"]),
70
73
  html: new Set(),
71
74
  sql: new Set(),
72
75
  };
73
76
 
74
- // ── Name extraction from enclosing scope text ─────────────────────────
75
-
76
- /** Extract a best-effort name from an enclosing scope node's source text. */
77
- function extractScopeName(_type: string, text: string): string {
78
- // JS/TS/Go/Rust: `function foo` or `fn foo` or `def foo`
79
- // Python: `def foo`
80
- const firstLine = text.split("\n")[0] ?? text;
81
- const match = firstLine.match(/(?:function|fn|func|def|method)\s+(\w+)/);
82
- if (match?.[1]) return match[1];
83
-
84
- // Ruby: `def foo`
85
- const rubyMatch = firstLine.match(/def\s+(\w+)/);
86
- if (rubyMatch?.[1]) return rubyMatch[1];
87
-
88
- // Bash: `foo()`
89
- const bashMatch = firstLine.match(/^(\w+)\s*\(\)/);
90
- if (bashMatch?.[1]) return bashMatch[1];
91
-
92
- // Kotlin/Java: `fun foo` or `fun Foo.bar()` — extract the last name part
93
- const kotlinMatch = firstLine.match(/fun\s+(?:[\w.]+\.)?(\w+)/);
94
- if (kotlinMatch?.[1]) return kotlinMatch[1];
95
-
96
- // C++: type name(params) { — heuristic: first word before `(`
97
- const cLikeMatch = firstLine.match(
98
- /(\w+)\s*\([^)]*\)\s*(?:const\s*)?(?:override\s*)?(?:final\s*)?[{;]/,
99
- );
100
- if (cLikeMatch?.[1]) return cLikeMatch[1];
101
-
102
- return "anonymous";
103
- }
104
-
105
77
  // ── Main entrypoint ──────────────────────────────────────────────────
106
78
 
107
- /**
108
- * Extract direct structural callee calls for a file at the given position.
109
- *
110
- * 1. Parses the file with the Tree-sitter runtime.
111
- * 2. Finds the enclosing function/method/callback scope at the position.
112
- * 3. Runs a grammar-specific callee query.
113
- * 4. Filters to captures within that enclosing scope.
114
- * 5. Excludes nested function/method/callback scopes that do not contain the anchor.
115
- * 6. Deduplicates by name.
116
- */
117
79
  /** Validate that coordinates are usable and grammar is supported. */
118
80
  function validateCalleeInput(
119
81
  filePath: string,
@@ -168,6 +130,16 @@ function findEnclosingScope(
168
130
  return null;
169
131
  }
170
132
 
133
+ /**
134
+ * Extract direct structural callee calls for a file at the given position.
135
+ *
136
+ * 1. Parses the file with the Tree-sitter runtime.
137
+ * 2. Finds the enclosing function/method/callback scope at the position.
138
+ * 3. Runs a grammar-specific callee query.
139
+ * 4. Filters to captures within that enclosing scope.
140
+ * 5. Excludes nested function/method/callback scopes that do not contain the anchor.
141
+ * 6. Deduplicates by name.
142
+ */
171
143
  // biome-ignore lint/complexity/useMaxParams: provider function needs runtime + coordinates + depth
172
144
  export async function lookupCalleesAt(
173
145
  runtime: TreeSitterRuntime,
@@ -189,6 +161,9 @@ export async function lookupCalleesAt(
189
161
  const { tree, source } = parseResult.data;
190
162
 
191
163
  try {
164
+ const boundsError = validatePublicPositionBounds(line, character, source);
165
+ if (boundsError) return boundsError;
166
+
192
167
  const scopes = ENCLOSING_SCOPE_TYPES[grammarId];
193
168
  const tsPoint = { row: line - 1, column: character - 1 };
194
169
  const node = tree.rootNode.descendantForPosition(tsPoint);
@@ -230,10 +205,17 @@ export async function lookupCalleesAt(
230
205
  };
231
206
  }
232
207
 
233
- const callees = filterCalleeCaptures(queryResult.data, enclosingNode, scopes, tsPoint, depth);
208
+ const callees = filterCalleeCaptures(
209
+ queryResult.data,
210
+ grammarId,
211
+ enclosingNode,
212
+ scopes,
213
+ tsPoint,
214
+ depth,
215
+ );
234
216
 
235
217
  const enclosingRange = nodeToSourceRange(enclosingNode);
236
- const scopeName = extractScopeName(enclosingNode.type, enclosingNode.text);
218
+ const scopeName = extractScopeName(enclosingNode);
237
219
 
238
220
  return {
239
221
  kind: "success",
@@ -329,7 +311,8 @@ function containsPoint(
329
311
  // biome-ignore lint/complexity/useMaxParams: filtering needs captures + node context + depth
330
312
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: filtering combines scope containment, column-aware inner-scope exclusion, and dedup in one cohesive pass
331
313
  function filterCalleeCaptures(
332
- captures: Array<{ range: SourceRange; text: string }>,
314
+ captures: Array<{ nodeType: string; range: SourceRange; text: string }>,
315
+ grammarId: GrammarId,
333
316
  // biome-ignore lint/suspicious/noExplicitAny: tree-sitter SyntaxNode is complex
334
317
  enclosingNode: any,
335
318
  scopeTypes: ReadonlySet<string>,
@@ -382,7 +365,7 @@ function filterCalleeCaptures(
382
365
  if (isInInner) continue;
383
366
  }
384
367
 
385
- const name = capture.text.replace(/\s+/g, "").slice(0, 60);
368
+ const name = normalizeCallName(capture.text, grammarId, capture.nodeType);
386
369
  if (name.length === 0 || seen.has(name)) continue;
387
370
  seen.add(name);
388
371
 
@@ -5,6 +5,7 @@ import { nodeToRange } from "../coordinates.ts";
5
5
  import type { SyntaxNodeLike } from "../syntax-node.ts";
6
6
  import type { ExportRecord, TreeSitterResult } from "../types.ts";
7
7
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
8
+ import { extractBindingIdentifiers } from "./js-binding-pattern.ts";
8
9
 
9
10
  /** Extract export records from a supported file. */
10
11
  export async function extractExports(
@@ -120,13 +121,9 @@ function extractLexicalDeclarationExports(
120
121
  ): void {
121
122
  for (const child of decl.children) {
122
123
  if (child.type !== "variable_declarator") continue;
123
- const nameNode = findNameNode(child);
124
- if (!nameNode) continue;
125
- exports.push({
126
- name: nameNode.text,
127
- kind: "variable",
128
- range: nodeToRange(child, source),
129
- });
124
+ const names = extractBindingIdentifiers(child.childForFieldName("name"));
125
+ const range = nodeToRange(child, source);
126
+ for (const name of names) exports.push({ name, kind: "variable", range });
130
127
  }
131
128
  }
132
129
 
@@ -139,6 +136,11 @@ function extractAmbientDeclarationExport(
139
136
  const nested = decl.children.find((child) => child.type !== "declare" && child.type !== ";");
140
137
  if (!nested) return;
141
138
 
139
+ if (nested.type === "lexical_declaration") {
140
+ extractLexicalDeclarationExports(nested, source, exports);
141
+ return;
142
+ }
143
+
142
144
  const name = getDeclarationName(nested);
143
145
  if (!name) return;
144
146
 
@@ -0,0 +1,26 @@
1
+ import type { SyntaxNodeLike } from "../syntax-node.ts";
2
+
3
+ /** Extract every identifier bound by one JavaScript or TypeScript pattern. */
4
+ export function extractBindingIdentifiers(pattern: SyntaxNodeLike | null): string[] {
5
+ if (!pattern) return [];
6
+
7
+ switch (pattern.type) {
8
+ case "identifier":
9
+ return [pattern.text];
10
+ case "shorthand_property_identifier_pattern":
11
+ return [pattern.text];
12
+ case "pair_pattern":
13
+ case "pair":
14
+ return extractBindingIdentifiers(pattern.childForFieldName("value"));
15
+ case "object_assignment_pattern":
16
+ case "assignment_pattern":
17
+ return extractBindingIdentifiers(pattern.childForFieldName("left"));
18
+ case "rest_pattern":
19
+ return pattern.children.flatMap((child) => extractBindingIdentifiers(child));
20
+ case "object_pattern":
21
+ case "array_pattern":
22
+ return pattern.children.flatMap((child) => extractBindingIdentifiers(child));
23
+ default:
24
+ return [];
25
+ }
26
+ }
@@ -1,7 +1,7 @@
1
1
  // Node-at-position lookup.
2
2
 
3
3
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
- import { nodeToRange, publicToTreeSitter, splitSourceLines } from "../coordinates.ts";
4
+ import { nodeToRange, publicToTreeSitter, validatePublicPositionBounds } from "../coordinates.ts";
5
5
  import type { NodeAtResult, SourceRange, TreeSitterResult } from "../types.ts";
6
6
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
7
7
 
@@ -28,7 +28,7 @@ export async function lookupNodeAt(
28
28
  const { tree, source } = parseResult.data;
29
29
 
30
30
  try {
31
- const boundsError = validateBounds(line, character, source);
31
+ const boundsError = validatePublicPositionBounds(line, character, source);
32
32
  if (boundsError) return boundsError;
33
33
 
34
34
  const tsPoint = publicToTreeSitter(line, character, source);
@@ -52,25 +52,6 @@ export async function lookupNodeAt(
52
52
  }
53
53
  }
54
54
 
55
- /** Validate that a requested public position exists in the source text. */
56
- function validateBounds(
57
- line: number,
58
- character: number,
59
- source: string,
60
- ): TreeSitterResult<NodeAtResult> | null {
61
- const lines = splitSourceLines(source);
62
- if (line > lines.length) {
63
- return { kind: "validation-error", message: "line is beyond end of file" };
64
- }
65
-
66
- const lineText = lines[line - 1] ?? "";
67
- if (character > lineText.length + 1) {
68
- return { kind: "validation-error", message: "character is beyond end of line" };
69
- }
70
-
71
- return null;
72
- }
73
-
74
55
  function collectAncestry(
75
56
  node: {
76
57
  type: string;