@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.
- package/README.md +115 -0
- package/docs/MIGRATION.md +100 -0
- package/package.json +89 -0
- package/scripts/cli.mjs +10 -0
- package/scripts/engine.d.mts +618 -0
- package/scripts/engine.mjs +10655 -0
- package/scripts/grammars/bash.wasm +0 -0
- package/scripts/grammars/c.wasm +0 -0
- package/scripts/grammars/c_sharp.wasm +0 -0
- package/scripts/grammars/cpp.wasm +0 -0
- package/scripts/grammars/go.wasm +0 -0
- package/scripts/grammars/java.wasm +0 -0
- package/scripts/grammars/javascript.wasm +0 -0
- package/scripts/grammars/lua.wasm +0 -0
- package/scripts/grammars/php.wasm +0 -0
- package/scripts/grammars/python.wasm +0 -0
- package/scripts/grammars/ruby.wasm +0 -0
- package/scripts/grammars/rust.wasm +0 -0
- package/scripts/grammars/scala.wasm +0 -0
- package/scripts/grammars/tsx.wasm +0 -0
- package/scripts/grammars/typescript.wasm +0 -0
- package/scripts/grammars/web-tree-sitter.wasm +0 -0
- package/src/ast/extract.ts +713 -0
- package/src/ast/loader.ts +104 -0
- package/src/bm25.ts +156 -0
- package/src/callers.ts +0 -0
- package/src/calls.ts +131 -0
- package/src/categorize.ts +80 -0
- package/src/centrality.ts +148 -0
- package/src/classify.ts +53 -0
- package/src/cli-entry.ts +16 -0
- package/src/community.ts +345 -0
- package/src/complexity.ts +69 -0
- package/src/coupling.ts +0 -0
- package/src/deadcode.ts +46 -0
- package/src/edit.ts +93 -0
- package/src/engine-cli.ts +278 -0
- package/src/engine.ts +148 -0
- package/src/extract/code.ts +376 -0
- package/src/extract/markdown.ts +135 -0
- package/src/git.ts +205 -0
- package/src/glob.ts +56 -0
- package/src/graph.ts +0 -0
- package/src/grep.ts +115 -0
- package/src/hash.ts +13 -0
- package/src/ignore.ts +134 -0
- package/src/lang/c.ts +26 -0
- package/src/lang/common.ts +68 -0
- package/src/lang/csharp.ts +22 -0
- package/src/lang/elixir.ts +18 -0
- package/src/lang/go.ts +22 -0
- package/src/lang/java.ts +19 -0
- package/src/lang/js-ts.ts +112 -0
- package/src/lang/kotlin.ts +20 -0
- package/src/lang/lua.ts +18 -0
- package/src/lang/php.ts +24 -0
- package/src/lang/python.ts +22 -0
- package/src/lang/registry.ts +54 -0
- package/src/lang/ruby.ts +17 -0
- package/src/lang/rust.ts +22 -0
- package/src/lang/scala.ts +19 -0
- package/src/lang/shell.ts +17 -0
- package/src/lang/swift.ts +23 -0
- package/src/mcp.ts +512 -0
- package/src/memory.ts +75 -0
- package/src/modules.ts +128 -0
- package/src/pipeline.ts +59 -0
- package/src/query.ts +118 -0
- package/src/render/graph-json.ts +16 -0
- package/src/render/symbols-json.ts +79 -0
- package/src/repomap.ts +52 -0
- package/src/resolve.ts +950 -0
- package/src/rules.ts +249 -0
- package/src/scan.ts +172 -0
- package/src/sort.ts +12 -0
- package/src/surprise.ts +58 -0
- package/src/tests-map.ts +105 -0
- package/src/types.ts +201 -0
- package/src/util.ts +169 -0
- package/src/viz.ts +44 -0
- package/src/walk.ts +216 -0
- 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
|
+
}
|