universal-ast-mapper 0.5.3 → 0.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 +31 -6
- package/dist/callgraph.js +266 -86
- package/dist/crosslang.js +312 -0
- package/dist/extractors/csharp.js +209 -0
- package/dist/extractors/java.js +152 -0
- package/dist/extractors/rust.js +249 -0
- package/dist/graph.js +77 -38
- package/dist/registry.js +18 -1
- package/dist/resolver.js +117 -60
- package/package.json +1 -1
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const TYPE_KINDS = new Set(["class", "interface", "enum", "struct"]);
|
|
4
|
+
/** Walk a symbol tree and yield each top-level type-like symbol. */
|
|
5
|
+
function topTypeSymbols(symbols) {
|
|
6
|
+
return symbols.filter((s) => TYPE_KINDS.has(s.kind));
|
|
7
|
+
}
|
|
8
|
+
function getDirectiveValue(skel, prefix) {
|
|
9
|
+
const hit = (skel.directives ?? []).find((d) => d.startsWith(prefix));
|
|
10
|
+
return hit ? hit.slice(prefix.length) : null;
|
|
11
|
+
}
|
|
12
|
+
function getAllDirectiveValues(skel, prefix) {
|
|
13
|
+
return (skel.directives ?? [])
|
|
14
|
+
.filter((d) => d.startsWith(prefix))
|
|
15
|
+
.map((d) => d.slice(prefix.length));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build Java + C# indexes from already-parsed skeletons.
|
|
19
|
+
* Cheap: O(symbols), no extra file reads.
|
|
20
|
+
*/
|
|
21
|
+
export function buildCrossLangIndex(skeletons) {
|
|
22
|
+
const index = {
|
|
23
|
+
javaFqcn: new Map(),
|
|
24
|
+
javaPackages: new Map(),
|
|
25
|
+
csharpNamespaces: new Map(),
|
|
26
|
+
csharpTypes: new Map(),
|
|
27
|
+
};
|
|
28
|
+
for (const skel of skeletons) {
|
|
29
|
+
if (skel.language === "java") {
|
|
30
|
+
const pkg = getDirectiveValue(skel, "package:");
|
|
31
|
+
if (!pkg)
|
|
32
|
+
continue;
|
|
33
|
+
const pkgFiles = index.javaPackages.get(pkg) ?? [];
|
|
34
|
+
pkgFiles.push(skel.file);
|
|
35
|
+
index.javaPackages.set(pkg, pkgFiles);
|
|
36
|
+
for (const sym of topTypeSymbols(skel.symbols)) {
|
|
37
|
+
index.javaFqcn.set(`${pkg}.${sym.name}`, skel.file);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (skel.language === "csharp") {
|
|
41
|
+
const namespaces = getAllDirectiveValues(skel, "namespace:");
|
|
42
|
+
for (const ns of namespaces) {
|
|
43
|
+
const arr = index.csharpNamespaces.get(ns) ?? [];
|
|
44
|
+
if (!arr.includes(skel.file))
|
|
45
|
+
arr.push(skel.file);
|
|
46
|
+
index.csharpNamespaces.set(ns, arr);
|
|
47
|
+
}
|
|
48
|
+
// Map every top-level type to <ns>.<TypeName>. For files with multiple
|
|
49
|
+
// namespaces this is approximate (we don't know per-symbol scoping
|
|
50
|
+
// without per-symbol namespace tracking) but accurate for the common case
|
|
51
|
+
// of one namespace per file.
|
|
52
|
+
for (const sym of topTypeSymbols(skel.symbols)) {
|
|
53
|
+
for (const ns of namespaces) {
|
|
54
|
+
index.csharpTypes.set(`${ns}.${sym.name}`, skel.file);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return index;
|
|
60
|
+
}
|
|
61
|
+
/* ─── Rust module resolution ──────────────────────────────────────────────── */
|
|
62
|
+
function findCargoRoot(fromAbs, projectRoot) {
|
|
63
|
+
let dir = path.dirname(fromAbs);
|
|
64
|
+
const stop = path.resolve(projectRoot);
|
|
65
|
+
while (true) {
|
|
66
|
+
if (fs.existsSync(path.join(dir, "Cargo.toml")))
|
|
67
|
+
return dir;
|
|
68
|
+
if (dir === stop || dir === path.dirname(dir))
|
|
69
|
+
return null;
|
|
70
|
+
dir = path.dirname(dir);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function existsFile(p) {
|
|
74
|
+
try {
|
|
75
|
+
return fs.statSync(p).isFile();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function existsDir(p) {
|
|
82
|
+
try {
|
|
83
|
+
return fs.statSync(p).isDirectory();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Walk module path segments down from a base dir, returning the resolved .rs file. */
|
|
90
|
+
function walkRustModule(base, segs) {
|
|
91
|
+
if (segs.length === 0) {
|
|
92
|
+
// base IS the target module dir/file. Try canonical roots.
|
|
93
|
+
for (const candidate of ["mod.rs", "lib.rs", "main.rs"]) {
|
|
94
|
+
const p = path.join(base, candidate);
|
|
95
|
+
if (existsFile(p))
|
|
96
|
+
return p;
|
|
97
|
+
}
|
|
98
|
+
if (existsFile(base + ".rs"))
|
|
99
|
+
return base + ".rs";
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const [head, ...rest] = segs;
|
|
103
|
+
if (rest.length === 0) {
|
|
104
|
+
// Terminal segment: `head.rs` or `head/mod.rs`.
|
|
105
|
+
const p1 = path.join(base, `${head}.rs`);
|
|
106
|
+
if (existsFile(p1))
|
|
107
|
+
return p1;
|
|
108
|
+
const p2 = path.join(base, head, "mod.rs");
|
|
109
|
+
if (existsFile(p2))
|
|
110
|
+
return p2;
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// Non-terminal: descend into a directory module.
|
|
114
|
+
const subDir = path.join(base, head);
|
|
115
|
+
if (existsDir(subDir))
|
|
116
|
+
return walkRustModule(subDir, rest);
|
|
117
|
+
// Rust-2018 style: `head.rs` next to a sibling `head/` directory.
|
|
118
|
+
if (existsFile(path.join(base, `${head}.rs`)) && existsDir(path.join(base, head))) {
|
|
119
|
+
return walkRustModule(path.join(base, head), rest);
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a Rust `use` path to an absolute .rs file.
|
|
125
|
+
* Handles `crate::`, `self::`, `super::` prefixes; anything else is treated
|
|
126
|
+
* as an external crate (e.g. `std::`, `tokio::`) and returns null.
|
|
127
|
+
*
|
|
128
|
+
* @param importFrom Full use path, e.g. "crate::foo::Bar"
|
|
129
|
+
* @param fromAbs Absolute path of the importing file
|
|
130
|
+
* @param projectRoot Absolute project root (security boundary)
|
|
131
|
+
*/
|
|
132
|
+
export function resolveRustModule(importFrom, fromAbs, projectRoot) {
|
|
133
|
+
const segs = importFrom.split("::");
|
|
134
|
+
if (segs.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
// Strip the imported item name (last segment) — leaves the module path.
|
|
137
|
+
const moduleSegs = segs.slice(0, -1);
|
|
138
|
+
if (moduleSegs.length === 0)
|
|
139
|
+
return null;
|
|
140
|
+
const head = moduleSegs[0];
|
|
141
|
+
let base;
|
|
142
|
+
let remaining;
|
|
143
|
+
if (head === "crate") {
|
|
144
|
+
const cargoRoot = findCargoRoot(fromAbs, projectRoot);
|
|
145
|
+
if (!cargoRoot)
|
|
146
|
+
return null;
|
|
147
|
+
base = path.join(cargoRoot, "src");
|
|
148
|
+
if (!existsDir(base))
|
|
149
|
+
base = cargoRoot;
|
|
150
|
+
remaining = moduleSegs.slice(1);
|
|
151
|
+
}
|
|
152
|
+
else if (head === "self") {
|
|
153
|
+
base = path.dirname(fromAbs);
|
|
154
|
+
remaining = moduleSegs.slice(1);
|
|
155
|
+
}
|
|
156
|
+
else if (head === "super") {
|
|
157
|
+
let dir = path.dirname(fromAbs);
|
|
158
|
+
let i = 0;
|
|
159
|
+
while (moduleSegs[i] === "super") {
|
|
160
|
+
dir = path.dirname(dir);
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
base = dir;
|
|
164
|
+
remaining = moduleSegs.slice(i);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
return null; // external crate
|
|
168
|
+
}
|
|
169
|
+
return walkRustModule(base, remaining);
|
|
170
|
+
}
|
|
171
|
+
// Cache per project root.
|
|
172
|
+
const goModuleCache = new Map();
|
|
173
|
+
function readGoModulePath(modFile) {
|
|
174
|
+
try {
|
|
175
|
+
const txt = fs.readFileSync(modFile, "utf8");
|
|
176
|
+
const m = /^\s*module\s+(\S+)/m.exec(txt);
|
|
177
|
+
return m ? m[1] : null;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Locate the go.mod ancestor (within projectRoot) of `fromAbs`. */
|
|
184
|
+
function findGoModule(fromAbs, projectRoot) {
|
|
185
|
+
const key = path.resolve(projectRoot);
|
|
186
|
+
if (goModuleCache.has(key)) {
|
|
187
|
+
// Cached at the project root level — assumes one go.mod per project root.
|
|
188
|
+
// For monorepos with multiple modules, ResolveGoImport falls back to a
|
|
189
|
+
// walk-up search below.
|
|
190
|
+
const cached = goModuleCache.get(key);
|
|
191
|
+
if (cached)
|
|
192
|
+
return cached;
|
|
193
|
+
}
|
|
194
|
+
let dir = path.dirname(fromAbs);
|
|
195
|
+
const stop = key;
|
|
196
|
+
while (true) {
|
|
197
|
+
const modFile = path.join(dir, "go.mod");
|
|
198
|
+
if (existsFile(modFile)) {
|
|
199
|
+
const modPath = readGoModulePath(modFile);
|
|
200
|
+
if (modPath) {
|
|
201
|
+
const result = { modulePath: modPath, moduleDir: dir };
|
|
202
|
+
if (dir === stop)
|
|
203
|
+
goModuleCache.set(key, result);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (dir === stop || dir === path.dirname(dir))
|
|
208
|
+
return null;
|
|
209
|
+
dir = path.dirname(dir);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Resolve a Go import path to a list of .go files in the resolved package
|
|
214
|
+
* directory. Returns null for stdlib / third-party / unresolvable paths.
|
|
215
|
+
*
|
|
216
|
+
* Go semantics:
|
|
217
|
+
* - A package is a directory containing one or more .go files.
|
|
218
|
+
* - `import "github.com/x/y/z"` maps to <moduleDir>/<subpath> when the prefix
|
|
219
|
+
* matches the current module's path.
|
|
220
|
+
*/
|
|
221
|
+
export function resolveGoImport(importFrom, fromAbs, projectRoot) {
|
|
222
|
+
const mod = findGoModule(fromAbs, projectRoot);
|
|
223
|
+
if (!mod)
|
|
224
|
+
return null;
|
|
225
|
+
let subPath;
|
|
226
|
+
if (importFrom === mod.modulePath) {
|
|
227
|
+
subPath = "";
|
|
228
|
+
}
|
|
229
|
+
else if (importFrom.startsWith(mod.modulePath + "/")) {
|
|
230
|
+
subPath = importFrom.slice(mod.modulePath.length + 1);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
return null; // external / stdlib
|
|
234
|
+
}
|
|
235
|
+
const pkgDir = subPath ? path.join(mod.moduleDir, subPath) : mod.moduleDir;
|
|
236
|
+
if (!existsDir(pkgDir))
|
|
237
|
+
return null;
|
|
238
|
+
// Collect every .go file in the directory (Go packages span all files in dir).
|
|
239
|
+
// Skip _test.go files — call graph cares about production code.
|
|
240
|
+
let entries;
|
|
241
|
+
try {
|
|
242
|
+
entries = fs.readdirSync(pkgDir, { withFileTypes: true });
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const files = [];
|
|
248
|
+
for (const e of entries) {
|
|
249
|
+
if (!e.isFile())
|
|
250
|
+
continue;
|
|
251
|
+
if (!e.name.endsWith(".go"))
|
|
252
|
+
continue;
|
|
253
|
+
if (e.name.endsWith("_test.go"))
|
|
254
|
+
continue;
|
|
255
|
+
const abs = path.join(pkgDir, e.name);
|
|
256
|
+
const rel = path.relative(projectRoot, abs).split(path.sep).join("/");
|
|
257
|
+
files.push(rel);
|
|
258
|
+
}
|
|
259
|
+
return files.length > 0 ? files : null;
|
|
260
|
+
}
|
|
261
|
+
/** Test/debug hook: drop the cached Go module info. */
|
|
262
|
+
export function clearGoModuleCache() {
|
|
263
|
+
goModuleCache.clear();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Resolve an ImportRef in a non-relative-path language to a graph target.
|
|
267
|
+
* Returns null for unresolvable / external imports.
|
|
268
|
+
*/
|
|
269
|
+
export function resolveCrossLangTarget(imp, skel, fromAbs, projectRoot, index) {
|
|
270
|
+
if (skel.language === "java") {
|
|
271
|
+
if (imp.symbol === "*") {
|
|
272
|
+
// wildcard: pull all files in the package
|
|
273
|
+
const files = index.javaPackages.get(imp.from);
|
|
274
|
+
if (files && files.length > 0)
|
|
275
|
+
return { kind: "file", files: files.slice() };
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const targetFile = index.javaFqcn.get(imp.from);
|
|
279
|
+
if (targetFile)
|
|
280
|
+
return { kind: "symbol", file: targetFile, symbol: imp.symbol };
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
if (skel.language === "csharp") {
|
|
284
|
+
const files = index.csharpNamespaces.get(imp.from);
|
|
285
|
+
if (files && files.length > 0) {
|
|
286
|
+
// Exclude self — a file `using App;` while declaring in App shouldn't self-edge.
|
|
287
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
288
|
+
if (filtered.length > 0)
|
|
289
|
+
return { kind: "file", files: filtered };
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (skel.language === "rust") {
|
|
294
|
+
const abs = resolveRustModule(imp.from, fromAbs, projectRoot);
|
|
295
|
+
if (!abs)
|
|
296
|
+
return null;
|
|
297
|
+
const rel = path.relative(projectRoot, abs).split(path.sep).join("/");
|
|
298
|
+
return { kind: "symbol", file: rel, symbol: imp.symbol };
|
|
299
|
+
}
|
|
300
|
+
if (skel.language === "go") {
|
|
301
|
+
const files = resolveGoImport(imp.from, fromAbs, projectRoot);
|
|
302
|
+
if (!files || files.length === 0)
|
|
303
|
+
return null;
|
|
304
|
+
// Exclude self (a file in package X importing X is unusual but possible
|
|
305
|
+
// for cyclic / generated cases — don't draw a self-edge).
|
|
306
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
307
|
+
if (filtered.length === 0)
|
|
308
|
+
return null;
|
|
309
|
+
return { kind: "file", files: filtered };
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { namedChildren, nameOf, headerSignature, leadingComment } from "../parser.js";
|
|
2
|
+
import { makeSymbol } from "./common.js";
|
|
3
|
+
/* ─── helpers ─────────────────────────────────────────────────────────────── */
|
|
4
|
+
function childOfType(node, type) {
|
|
5
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
6
|
+
const c = node.child(i);
|
|
7
|
+
if (c && c.type === type)
|
|
8
|
+
return c;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function modifiersText(node) {
|
|
13
|
+
let s = "";
|
|
14
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
15
|
+
const c = node.child(i);
|
|
16
|
+
if (c && c.type === "modifier")
|
|
17
|
+
s += c.text + " ";
|
|
18
|
+
}
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
function vis(node) {
|
|
22
|
+
return /\bpublic\b/.test(modifiersText(node)) ? "public" : "private";
|
|
23
|
+
}
|
|
24
|
+
function exported(node) {
|
|
25
|
+
return /\bpublic\b/.test(modifiersText(node));
|
|
26
|
+
}
|
|
27
|
+
function bodyLike(node) {
|
|
28
|
+
return (node.childForFieldName("body") ??
|
|
29
|
+
childOfType(node, "accessor_list") ??
|
|
30
|
+
childOfType(node, "arrow_expression_clause") ??
|
|
31
|
+
childOfType(node, "block"));
|
|
32
|
+
}
|
|
33
|
+
/* ─── directives (namespace declarations) ─────────────────────────────────── */
|
|
34
|
+
export function extractDirectivesCSharp(root, _source) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const visit = (node) => {
|
|
37
|
+
for (const c of namedChildren(node)) {
|
|
38
|
+
if (c.type === "namespace_declaration" || c.type === "file_scoped_namespace_declaration") {
|
|
39
|
+
const name = c.childForFieldName("name");
|
|
40
|
+
if (name)
|
|
41
|
+
out.push(`namespace:${name.text}`);
|
|
42
|
+
const body = c.childForFieldName("body");
|
|
43
|
+
if (body)
|
|
44
|
+
visit(body);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
visit(root);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/* ─── import extraction ───────────────────────────────────────────────────── */
|
|
52
|
+
export function extractImportsCSharp(root, _source) {
|
|
53
|
+
const out = [];
|
|
54
|
+
walkUsings(root, out);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function walkUsings(node, out) {
|
|
58
|
+
for (const child of namedChildren(node)) {
|
|
59
|
+
if (child.type === "using_directive") {
|
|
60
|
+
const isStatic = /\bstatic\b/.test(child.text);
|
|
61
|
+
const alias = childOfType(child, "name_equals");
|
|
62
|
+
const pathNode = childOfType(child, "qualified_name") ?? childOfType(child, "identifier");
|
|
63
|
+
const from = pathNode ? pathNode.text : child.text.replace(/^using\s+|;\s*$/g, "").trim();
|
|
64
|
+
const symbol = from.split(".").pop() ?? from;
|
|
65
|
+
const ref = { symbol, from, isNamespaceImport: !isStatic && !alias };
|
|
66
|
+
if (alias) {
|
|
67
|
+
const aliasId = childOfType(alias, "identifier");
|
|
68
|
+
if (aliasId)
|
|
69
|
+
ref.alias = aliasId.text;
|
|
70
|
+
}
|
|
71
|
+
out.push(ref);
|
|
72
|
+
}
|
|
73
|
+
else if (child.type === "namespace_declaration" ||
|
|
74
|
+
child.type === "file_scoped_namespace_declaration") {
|
|
75
|
+
const body = child.childForFieldName("body");
|
|
76
|
+
if (body)
|
|
77
|
+
walkUsings(body, out);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/* ─── symbol extraction ───────────────────────────────────────────────────── */
|
|
82
|
+
export function extractCSharp(root, _source) {
|
|
83
|
+
return collect(namedChildren(root));
|
|
84
|
+
}
|
|
85
|
+
function collect(nodes) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const n of nodes) {
|
|
88
|
+
const res = handle(n);
|
|
89
|
+
if (Array.isArray(res))
|
|
90
|
+
out.push(...res);
|
|
91
|
+
else if (res)
|
|
92
|
+
out.push(res);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function handle(node) {
|
|
97
|
+
switch (node.type) {
|
|
98
|
+
case "namespace_declaration":
|
|
99
|
+
case "file_scoped_namespace_declaration": {
|
|
100
|
+
const body = node.childForFieldName("body");
|
|
101
|
+
return body ? collect(namedChildren(body)) : null;
|
|
102
|
+
}
|
|
103
|
+
case "class_declaration":
|
|
104
|
+
case "record_declaration":
|
|
105
|
+
case "record_struct_declaration": {
|
|
106
|
+
const body = node.childForFieldName("body");
|
|
107
|
+
return makeSymbol({
|
|
108
|
+
name: nameOf(node) ?? "(class)",
|
|
109
|
+
kind: "class",
|
|
110
|
+
node,
|
|
111
|
+
rawKind: node.type,
|
|
112
|
+
visibility: vis(node),
|
|
113
|
+
exported: exported(node),
|
|
114
|
+
doc: leadingComment(node),
|
|
115
|
+
children: body ? collect(namedChildren(body)) : [],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
case "struct_declaration": {
|
|
119
|
+
const body = node.childForFieldName("body");
|
|
120
|
+
return makeSymbol({
|
|
121
|
+
name: nameOf(node) ?? "(struct)",
|
|
122
|
+
kind: "struct",
|
|
123
|
+
node,
|
|
124
|
+
rawKind: node.type,
|
|
125
|
+
visibility: vis(node),
|
|
126
|
+
exported: exported(node),
|
|
127
|
+
doc: leadingComment(node),
|
|
128
|
+
children: body ? collect(namedChildren(body)) : [],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
case "interface_declaration": {
|
|
132
|
+
const body = node.childForFieldName("body");
|
|
133
|
+
return makeSymbol({
|
|
134
|
+
name: nameOf(node) ?? "(interface)",
|
|
135
|
+
kind: "interface",
|
|
136
|
+
node,
|
|
137
|
+
rawKind: node.type,
|
|
138
|
+
visibility: vis(node),
|
|
139
|
+
exported: exported(node),
|
|
140
|
+
doc: leadingComment(node),
|
|
141
|
+
children: body ? collect(namedChildren(body)) : [],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
case "enum_declaration":
|
|
145
|
+
return makeSymbol({
|
|
146
|
+
name: nameOf(node) ?? "(enum)",
|
|
147
|
+
kind: "enum",
|
|
148
|
+
node,
|
|
149
|
+
rawKind: node.type,
|
|
150
|
+
visibility: vis(node),
|
|
151
|
+
exported: exported(node),
|
|
152
|
+
doc: leadingComment(node),
|
|
153
|
+
});
|
|
154
|
+
case "method_declaration":
|
|
155
|
+
case "constructor_declaration":
|
|
156
|
+
case "destructor_declaration":
|
|
157
|
+
case "operator_declaration":
|
|
158
|
+
return makeSymbol({
|
|
159
|
+
name: nameOf(node) ?? "(method)",
|
|
160
|
+
kind: "method",
|
|
161
|
+
node,
|
|
162
|
+
rawKind: node.type,
|
|
163
|
+
signature: headerSignature(node, bodyLike(node)),
|
|
164
|
+
visibility: vis(node),
|
|
165
|
+
exported: exported(node),
|
|
166
|
+
doc: leadingComment(node),
|
|
167
|
+
});
|
|
168
|
+
case "property_declaration":
|
|
169
|
+
return makeSymbol({
|
|
170
|
+
name: nameOf(node) ?? "(property)",
|
|
171
|
+
kind: "field",
|
|
172
|
+
node,
|
|
173
|
+
rawKind: node.type,
|
|
174
|
+
signature: headerSignature(node, bodyLike(node)),
|
|
175
|
+
visibility: vis(node),
|
|
176
|
+
exported: exported(node),
|
|
177
|
+
doc: leadingComment(node),
|
|
178
|
+
});
|
|
179
|
+
case "field_declaration":
|
|
180
|
+
return fieldDeclarators(node);
|
|
181
|
+
default:
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function fieldDeclarators(node) {
|
|
186
|
+
const m = modifiersText(node);
|
|
187
|
+
const kind = /\bconst\b/.test(m) || (/\bstatic\b/.test(m) && /\breadonly\b/.test(m)) ? "const" : "field";
|
|
188
|
+
const decl = childOfType(node, "variable_declaration");
|
|
189
|
+
if (!decl)
|
|
190
|
+
return [];
|
|
191
|
+
const out = [];
|
|
192
|
+
for (const d of namedChildren(decl)) {
|
|
193
|
+
if (d.type !== "variable_declarator")
|
|
194
|
+
continue;
|
|
195
|
+
const id = childOfType(d, "identifier") ?? d.namedChild(0);
|
|
196
|
+
if (!id)
|
|
197
|
+
continue;
|
|
198
|
+
out.push(makeSymbol({
|
|
199
|
+
name: id.text,
|
|
200
|
+
kind,
|
|
201
|
+
node: d,
|
|
202
|
+
rawKind: node.type,
|
|
203
|
+
signature: node.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
|
|
204
|
+
visibility: vis(node),
|
|
205
|
+
exported: exported(node),
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { namedChildren, nameOf, headerSignature, leadingComment } from "../parser.js";
|
|
2
|
+
import { makeSymbol } from "./common.js";
|
|
3
|
+
/* ─── helpers ─────────────────────────────────────────────────────────────── */
|
|
4
|
+
function childOfType(node, type) {
|
|
5
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
6
|
+
const c = node.child(i);
|
|
7
|
+
if (c && c.type === type)
|
|
8
|
+
return c;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function modifiersText(node) {
|
|
13
|
+
const m = childOfType(node, "modifiers");
|
|
14
|
+
return m ? m.text : "";
|
|
15
|
+
}
|
|
16
|
+
function vis(node) {
|
|
17
|
+
const m = modifiersText(node);
|
|
18
|
+
if (/\b(private|protected)\b/.test(m))
|
|
19
|
+
return "private";
|
|
20
|
+
return "public";
|
|
21
|
+
}
|
|
22
|
+
function exported(node) {
|
|
23
|
+
return /\bpublic\b/.test(modifiersText(node));
|
|
24
|
+
}
|
|
25
|
+
/* ─── directives (package declaration) ────────────────────────────────────── */
|
|
26
|
+
export function extractDirectivesJava(root, _source) {
|
|
27
|
+
for (const child of namedChildren(root)) {
|
|
28
|
+
if (child.type !== "package_declaration")
|
|
29
|
+
continue;
|
|
30
|
+
const id = childOfType(child, "scoped_identifier") ?? childOfType(child, "identifier");
|
|
31
|
+
if (id)
|
|
32
|
+
return [`package:${id.text}`];
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
/* ─── import extraction ───────────────────────────────────────────────────── */
|
|
37
|
+
export function extractImportsJava(root, _source) {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const child of namedChildren(root)) {
|
|
40
|
+
if (child.type !== "import_declaration")
|
|
41
|
+
continue;
|
|
42
|
+
const isStatic = /\bstatic\b/.test(child.text);
|
|
43
|
+
const isWildcard = /\.\s*\*/.test(child.text);
|
|
44
|
+
const pathNode = childOfType(child, "scoped_identifier") ?? childOfType(child, "identifier");
|
|
45
|
+
const from = pathNode ? pathNode.text : child.text.replace(/^import\s+|;\s*$/g, "").trim();
|
|
46
|
+
if (isWildcard) {
|
|
47
|
+
out.push({ symbol: "*", from, isNamespaceImport: true, isTypeOnly: !isStatic });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const symbol = from.split(".").pop() ?? from;
|
|
51
|
+
out.push({ symbol, from, isTypeOnly: !isStatic });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/* ─── symbol extraction ───────────────────────────────────────────────────── */
|
|
57
|
+
export function extractJava(root, _source) {
|
|
58
|
+
return collect(namedChildren(root));
|
|
59
|
+
}
|
|
60
|
+
function collect(nodes) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const n of nodes) {
|
|
63
|
+
const res = handle(n);
|
|
64
|
+
if (Array.isArray(res))
|
|
65
|
+
out.push(...res);
|
|
66
|
+
else if (res)
|
|
67
|
+
out.push(res);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function handle(node) {
|
|
72
|
+
switch (node.type) {
|
|
73
|
+
case "class_declaration":
|
|
74
|
+
case "record_declaration": {
|
|
75
|
+
const body = node.childForFieldName("body");
|
|
76
|
+
return makeSymbol({
|
|
77
|
+
name: nameOf(node) ?? "(class)",
|
|
78
|
+
kind: "class",
|
|
79
|
+
node,
|
|
80
|
+
rawKind: node.type,
|
|
81
|
+
visibility: vis(node),
|
|
82
|
+
exported: exported(node),
|
|
83
|
+
doc: leadingComment(node),
|
|
84
|
+
children: body ? collect(namedChildren(body)) : [],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
case "interface_declaration": {
|
|
88
|
+
const body = node.childForFieldName("body");
|
|
89
|
+
return makeSymbol({
|
|
90
|
+
name: nameOf(node) ?? "(interface)",
|
|
91
|
+
kind: "interface",
|
|
92
|
+
node,
|
|
93
|
+
rawKind: node.type,
|
|
94
|
+
visibility: vis(node),
|
|
95
|
+
exported: exported(node),
|
|
96
|
+
doc: leadingComment(node),
|
|
97
|
+
children: body ? collect(namedChildren(body)) : [],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
case "enum_declaration": {
|
|
101
|
+
const body = node.childForFieldName("body");
|
|
102
|
+
return makeSymbol({
|
|
103
|
+
name: nameOf(node) ?? "(enum)",
|
|
104
|
+
kind: "enum",
|
|
105
|
+
node,
|
|
106
|
+
rawKind: node.type,
|
|
107
|
+
visibility: vis(node),
|
|
108
|
+
exported: exported(node),
|
|
109
|
+
doc: leadingComment(node),
|
|
110
|
+
children: body ? collect(namedChildren(body).filter((c) => c.type !== "enum_constant")) : [],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
case "method_declaration":
|
|
114
|
+
case "constructor_declaration":
|
|
115
|
+
return makeSymbol({
|
|
116
|
+
name: nameOf(node) ?? "(method)",
|
|
117
|
+
kind: "method",
|
|
118
|
+
node,
|
|
119
|
+
rawKind: node.type,
|
|
120
|
+
signature: headerSignature(node, node.childForFieldName("body")),
|
|
121
|
+
visibility: vis(node),
|
|
122
|
+
exported: exported(node),
|
|
123
|
+
doc: leadingComment(node),
|
|
124
|
+
});
|
|
125
|
+
case "field_declaration":
|
|
126
|
+
return fieldDeclarators(node);
|
|
127
|
+
default:
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function fieldDeclarators(node) {
|
|
132
|
+
const m = modifiersText(node);
|
|
133
|
+
const kind = /\bstatic\b/.test(m) && /\bfinal\b/.test(m) ? "const" : "field";
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const decl of namedChildren(node)) {
|
|
136
|
+
if (decl.type !== "variable_declarator")
|
|
137
|
+
continue;
|
|
138
|
+
const name = nameOf(decl);
|
|
139
|
+
if (!name)
|
|
140
|
+
continue;
|
|
141
|
+
out.push(makeSymbol({
|
|
142
|
+
name,
|
|
143
|
+
kind,
|
|
144
|
+
node: decl,
|
|
145
|
+
rawKind: node.type,
|
|
146
|
+
signature: node.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
|
|
147
|
+
visibility: vis(node),
|
|
148
|
+
exported: exported(node),
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|