@maxgfr/codeindex 2.7.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 (82) hide show
  1. package/README.md +115 -0
  2. package/docs/MIGRATION.md +100 -0
  3. package/package.json +89 -0
  4. package/scripts/cli.mjs +10 -0
  5. package/scripts/engine.d.mts +618 -0
  6. package/scripts/engine.mjs +10655 -0
  7. package/scripts/grammars/bash.wasm +0 -0
  8. package/scripts/grammars/c.wasm +0 -0
  9. package/scripts/grammars/c_sharp.wasm +0 -0
  10. package/scripts/grammars/cpp.wasm +0 -0
  11. package/scripts/grammars/go.wasm +0 -0
  12. package/scripts/grammars/java.wasm +0 -0
  13. package/scripts/grammars/javascript.wasm +0 -0
  14. package/scripts/grammars/lua.wasm +0 -0
  15. package/scripts/grammars/php.wasm +0 -0
  16. package/scripts/grammars/python.wasm +0 -0
  17. package/scripts/grammars/ruby.wasm +0 -0
  18. package/scripts/grammars/rust.wasm +0 -0
  19. package/scripts/grammars/scala.wasm +0 -0
  20. package/scripts/grammars/tsx.wasm +0 -0
  21. package/scripts/grammars/typescript.wasm +0 -0
  22. package/scripts/grammars/web-tree-sitter.wasm +0 -0
  23. package/src/ast/extract.ts +713 -0
  24. package/src/ast/loader.ts +104 -0
  25. package/src/bm25.ts +156 -0
  26. package/src/callers.ts +0 -0
  27. package/src/calls.ts +131 -0
  28. package/src/categorize.ts +80 -0
  29. package/src/centrality.ts +148 -0
  30. package/src/classify.ts +53 -0
  31. package/src/cli-entry.ts +16 -0
  32. package/src/community.ts +345 -0
  33. package/src/complexity.ts +69 -0
  34. package/src/coupling.ts +0 -0
  35. package/src/deadcode.ts +46 -0
  36. package/src/edit.ts +93 -0
  37. package/src/engine-cli.ts +278 -0
  38. package/src/engine.ts +148 -0
  39. package/src/extract/code.ts +376 -0
  40. package/src/extract/markdown.ts +135 -0
  41. package/src/git.ts +205 -0
  42. package/src/glob.ts +56 -0
  43. package/src/graph.ts +0 -0
  44. package/src/grep.ts +115 -0
  45. package/src/hash.ts +13 -0
  46. package/src/ignore.ts +134 -0
  47. package/src/lang/c.ts +26 -0
  48. package/src/lang/common.ts +68 -0
  49. package/src/lang/csharp.ts +22 -0
  50. package/src/lang/elixir.ts +18 -0
  51. package/src/lang/go.ts +22 -0
  52. package/src/lang/java.ts +19 -0
  53. package/src/lang/js-ts.ts +112 -0
  54. package/src/lang/kotlin.ts +20 -0
  55. package/src/lang/lua.ts +18 -0
  56. package/src/lang/php.ts +24 -0
  57. package/src/lang/python.ts +22 -0
  58. package/src/lang/registry.ts +54 -0
  59. package/src/lang/ruby.ts +17 -0
  60. package/src/lang/rust.ts +22 -0
  61. package/src/lang/scala.ts +19 -0
  62. package/src/lang/shell.ts +17 -0
  63. package/src/lang/swift.ts +23 -0
  64. package/src/mcp.ts +512 -0
  65. package/src/memory.ts +75 -0
  66. package/src/modules.ts +128 -0
  67. package/src/pipeline.ts +59 -0
  68. package/src/query.ts +118 -0
  69. package/src/render/graph-json.ts +16 -0
  70. package/src/render/symbols-json.ts +79 -0
  71. package/src/repomap.ts +52 -0
  72. package/src/resolve.ts +950 -0
  73. package/src/rules.ts +249 -0
  74. package/src/scan.ts +172 -0
  75. package/src/sort.ts +12 -0
  76. package/src/surprise.ts +58 -0
  77. package/src/tests-map.ts +105 -0
  78. package/src/types.ts +201 -0
  79. package/src/util.ts +169 -0
  80. package/src/viz.ts +44 -0
  81. package/src/walk.ts +216 -0
  82. package/src/workspaces.ts +748 -0
@@ -0,0 +1,713 @@
1
+ import type { CodeSymbol, RawRef } from "../types.js";
2
+ import { byStr } from "../sort.js";
3
+ import { grammarKeyForExt, grammarReady, parserFor } from "./loader.js";
4
+
5
+ // A tree-sitter Node — typed structurally so we don't depend on web-tree-sitter's
6
+ // exported types leaking through the bundle. Only the members we use.
7
+ interface TSNode {
8
+ type: string;
9
+ text: string;
10
+ startPosition: { row: number; column: number };
11
+ endPosition: { row: number; column: number };
12
+ namedChildCount: number;
13
+ namedChild(i: number): TSNode | null;
14
+ childForFieldName(name: string): TSNode | null;
15
+ children: TSNode[];
16
+ }
17
+
18
+ export interface AstResult {
19
+ symbols: CodeSymbol[];
20
+ refs: RawRef[];
21
+ pkg?: string;
22
+ // Distinctive identifiers this file REFERENCES (not defines) — the raw material
23
+ // for code→code `use` edges. Bounded and pre-filtered to identifiers that could
24
+ // plausibly be a unique exported symbol (length ≥ 5), so the set stays small.
25
+ idents: string[];
26
+ // Unresolved call-site callee names — raw material for the cross-file call
27
+ // graph, resolved globally later. Always present (empty when the grammar has no
28
+ // `calls` mapping), mirroring how `idents` is always present. `receiver` is the
29
+ // immediate receiver of a qualified call (`axios.get(...)` → "axios").
30
+ calls: { name: string; line: number; receiver?: string }[];
31
+ // JS/TS named-import bindings — always present (empty for non-JS/TS).
32
+ importedNames: string[];
33
+ }
34
+
35
+ const MAX_REF_IDENTS = 256;
36
+ const MAX_CALLS = 512;
37
+ const MAX_IMPORTED_NAMES = 256;
38
+
39
+ // JS/TS node types an anonymous `export default` can wrap ("function" and
40
+ // "class" are the expression forms; the _declaration forms cover grammars that
41
+ // keep the declaration node but omit the name).
42
+ const ANON_DEFAULT_FN = new Set([
43
+ "function", "function_expression", "function_declaration",
44
+ "generator_function", "generator_function_declaration", "arrow_function",
45
+ ]);
46
+ const ANON_DEFAULT_CLASS = new Set(["class", "class_declaration", "abstract_class_declaration"]);
47
+
48
+ // Collect distinctive referenced identifiers across the whole tree, minus the
49
+ // file's own definition names. Deterministic (sorted) and capped.
50
+ function collectRefIdents(root: TSNode, defNames: Set<string>): string[] {
51
+ const found = new Set<string>();
52
+ const visit = (node: TSNode): void => {
53
+ if (
54
+ node.namedChildCount === 0 &&
55
+ /identifier|constant|(^|_)name$/.test(node.type) &&
56
+ /^[A-Za-z_]\w{4,}$/.test(node.text) &&
57
+ !defNames.has(node.text)
58
+ ) {
59
+ found.add(node.text);
60
+ }
61
+ for (let i = 0; i < node.namedChildCount; i++) visit(node.namedChild(i)!);
62
+ };
63
+ visit(root);
64
+ return [...found].sort().slice(0, MAX_REF_IDENTS);
65
+ }
66
+
67
+ // How one grammar's declarations map to symbols. `defs` maps a node type to a
68
+ // symbol kind; `containers` are nodes whose body we recurse into for nested
69
+ // members (methods, namespace/impl bodies); `exported` decides visibility from
70
+ // the declaration's first line or name.
71
+ interface LangSpec {
72
+ lang: string;
73
+ defs: Record<string, string>;
74
+ containers: Set<string>;
75
+ exported: (firstLine: string, name: string) => boolean;
76
+ imports?: Record<string, "string" | "path">; // node type → how to read the specifier
77
+ // Call-expression node type → how to read the callee name. "function": read the
78
+ // callee/function field, descending to the rightmost segment of a member/
79
+ // attribute/selector/scoped callee. "member": a dedicated member-call node —
80
+ // read its `name` field. "constructor": a new/object-creation node — read the
81
+ // constructed type identifier.
82
+ calls?: Record<string, "function" | "member" | "constructor">;
83
+ // Also surface top-level ASSIGNMENTS of function/class expressions —
84
+ // `res.sendStatus = function sendStatus() {}`, `Foo.prototype.bar = () => {}`,
85
+ // `exports.helper = function () {}` — the CommonJS definition style that
86
+ // declaration-node walks miss entirely (express, connect, older Node code).
87
+ // Two grammar shapes are handled: JS/TS expression_statement>assignment_
88
+ // expression and lua assignment_statement (variable_list = expression_list).
89
+ assignments?: boolean;
90
+ }
91
+
92
+ const byPublicKeyword = (line: string): boolean => /\b(public|internal)\b/.test(line);
93
+ // Scala is public by default; `private`/`protected` modifiers sit on the
94
+ // declaration's first line (same stance as the regex tier).
95
+ const byNotPrivate = (line: string): boolean => !/\b(private|protected)\b/.test(line);
96
+ // Lua: `local function f` is file-local by construction. Assignment-style
97
+ // `local f = function()` stays exported — regex-tier parity (its rule marks
98
+ // those exported), and the `local` keyword lives on the wrapping
99
+ // variable_declaration, outside the assignment node this line is read from.
100
+ const byNotLocal = (line: string): boolean => !/^local\b/.test(line);
101
+ const byPub = (line: string): boolean => /\bpub\b/.test(line);
102
+ const byCapital = (_l: string, name: string): boolean => /^[A-Z]/.test(name);
103
+ const byPyConvention = (_l: string, name: string): boolean => !name.startsWith("_") || /^__\w+__$/.test(name);
104
+ const always = (): boolean => true;
105
+ // JS/TS export is structural (an `export` statement wraps the declaration); a
106
+ // bare declaration is module-private, so the name/line heuristic never marks it.
107
+ const neverExport = (): boolean => false;
108
+
109
+ // TypeScript is the base for tsx and javascript, so it is a named const rather
110
+ // than indexed back out of SPECS (which noUncheckedIndexedAccess would widen to
111
+ // `LangSpec | undefined`, breaking the derived spreads below).
112
+ const TS_SPEC: LangSpec = {
113
+ lang: "typescript",
114
+ defs: {
115
+ function_declaration: "function", generator_function_declaration: "function",
116
+ class_declaration: "class", abstract_class_declaration: "class",
117
+ interface_declaration: "interface", type_alias_declaration: "type",
118
+ enum_declaration: "enum", method_definition: "method", variable_declarator: "const",
119
+ },
120
+ containers: new Set(["class_body", "export_statement", "program", "lexical_declaration", "variable_declaration"]),
121
+ exported: neverExport, // export is tracked structurally via export_statement; see walk
122
+ imports: { import_statement: "string" },
123
+ calls: { call_expression: "function", new_expression: "constructor" },
124
+ assignments: true,
125
+ };
126
+
127
+ const SPECS: Record<string, LangSpec> = {
128
+ typescript: TS_SPEC,
129
+ tsx: { ...TS_SPEC, lang: "typescript" },
130
+ javascript: {
131
+ ...TS_SPEC,
132
+ lang: "javascript",
133
+ defs: {
134
+ function_declaration: "function", generator_function_declaration: "function",
135
+ class_declaration: "class", method_definition: "method", variable_declarator: "const",
136
+ },
137
+ },
138
+ python: {
139
+ lang: "python",
140
+ defs: { function_definition: "function", class_definition: "class" },
141
+ containers: new Set(["block", "decorated_definition", "module"]),
142
+ exported: byPyConvention,
143
+ imports: { import_statement: "path", import_from_statement: "path" },
144
+ calls: { call: "function" },
145
+ },
146
+ go: {
147
+ lang: "go",
148
+ defs: {
149
+ function_declaration: "function", method_declaration: "method",
150
+ type_spec: "type", const_spec: "const", var_spec: "var",
151
+ },
152
+ containers: new Set(["type_declaration", "const_declaration", "var_declaration", "source_file"]),
153
+ exported: byCapital,
154
+ imports: { import_declaration: "string" },
155
+ calls: { call_expression: "function" },
156
+ },
157
+ ruby: {
158
+ lang: "ruby",
159
+ defs: { method: "def", singleton_method: "def", class: "class", module: "module" },
160
+ containers: new Set(["class", "module", "body_statement", "program"]),
161
+ exported: always,
162
+ // Ruby models every invocation — dotted, parenthesized, or bare command form
163
+ // (`puts "x"`) — as a `call` node whose callee is the `method` field.
164
+ calls: { call: "function" },
165
+ },
166
+ java: {
167
+ lang: "java",
168
+ defs: {
169
+ class_declaration: "class", interface_declaration: "interface",
170
+ enum_declaration: "enum", record_declaration: "record",
171
+ method_declaration: "method", constructor_declaration: "constructor",
172
+ },
173
+ containers: new Set(["class_body", "interface_body", "enum_body", "program"]),
174
+ exported: byPublicKeyword,
175
+ imports: { import_declaration: "path" },
176
+ calls: { method_invocation: "function", object_creation_expression: "constructor" },
177
+ },
178
+ rust: {
179
+ lang: "rust",
180
+ defs: {
181
+ function_item: "function", struct_item: "struct", enum_item: "enum",
182
+ trait_item: "trait", type_item: "type", mod_item: "mod",
183
+ const_item: "const", static_item: "static", union_item: "union", macro_definition: "macro",
184
+ },
185
+ containers: new Set(["impl_item", "declaration_list", "source_file"]),
186
+ exported: byPub,
187
+ calls: { call_expression: "function" },
188
+ },
189
+ c_sharp: {
190
+ lang: "csharp",
191
+ defs: {
192
+ class_declaration: "class", interface_declaration: "interface",
193
+ struct_declaration: "struct", enum_declaration: "enum", record_declaration: "record",
194
+ method_declaration: "method", constructor_declaration: "constructor", property_declaration: "property",
195
+ },
196
+ containers: new Set(["namespace_declaration", "declaration_list", "compilation_unit", "file_scoped_namespace_declaration"]),
197
+ exported: byPublicKeyword,
198
+ calls: { invocation_expression: "function", object_creation_expression: "constructor" },
199
+ },
200
+ php: {
201
+ lang: "php",
202
+ defs: {
203
+ function_definition: "function", class_declaration: "class",
204
+ interface_declaration: "interface", trait_declaration: "trait",
205
+ enum_declaration: "enum", method_declaration: "method",
206
+ },
207
+ containers: new Set(["declaration_list", "program"]),
208
+ exported: always,
209
+ calls: { function_call_expression: "function", member_call_expression: "member", object_creation_expression: "constructor" },
210
+ },
211
+ c: {
212
+ lang: "c",
213
+ defs: {
214
+ function_definition: "function", struct_specifier: "struct",
215
+ enum_specifier: "enum", union_specifier: "union", type_definition: "type",
216
+ },
217
+ // C has no visibility keyword — headers are the interface, so everything
218
+ // counts as exported (same stance as the regex extractor).
219
+ containers: new Set(["translation_unit", "declaration_list", "linkage_specification", "preproc_ifdef", "preproc_if"]),
220
+ exported: always,
221
+ calls: { call_expression: "function" },
222
+ },
223
+ cpp: {
224
+ lang: "cpp",
225
+ defs: {
226
+ function_definition: "function", class_specifier: "class", struct_specifier: "struct",
227
+ enum_specifier: "enum", union_specifier: "union", type_definition: "type",
228
+ namespace_definition: "namespace",
229
+ },
230
+ containers: new Set([
231
+ "translation_unit", "declaration_list", "field_declaration_list",
232
+ "template_declaration", "linkage_specification", "preproc_ifdef", "preproc_if",
233
+ ]),
234
+ exported: always,
235
+ calls: { call_expression: "function", new_expression: "constructor" },
236
+ },
237
+ scala: {
238
+ lang: "scala",
239
+ defs: {
240
+ class_definition: "class", object_definition: "object", trait_definition: "trait",
241
+ enum_definition: "enum", function_definition: "def", function_declaration: "def",
242
+ val_definition: "val", var_definition: "var", type_definition: "type", given_definition: "given",
243
+ },
244
+ // package_clause carries braced-package bodies (`package com.acme { … }`);
245
+ // template_body is every class/object/trait body.
246
+ containers: new Set(["compilation_unit", "package_clause", "template_body"]),
247
+ exported: byNotPrivate,
248
+ // Qualified calls are call_expression → field_expression (value/field);
249
+ // `new Widget(...)` is an instance_expression with a bare type child.
250
+ calls: { call_expression: "function", instance_expression: "constructor" },
251
+ },
252
+ bash: {
253
+ lang: "shell",
254
+ defs: { function_definition: "function" },
255
+ // if/compound bodies carry guarded definitions (`if …; then f() { … }; fi`).
256
+ containers: new Set(["program", "if_statement", "compound_statement"]),
257
+ // Shell has no visibility — every function is callable from outside.
258
+ exported: always,
259
+ // Every invocation is a `command` whose `name` field is a command_name
260
+ // wrapping a `word` leaf (hence IDENT_LEAF includes `word`).
261
+ calls: { command: "function" },
262
+ },
263
+ lua: {
264
+ lang: "lua",
265
+ defs: { function_declaration: "function" },
266
+ // variable_declaration wraps `local x = function()` assignment statements.
267
+ containers: new Set(["chunk", "variable_declaration"]),
268
+ exported: byNotLocal,
269
+ // function_call's `name` is an identifier, a dot_index_expression
270
+ // (table/field) or a method_index_expression (table/method) — the receiver
271
+ // is the `table` field in both qualified forms.
272
+ calls: { function_call: "function" },
273
+ assignments: true, // `M.alias = function(z) … end` (assignment_statement shape)
274
+ },
275
+ };
276
+
277
+ function firstLine(node: TSNode): string {
278
+ const nl = node.text.indexOf("\n");
279
+ return (nl === -1 ? node.text : node.text.slice(0, nl)).trim().slice(0, 200);
280
+ }
281
+
282
+ function nameOf(node: TSNode): string | undefined {
283
+ const named = node.childForFieldName("name");
284
+ if (named?.text) return named.text;
285
+ // C/C++: the name hides inside a declarator chain (function_definition →
286
+ // [pointer_]declarator → function_declarator → identifier). Follow the
287
+ // `declarator` field down to the identifier leaf.
288
+ let decl = node.childForFieldName("declarator");
289
+ while (decl) {
290
+ if (decl.namedChildCount === 0 && /(^|_)identifier$/.test(decl.type)) return decl.text;
291
+ const next = decl.childForFieldName("declarator");
292
+ if (!next || next === decl) break;
293
+ decl = next;
294
+ }
295
+ // Fall back to the first identifier-like named child (covers grammars that do
296
+ // not expose a `name` field on a given node).
297
+ for (let i = 0; i < node.namedChildCount; i++) {
298
+ const c = node.namedChild(i)!;
299
+ if (/(^|_)(identifier|name|constant)$/.test(c.type)) return c.text;
300
+ }
301
+ return undefined;
302
+ }
303
+
304
+ // Read import specifiers from the whole tree by scanning for the grammar's import
305
+ // node types. "string" pulls the first string literal's inner text; "path" takes
306
+ // the dotted/namespaced module text verbatim (resolution happens later).
307
+ function collectImports(root: TSNode, spec: LangSpec): RawRef[] {
308
+ if (!spec.imports) return [];
309
+ const out: RawRef[] = [];
310
+ const seen = new Set<string>();
311
+ const add = (s: string): void => {
312
+ const v = s.trim();
313
+ if (v && !seen.has(v)) {
314
+ seen.add(v);
315
+ out.push({ kind: "import", spec: v });
316
+ }
317
+ };
318
+ const visit = (node: TSNode): void => {
319
+ const how = spec.imports![node.type];
320
+ if (how === "string") {
321
+ const str = findFirst(node, (n) => /string/.test(n.type));
322
+ if (str) add(str.text.replace(/^['"]|['"]$/g, ""));
323
+ } else if (how === "path") {
324
+ const name = node.childForFieldName("name") ?? node.childForFieldName("module_name");
325
+ add((name ?? node).text.replace(/^(import|from)\s+/, "").split(/\s+/)[0]!);
326
+ }
327
+ for (let i = 0; i < node.namedChildCount; i++) visit(node.namedChild(i)!);
328
+ };
329
+ visit(root);
330
+ return out;
331
+ }
332
+
333
+ function findFirst(node: TSNode, pred: (n: TSNode) => boolean): TSNode | undefined {
334
+ for (let i = 0; i < node.namedChildCount; i++) {
335
+ const c = node.namedChild(i)!;
336
+ if (pred(c)) return c;
337
+ const deep = findFirst(c, pred);
338
+ if (deep) return deep;
339
+ }
340
+ return undefined;
341
+ }
342
+
343
+ // True for a leaf node that IS an identifier-ish name (identifier,
344
+ // property_identifier, type_identifier, field_identifier, constant, php's
345
+ // `name`, or bash's `word` — the leaf inside a command_name). The rightmost
346
+ // such leaf of a callee is the called name. End-anchored, so python/ruby
347
+ // keyword_* node types (keyword_argument, keyword_pattern, …) never match.
348
+ const IDENT_LEAF = /(^|_)(identifier|name|constant|word)$/;
349
+
350
+ // Read the callee's simple name from a (possibly qualified) callee node: a bare
351
+ // identifier returns itself; a member/attribute/selector/scoped access returns its
352
+ // final segment via the field name that grammar uses (falling back to the last
353
+ // named child). Returns undefined for a computed/complex callee we can't name.
354
+ function readName(node: TSNode | null): string | undefined {
355
+ if (!node) return undefined;
356
+ if (node.namedChildCount === 0) return IDENT_LEAF.test(node.type) ? node.text : undefined;
357
+ const seg =
358
+ node.childForFieldName("name") ??
359
+ node.childForFieldName("property") ??
360
+ node.childForFieldName("attribute") ??
361
+ node.childForFieldName("field") ??
362
+ // Callee wrappers that point at the real callee via a `function` field:
363
+ // scala's generic_function (`foo[Int](x)`) and a curried/chained
364
+ // call_expression callee (`curried(a)(b)`) — descend to the inner name
365
+ // instead of tripping over type_arguments/arguments as the last child.
366
+ node.childForFieldName("function");
367
+ if (seg) return readName(seg);
368
+ const last = node.namedChild(node.namedChildCount - 1);
369
+ return last && last !== node ? readName(last) : undefined;
370
+ }
371
+
372
+ // The IMMEDIATE receiver of a qualified call: the rightmost name segment of the
373
+ // object the callee is read from (`axios.get(...)` → "axios"; `a.b.c(...)` →
374
+ // "b"). Grammars name the object field differently — `object` (JS/TS
375
+ // member_expression, python attribute, java method_invocation, php member call),
376
+ // go selector_expression's `operand`, rust field_expression's `value` and
377
+ // scoped_identifier's `path`, c# member_access_expression's `expression`, c/c++
378
+ // field_expression's `argument`, ruby call's `receiver`, lua dot/method_index_
379
+ // expression's `table` (scala's field_expression reuses `value`). Undefined for a
380
+ // bare callee or a computed/complex receiver (`fetch().then(...)`, `arr[0].map(...)`).
381
+ function readReceiver(node: TSNode | null): string | undefined {
382
+ if (!node || node.namedChildCount === 0) return undefined;
383
+ const obj =
384
+ node.childForFieldName("object") ??
385
+ node.childForFieldName("operand") ??
386
+ node.childForFieldName("value") ??
387
+ node.childForFieldName("path") ??
388
+ node.childForFieldName("expression") ??
389
+ node.childForFieldName("argument") ??
390
+ node.childForFieldName("receiver") ??
391
+ node.childForFieldName("table");
392
+ const name = obj ? readName(obj) : undefined;
393
+ return name && /^[A-Za-z_]\w*$/.test(name) ? name : undefined;
394
+ }
395
+
396
+ // Collect callee names for every call-expression node the grammar maps. "function"
397
+ // reads the callee/function field (grammars that name it differently — Java's
398
+ // `method_invocation` — expose the callee under `name`); "member" reads the
399
+ // dedicated member-call node's `name`; "constructor" reads the constructed type.
400
+ // A qualified call also carries the immediate `receiver` name (see readReceiver).
401
+ // Names are filtered to plausible identifiers (≥ 2 chars), deduped by name+line,
402
+ // sorted, and capped, so the set stays small and deterministic.
403
+ function collectCalls(root: TSNode, spec: LangSpec): { name: string; line: number; receiver?: string }[] {
404
+ if (!spec.calls) return [];
405
+ const out: { name: string; line: number; receiver?: string }[] = [];
406
+ const seen = new Set<string>();
407
+ const add = (name: string | undefined, node: TSNode, receiver?: string): void => {
408
+ if (!name || name.length < 2 || !/^[A-Za-z_]\w*$/.test(name)) return;
409
+ const line = node.startPosition.row + 1;
410
+ const key = `${name} ${line}`;
411
+ if (seen.has(key)) return;
412
+ seen.add(key);
413
+ out.push(receiver ? { name, line, receiver } : { name, line });
414
+ };
415
+ const visit = (node: TSNode): void => {
416
+ const how = spec.calls![node.type];
417
+ if (how === "function") {
418
+ // Grammars name the callee field differently: `function` (TS/py/go/rust/
419
+ // c#/php), `name` (Java's method_invocation), `method` (Ruby's call). The
420
+ // receiver lives on the qualified callee node, or (java/ruby) on the call
421
+ // node itself.
422
+ const callee =
423
+ node.childForFieldName("function") ?? node.childForFieldName("callee") ?? node.childForFieldName("method") ?? node.childForFieldName("name");
424
+ add(readName(callee), node, readReceiver(callee) ?? readReceiver(node));
425
+ } else if (how === "member") {
426
+ add(readName(node.childForFieldName("name")), node, readReceiver(node));
427
+ } else if (how === "constructor") {
428
+ // TS/Java/C# expose the type under a `constructor`/`type` field; PHP's
429
+ // object_creation_expression carries it as a bare `name` child, so fall
430
+ // back to the first identifier-ish child when no field matches.
431
+ let t = node.childForFieldName("constructor") ?? node.childForFieldName("type") ?? node.childForFieldName("name");
432
+ for (let i = 0; !t && i < node.namedChildCount; i++) {
433
+ const c = node.namedChild(i)!;
434
+ if (IDENT_LEAF.test(c.type)) t = c;
435
+ }
436
+ add(readName(t), node, readReceiver(t ?? null));
437
+ }
438
+ for (let i = 0; i < node.namedChildCount; i++) visit(node.namedChild(i)!);
439
+ };
440
+ visit(root);
441
+ out.sort((a, b) => byStr(a.name, b.name) || a.line - b.line);
442
+ return out.slice(0, MAX_CALLS);
443
+ }
444
+
445
+ // Collect JS/TS named-import bindings: `import { a, b as c } from "x"` →
446
+ // `import_clause → named_imports → import_specifier`, reading each specifier's
447
+ // `name` field (the pre-alias name). Default/namespace bindings are intentionally
448
+ // NOT collected — the call-resolution gate only corroborates named imports, and a
449
+ // default/namespace binding names a module, not a specific exported symbol.
450
+ function collectImportedNames(root: TSNode, spec: LangSpec): string[] {
451
+ if (!spec.imports?.import_statement) return [];
452
+ const found = new Set<string>();
453
+ const visit = (node: TSNode): void => {
454
+ if (node.type === "import_statement") {
455
+ for (let i = 0; i < node.namedChildCount; i++) {
456
+ const clause = node.namedChild(i)!;
457
+ if (clause.type !== "import_clause") continue;
458
+ for (let j = 0; j < clause.namedChildCount; j++) {
459
+ const named = clause.namedChild(j)!;
460
+ if (named.type !== "named_imports") continue;
461
+ for (let k = 0; k < named.namedChildCount; k++) {
462
+ const specifier = named.namedChild(k)!;
463
+ if (specifier.type !== "import_specifier") continue;
464
+ const nm = specifier.childForFieldName("name") ?? specifier.namedChild(0);
465
+ if (nm?.text) found.add(nm.text);
466
+ }
467
+ }
468
+ }
469
+ }
470
+ for (let i = 0; i < node.namedChildCount; i++) visit(node.namedChild(i)!);
471
+ };
472
+ visit(root);
473
+ return [...found].sort(byStr).slice(0, MAX_IMPORTED_NAMES);
474
+ }
475
+
476
+ // Extract declared symbols from one file via its committed grammar. Returns
477
+ // undefined when no grammar is loaded for the extension (caller falls back to the
478
+ // regex extractor). Walks top-level declarations plus one level of nested members.
479
+ export function extractAst(rel: string, ext: string, content: string): AstResult | undefined {
480
+ const key = grammarKeyForExt(ext);
481
+ if (!key || !grammarReady(key)) return undefined;
482
+ const spec = SPECS[key];
483
+ if (!spec) return undefined;
484
+ const parser = parserFor(key);
485
+ if (!parser) return undefined;
486
+
487
+ let tree: { rootNode: TSNode; delete(): void } | null = null;
488
+ try {
489
+ tree = parser.parse(content) as unknown as { rootNode: TSNode; delete(): void };
490
+ if (!tree) return undefined;
491
+ const symbols: CodeSymbol[] = [];
492
+ const root = tree.rootNode;
493
+ // The file stem names an anonymous `export default` (Button.tsx → "Button").
494
+ const stem = (rel.split("/").pop() ?? "").replace(/\.[^.]+$/, "");
495
+ // `export default Foo;` / `export { Foo }` re-export a declaration made
496
+ // earlier in the file; record those names and mark the matching symbols
497
+ // exported after the walk (the declaration node itself is not wrapped).
498
+ const exportedNames = new Set<string>();
499
+
500
+ const walk = (node: TSNode, parent: string | undefined, exported: boolean): void => {
501
+ // `export …` / `export default …` (JS/TS) marks the wrapped declaration.
502
+ const nowExported = exported || node.type === "export_statement";
503
+ if (node.type === "export_statement") {
504
+ for (let i = 0; i < node.namedChildCount; i++) {
505
+ const c = node.namedChild(i)!;
506
+ if (c.type === "identifier") exportedNames.add(c.text);
507
+ else if (c.type === "export_clause") {
508
+ for (let j = 0; j < c.namedChildCount; j++) {
509
+ const spec = c.namedChild(j)!;
510
+ const nm = spec.childForFieldName("name") ?? spec.namedChild(0);
511
+ if (nm?.text) exportedNames.add(nm.text);
512
+ }
513
+ }
514
+ }
515
+ // An anonymous `export default function/class/arrow` has no name node the
516
+ // declaration walk could pick up — name it after the file stem (ultradoc
517
+ // parity), so the module's default export is a real, referencable symbol.
518
+ if (stem && node.children.some((c) => c.type === "default")) {
519
+ for (let i = 0; i < node.namedChildCount; i++) {
520
+ const c = node.namedChild(i)!;
521
+ const fnLike = ANON_DEFAULT_FN.has(c.type);
522
+ const classLike = ANON_DEFAULT_CLASS.has(c.type);
523
+ if ((fnLike || classLike) && !c.childForFieldName("name")) {
524
+ symbols.push({
525
+ name: stem,
526
+ kind: classLike ? "class" : "function",
527
+ file: rel,
528
+ line: node.startPosition.row + 1,
529
+ endLine: node.endPosition.row + 1,
530
+ signature: firstLine(node),
531
+ exported: true,
532
+ lang: spec.lang,
533
+ });
534
+ break;
535
+ }
536
+ }
537
+ }
538
+ }
539
+ // CommonJS-style definition: a top-level `<target> = <function|class>`
540
+ // expression. Named after the assigned property (or identifier); only
541
+ // `exports.*` / `module.exports.*` targets count as exported — augmenting
542
+ // a local object (res.*, Foo.prototype.*) is not a module export.
543
+ if (spec.assignments && node.type === "expression_statement") {
544
+ const expr = node.namedChild(0);
545
+ if (expr?.type === "assignment_expression") {
546
+ const left = expr.childForFieldName("left");
547
+ const right = expr.childForFieldName("right");
548
+ if (left?.type === "member_expression" && left.text === "module.exports" && right) {
549
+ // `module.exports = { foo, bar: baz }` — a CJS export list: mark the
550
+ // shorthand names, keys and identifier values as exported (key = the
551
+ // exported surface, identifier value = the local declaration).
552
+ if (right.type === "object") {
553
+ for (let i = 0; i < right.namedChildCount; i++) {
554
+ const p = right.namedChild(i)!;
555
+ if (p.type === "shorthand_property_identifier") exportedNames.add(p.text);
556
+ else if (p.type === "pair") {
557
+ const k = p.childForFieldName("key");
558
+ const v = p.childForFieldName("value");
559
+ if (k?.type === "property_identifier") exportedNames.add(k.text);
560
+ if (v?.type === "identifier") exportedNames.add(v.text);
561
+ }
562
+ }
563
+ return;
564
+ }
565
+ // `module.exports = Foo;` — the CJS default export of a local decl.
566
+ if (right.type === "identifier") {
567
+ exportedNames.add(right.text);
568
+ return;
569
+ }
570
+ }
571
+ const funcy = right && ["function_expression", "function", "generator_function", "arrow_function", "class"].includes(right.type);
572
+ if (left && right && funcy) {
573
+ let name: string | undefined;
574
+ let exportedAssign = false;
575
+ if (left.type === "member_expression") {
576
+ const prop = left.childForFieldName("property");
577
+ if (prop?.type === "property_identifier") {
578
+ name = prop.text;
579
+ const obj = left.text.slice(0, left.text.length - prop.text.length - 1);
580
+ exportedAssign = obj === "exports" || obj === "module.exports";
581
+ }
582
+ } else if (left.type === "identifier") {
583
+ name = left.text;
584
+ }
585
+ if (name) {
586
+ symbols.push({
587
+ name,
588
+ kind: right.type === "class" ? "class" : "function",
589
+ file: rel,
590
+ line: expr.startPosition.row + 1,
591
+ endLine: expr.endPosition.row + 1,
592
+ ...(parent ? { parent } : {}),
593
+ signature: firstLine(expr),
594
+ exported: nowExported || exportedAssign,
595
+ lang: spec.lang,
596
+ });
597
+ return;
598
+ }
599
+ } else if (left?.type === "member_expression" && right) {
600
+ // CJS named VALUE export — `exports.foo = 42`, `exports.foo = bar`:
601
+ // emit `foo` as an exported const (ultradoc parity); when the RHS is
602
+ // a bare identifier, mark that local declaration exported too (and
603
+ // skip the emission when it would only duplicate the same name).
604
+ const prop = left.childForFieldName("property");
605
+ if (prop?.type === "property_identifier") {
606
+ const obj = left.text.slice(0, left.text.length - prop.text.length - 1);
607
+ if (obj === "exports" || obj === "module.exports") {
608
+ if (right.type === "identifier") exportedNames.add(right.text);
609
+ if (right.type !== "identifier" || right.text !== prop.text) {
610
+ symbols.push({
611
+ name: prop.text,
612
+ kind: "const",
613
+ file: rel,
614
+ line: expr.startPosition.row + 1,
615
+ endLine: expr.endPosition.row + 1,
616
+ ...(parent ? { parent } : {}),
617
+ signature: firstLine(expr),
618
+ exported: true,
619
+ lang: spec.lang,
620
+ });
621
+ }
622
+ return;
623
+ }
624
+ }
625
+ }
626
+ }
627
+ }
628
+ // Lua-flavored assignment definitions: `M.alias = function(z) … end` /
629
+ // `local alias = function(y) … end` — an assignment_statement pairs a
630
+ // `variable_list` of targets with an `expression_list` of values (fields
631
+ // name/value, index-aligned). Only function-valued targets become
632
+ // symbols, named after the full target text (dotted/colon names stay
633
+ // whole — regex-tier parity).
634
+ if (spec.assignments && node.type === "assignment_statement") {
635
+ const vars = node.children.find((c) => c.type === "variable_list");
636
+ const vals = node.children.find((c) => c.type === "expression_list");
637
+ const pairs = Math.min(vars?.namedChildCount ?? 0, vals?.namedChildCount ?? 0);
638
+ for (let i = 0; i < pairs; i++) {
639
+ const target = vars!.namedChild(i)!;
640
+ const value = vals!.namedChild(i)!;
641
+ if (value.type !== "function_definition" || !/^[\w.:]+$/.test(target.text)) continue;
642
+ symbols.push({
643
+ name: target.text,
644
+ kind: "function",
645
+ file: rel,
646
+ line: node.startPosition.row + 1,
647
+ endLine: node.endPosition.row + 1,
648
+ ...(parent ? { parent } : {}),
649
+ signature: firstLine(node),
650
+ exported: nowExported || spec.exported(firstLine(node), target.text),
651
+ lang: spec.lang,
652
+ });
653
+ }
654
+ return;
655
+ }
656
+ const kind = spec.defs[node.type];
657
+ if (kind) {
658
+ const name = nameOf(node);
659
+ if (name) {
660
+ const line = firstLine(node);
661
+ symbols.push({
662
+ name,
663
+ kind,
664
+ file: rel,
665
+ line: node.startPosition.row + 1,
666
+ endLine: node.endPosition.row + 1,
667
+ ...(parent ? { parent } : {}),
668
+ signature: line,
669
+ exported: nowExported || spec.exported(line, name),
670
+ lang: spec.lang,
671
+ });
672
+ // Recurse into this declaration's body for nested members (methods),
673
+ // scoping their parent to this symbol.
674
+ for (let i = 0; i < node.namedChildCount; i++) {
675
+ walkBody(node.namedChild(i)!, name, nowExported);
676
+ }
677
+ return;
678
+ }
679
+ }
680
+ if (spec.containers.has(node.type)) {
681
+ for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i)!, parent, nowExported);
682
+ }
683
+ };
684
+ // Recurse a declaration body one level: only container-ish children yield more
685
+ // members, so nested functions inside a method body are not surfaced (matches
686
+ // the "top-level + one level" contract).
687
+ const walkBody = (node: TSNode, parent: string, exported: boolean): void => {
688
+ if (spec.containers.has(node.type)) {
689
+ for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i)!, parent, exported);
690
+ }
691
+ };
692
+
693
+ walk(root, undefined, false);
694
+ if (exportedNames.size) {
695
+ for (const s of symbols) if (!s.exported && exportedNames.has(s.name)) s.exported = true;
696
+ }
697
+
698
+ const refs = collectImports(root, spec);
699
+ const idents = collectRefIdents(root, new Set(symbols.map((s) => s.name)));
700
+ const calls = collectCalls(root, spec);
701
+ const importedNames = collectImportedNames(root, spec);
702
+ let pkg: string | undefined;
703
+ if (spec.lang === "java") {
704
+ const p = findFirst(root, (n) => n.type === "package_declaration");
705
+ if (p) pkg = p.text.replace(/^package\s+/, "").replace(/;.*$/, "").trim();
706
+ }
707
+ return { symbols, refs, pkg, idents, calls, importedNames };
708
+ } catch {
709
+ return undefined; // any parse/walk failure → regex fallback
710
+ } finally {
711
+ tree?.delete(); // free wasm-side memory every file (not GC'd otherwise)
712
+ }
713
+ }