universal-ast-mapper 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +261 -12
- package/dist/ai-refactor.js +185 -0
- package/dist/ai-testgen.js +105 -0
- package/dist/analysis.js +134 -0
- package/dist/arch-rules.js +82 -0
- package/dist/callgraph.js +467 -0
- package/dist/check.js +112 -0
- package/dist/cli.js +2284 -0
- package/dist/complexity.js +98 -0
- package/dist/config.js +53 -0
- package/dist/contextpack.js +79 -0
- package/dist/coupling.js +35 -0
- package/dist/covmerge.js +176 -0
- package/dist/crosslang.js +425 -0
- package/dist/dashboard.js +259 -0
- package/dist/diagram.js +264 -0
- package/dist/diskcache.js +97 -0
- package/dist/docgen.js +156 -0
- package/dist/embeddings.js +136 -0
- package/dist/explain.js +123 -0
- package/dist/explorer.js +123 -0
- package/dist/extractors/c.js +204 -0
- package/dist/extractors/common.js +56 -0
- package/dist/extractors/cpp.js +272 -0
- package/dist/extractors/csharp.js +209 -0
- package/dist/extractors/go.js +212 -0
- package/dist/extractors/java.js +152 -0
- package/dist/extractors/kotlin.js +159 -0
- package/dist/extractors/php.js +208 -0
- package/dist/extractors/python.js +153 -0
- package/dist/extractors/ruby.js +146 -0
- package/dist/extractors/rust.js +249 -0
- package/dist/extractors/swift.js +192 -0
- package/dist/extractors/typescript.js +577 -0
- package/dist/fix.js +92 -0
- package/dist/gitdiff.js +178 -0
- package/dist/graph-analysis.js +279 -0
- package/dist/graph.js +165 -0
- package/dist/history.js +36 -0
- package/dist/html.js +658 -0
- package/dist/incremental.js +122 -0
- package/dist/index.js +1945 -0
- package/dist/indexstore.js +105 -0
- package/dist/layers.js +36 -0
- package/dist/lsp.js +238 -0
- package/dist/modulecoupling.js +0 -0
- package/dist/parser.js +84 -0
- package/dist/patch.js +199 -0
- package/dist/plugins.js +88 -0
- package/dist/pool.js +114 -0
- package/dist/prompts.js +67 -0
- package/dist/registry.js +87 -0
- package/dist/report.js +441 -0
- package/dist/resolver.js +222 -0
- package/dist/roots.js +47 -0
- package/dist/search.js +68 -0
- package/dist/security.js +178 -0
- package/dist/semantic.js +365 -0
- package/dist/serve.js +328 -0
- package/dist/sfc.js +27 -0
- package/dist/similar.js +98 -0
- package/dist/skeleton.js +132 -0
- package/dist/smells.js +285 -0
- package/dist/sourcemap.js +60 -0
- package/dist/testgen.js +280 -0
- package/dist/testmap.js +167 -0
- package/dist/tsconfig.js +212 -0
- package/dist/typeflow.js +124 -0
- package/dist/types.js +5 -0
- package/dist/unused-params.js +127 -0
- package/dist/webapp.js +646 -0
- package/dist/worker.js +27 -0
- package/dist/workspace.js +330 -0
- package/package.json +2 -1
|
@@ -0,0 +1,425 @@
|
|
|
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
|
+
kotlinFqcn: new Map(),
|
|
28
|
+
kotlinPackages: new Map(),
|
|
29
|
+
swiftModules: new Map(),
|
|
30
|
+
};
|
|
31
|
+
for (const skel of skeletons) {
|
|
32
|
+
if (skel.language === "java") {
|
|
33
|
+
const pkg = getDirectiveValue(skel, "package:");
|
|
34
|
+
if (!pkg)
|
|
35
|
+
continue;
|
|
36
|
+
const pkgFiles = index.javaPackages.get(pkg) ?? [];
|
|
37
|
+
pkgFiles.push(skel.file);
|
|
38
|
+
index.javaPackages.set(pkg, pkgFiles);
|
|
39
|
+
for (const sym of topTypeSymbols(skel.symbols)) {
|
|
40
|
+
index.javaFqcn.set(`${pkg}.${sym.name}`, skel.file);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (skel.language === "csharp") {
|
|
44
|
+
const namespaces = getAllDirectiveValues(skel, "namespace:");
|
|
45
|
+
for (const ns of namespaces) {
|
|
46
|
+
const arr = index.csharpNamespaces.get(ns) ?? [];
|
|
47
|
+
if (!arr.includes(skel.file))
|
|
48
|
+
arr.push(skel.file);
|
|
49
|
+
index.csharpNamespaces.set(ns, arr);
|
|
50
|
+
}
|
|
51
|
+
// Map every top-level type to <ns>.<TypeName>. For files with multiple
|
|
52
|
+
// namespaces this is approximate (we don't know per-symbol scoping
|
|
53
|
+
// without per-symbol namespace tracking) but accurate for the common case
|
|
54
|
+
// of one namespace per file.
|
|
55
|
+
for (const sym of topTypeSymbols(skel.symbols)) {
|
|
56
|
+
for (const ns of namespaces) {
|
|
57
|
+
index.csharpTypes.set(`${ns}.${sym.name}`, skel.file);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (skel.language === "kotlin") {
|
|
62
|
+
const pkg = getDirectiveValue(skel, "package:");
|
|
63
|
+
if (!pkg)
|
|
64
|
+
continue;
|
|
65
|
+
const pkgFiles = index.kotlinPackages.get(pkg) ?? [];
|
|
66
|
+
pkgFiles.push(skel.file);
|
|
67
|
+
index.kotlinPackages.set(pkg, pkgFiles);
|
|
68
|
+
for (const sym of topTypeSymbols(skel.symbols)) {
|
|
69
|
+
index.kotlinFqcn.set(`${pkg}.${sym.name}`, skel.file);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (skel.language === "swift") {
|
|
73
|
+
const mod = swiftModuleOf(skel.file);
|
|
74
|
+
if (!mod)
|
|
75
|
+
continue;
|
|
76
|
+
const arr = index.swiftModules.get(mod) ?? [];
|
|
77
|
+
arr.push(skel.file);
|
|
78
|
+
index.swiftModules.set(mod, arr);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return index;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Derive a Swift module name from a project-relative file path.
|
|
85
|
+
* SwiftPM convention: sources live under `Sources/<ModuleName>/...`, so the
|
|
86
|
+
* module is the path segment right after `Sources`. For flat layouts we fall
|
|
87
|
+
* back to the immediate parent directory name. Returns null for bare files.
|
|
88
|
+
*/
|
|
89
|
+
function swiftModuleOf(relFile) {
|
|
90
|
+
const parts = relFile.split("/");
|
|
91
|
+
const srcIdx = parts.lastIndexOf("Sources");
|
|
92
|
+
if (srcIdx >= 0 && srcIdx + 1 < parts.length - 1)
|
|
93
|
+
return parts[srcIdx + 1];
|
|
94
|
+
if (parts.length >= 2)
|
|
95
|
+
return parts[parts.length - 2];
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/* ─── Rust module resolution ──────────────────────────────────────────────── */
|
|
99
|
+
function findCargoRoot(fromAbs, projectRoot) {
|
|
100
|
+
let dir = path.dirname(fromAbs);
|
|
101
|
+
const stop = path.resolve(projectRoot);
|
|
102
|
+
while (true) {
|
|
103
|
+
if (fs.existsSync(path.join(dir, "Cargo.toml")))
|
|
104
|
+
return dir;
|
|
105
|
+
if (dir === stop || dir === path.dirname(dir))
|
|
106
|
+
return null;
|
|
107
|
+
dir = path.dirname(dir);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function existsFile(p) {
|
|
111
|
+
try {
|
|
112
|
+
return fs.statSync(p).isFile();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function existsDir(p) {
|
|
119
|
+
try {
|
|
120
|
+
return fs.statSync(p).isDirectory();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Walk module path segments down from a base dir, returning the resolved .rs file. */
|
|
127
|
+
function walkRustModule(base, segs) {
|
|
128
|
+
if (segs.length === 0) {
|
|
129
|
+
// base IS the target module dir/file. Try canonical roots.
|
|
130
|
+
for (const candidate of ["mod.rs", "lib.rs", "main.rs"]) {
|
|
131
|
+
const p = path.join(base, candidate);
|
|
132
|
+
if (existsFile(p))
|
|
133
|
+
return p;
|
|
134
|
+
}
|
|
135
|
+
if (existsFile(base + ".rs"))
|
|
136
|
+
return base + ".rs";
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const [head, ...rest] = segs;
|
|
140
|
+
if (rest.length === 0) {
|
|
141
|
+
// Terminal segment: `head.rs` or `head/mod.rs`.
|
|
142
|
+
const p1 = path.join(base, `${head}.rs`);
|
|
143
|
+
if (existsFile(p1))
|
|
144
|
+
return p1;
|
|
145
|
+
const p2 = path.join(base, head, "mod.rs");
|
|
146
|
+
if (existsFile(p2))
|
|
147
|
+
return p2;
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// Non-terminal: descend into a directory module.
|
|
151
|
+
const subDir = path.join(base, head);
|
|
152
|
+
if (existsDir(subDir))
|
|
153
|
+
return walkRustModule(subDir, rest);
|
|
154
|
+
// Rust-2018 style: `head.rs` next to a sibling `head/` directory.
|
|
155
|
+
if (existsFile(path.join(base, `${head}.rs`)) && existsDir(path.join(base, head))) {
|
|
156
|
+
return walkRustModule(path.join(base, head), rest);
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve a Rust `use` path to an absolute .rs file.
|
|
162
|
+
* Handles `crate::`, `self::`, `super::` prefixes; anything else is treated
|
|
163
|
+
* as an external crate (e.g. `std::`, `tokio::`) and returns null.
|
|
164
|
+
*
|
|
165
|
+
* @param importFrom Full use path, e.g. "crate::foo::Bar"
|
|
166
|
+
* @param fromAbs Absolute path of the importing file
|
|
167
|
+
* @param projectRoot Absolute project root (security boundary)
|
|
168
|
+
*/
|
|
169
|
+
export function resolveRustModule(importFrom, fromAbs, projectRoot) {
|
|
170
|
+
const segs = importFrom.split("::");
|
|
171
|
+
if (segs.length === 0)
|
|
172
|
+
return null;
|
|
173
|
+
// Strip the imported item name (last segment) — leaves the module path.
|
|
174
|
+
const moduleSegs = segs.slice(0, -1);
|
|
175
|
+
if (moduleSegs.length === 0)
|
|
176
|
+
return null;
|
|
177
|
+
const head = moduleSegs[0];
|
|
178
|
+
let base;
|
|
179
|
+
let remaining;
|
|
180
|
+
if (head === "crate") {
|
|
181
|
+
const cargoRoot = findCargoRoot(fromAbs, projectRoot);
|
|
182
|
+
if (!cargoRoot)
|
|
183
|
+
return null;
|
|
184
|
+
base = path.join(cargoRoot, "src");
|
|
185
|
+
if (!existsDir(base))
|
|
186
|
+
base = cargoRoot;
|
|
187
|
+
remaining = moduleSegs.slice(1);
|
|
188
|
+
}
|
|
189
|
+
else if (head === "self") {
|
|
190
|
+
base = path.dirname(fromAbs);
|
|
191
|
+
remaining = moduleSegs.slice(1);
|
|
192
|
+
}
|
|
193
|
+
else if (head === "super") {
|
|
194
|
+
let dir = path.dirname(fromAbs);
|
|
195
|
+
let i = 0;
|
|
196
|
+
while (moduleSegs[i] === "super") {
|
|
197
|
+
dir = path.dirname(dir);
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
base = dir;
|
|
201
|
+
remaining = moduleSegs.slice(i);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
return null; // external crate
|
|
205
|
+
}
|
|
206
|
+
return walkRustModule(base, remaining);
|
|
207
|
+
}
|
|
208
|
+
// Cache per project root.
|
|
209
|
+
const goModuleCache = new Map();
|
|
210
|
+
function readGoModulePath(modFile) {
|
|
211
|
+
try {
|
|
212
|
+
const txt = fs.readFileSync(modFile, "utf8");
|
|
213
|
+
const m = /^\s*module\s+(\S+)/m.exec(txt);
|
|
214
|
+
return m ? m[1] : null;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Locate the go.mod ancestor (within projectRoot) of `fromAbs`. */
|
|
221
|
+
function findGoModule(fromAbs, projectRoot) {
|
|
222
|
+
const key = path.resolve(projectRoot);
|
|
223
|
+
if (goModuleCache.has(key)) {
|
|
224
|
+
// Cached at the project root level — assumes one go.mod per project root.
|
|
225
|
+
// For monorepos with multiple modules, ResolveGoImport falls back to a
|
|
226
|
+
// walk-up search below.
|
|
227
|
+
const cached = goModuleCache.get(key);
|
|
228
|
+
if (cached)
|
|
229
|
+
return cached;
|
|
230
|
+
}
|
|
231
|
+
let dir = path.dirname(fromAbs);
|
|
232
|
+
const stop = key;
|
|
233
|
+
while (true) {
|
|
234
|
+
const modFile = path.join(dir, "go.mod");
|
|
235
|
+
if (existsFile(modFile)) {
|
|
236
|
+
const modPath = readGoModulePath(modFile);
|
|
237
|
+
if (modPath) {
|
|
238
|
+
const result = { modulePath: modPath, moduleDir: dir };
|
|
239
|
+
if (dir === stop)
|
|
240
|
+
goModuleCache.set(key, result);
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (dir === stop || dir === path.dirname(dir))
|
|
245
|
+
return null;
|
|
246
|
+
dir = path.dirname(dir);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Resolve a Go import path to a list of .go files in the resolved package
|
|
251
|
+
* directory. Returns null for stdlib / third-party / unresolvable paths.
|
|
252
|
+
*
|
|
253
|
+
* Go semantics:
|
|
254
|
+
* - A package is a directory containing one or more .go files.
|
|
255
|
+
* - `import "github.com/x/y/z"` maps to <moduleDir>/<subpath> when the prefix
|
|
256
|
+
* matches the current module's path.
|
|
257
|
+
*/
|
|
258
|
+
export function resolveGoImport(importFrom, fromAbs, projectRoot) {
|
|
259
|
+
const mod = findGoModule(fromAbs, projectRoot);
|
|
260
|
+
if (!mod)
|
|
261
|
+
return null;
|
|
262
|
+
let subPath;
|
|
263
|
+
if (importFrom === mod.modulePath) {
|
|
264
|
+
subPath = "";
|
|
265
|
+
}
|
|
266
|
+
else if (importFrom.startsWith(mod.modulePath + "/")) {
|
|
267
|
+
subPath = importFrom.slice(mod.modulePath.length + 1);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
return null; // external / stdlib
|
|
271
|
+
}
|
|
272
|
+
const pkgDir = subPath ? path.join(mod.moduleDir, subPath) : mod.moduleDir;
|
|
273
|
+
if (!existsDir(pkgDir))
|
|
274
|
+
return null;
|
|
275
|
+
// Collect every .go file in the directory (Go packages span all files in dir).
|
|
276
|
+
// Skip _test.go files — call graph cares about production code.
|
|
277
|
+
let entries;
|
|
278
|
+
try {
|
|
279
|
+
entries = fs.readdirSync(pkgDir, { withFileTypes: true });
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const files = [];
|
|
285
|
+
for (const e of entries) {
|
|
286
|
+
if (!e.isFile())
|
|
287
|
+
continue;
|
|
288
|
+
if (!e.name.endsWith(".go"))
|
|
289
|
+
continue;
|
|
290
|
+
if (e.name.endsWith("_test.go"))
|
|
291
|
+
continue;
|
|
292
|
+
const abs = path.join(pkgDir, e.name);
|
|
293
|
+
const rel = path.relative(projectRoot, abs).split(path.sep).join("/");
|
|
294
|
+
files.push(rel);
|
|
295
|
+
}
|
|
296
|
+
return files.length > 0 ? files : null;
|
|
297
|
+
}
|
|
298
|
+
/** Test/debug hook: drop the cached Go module info. */
|
|
299
|
+
export function clearGoModuleCache() {
|
|
300
|
+
goModuleCache.clear();
|
|
301
|
+
}
|
|
302
|
+
/* ─── C / C++ #include resolution ─────────────────────────────────────────── */
|
|
303
|
+
const HEADER_EXTS = [".h", ".hpp", ".hxx", ".hh"];
|
|
304
|
+
const IMPL_EXTS = [".c", ".cpp", ".cc", ".cxx"];
|
|
305
|
+
/**
|
|
306
|
+
* Resolve a C/C++ `#include "foo.h"` to in-project files.
|
|
307
|
+
* Convention: also pair foo.h with foo.c/.cpp in the same directory so the
|
|
308
|
+
* graph captures the header → impl relationship.
|
|
309
|
+
* `#include <foo.h>` (system headers) returns null (external).
|
|
310
|
+
*/
|
|
311
|
+
export function resolveCInclude(importFrom, fromAbs, projectRoot) {
|
|
312
|
+
// System headers like stdio.h, vector, etc. — leave to external.
|
|
313
|
+
const isSystemHeader = !importFrom.includes("/") && !importFrom.includes(".") ||
|
|
314
|
+
/^(stdio|stdlib|string|vector|memory|cstdint|cstdlib|cstring|iostream)/.test(importFrom);
|
|
315
|
+
// We only check the actual filesystem; if a system header happens to exist
|
|
316
|
+
// locally we still link it, otherwise it falls through to null.
|
|
317
|
+
const fromDir = path.dirname(fromAbs);
|
|
318
|
+
const headerAbs = path.resolve(fromDir, importFrom);
|
|
319
|
+
const out = [];
|
|
320
|
+
if (existsFile(headerAbs)) {
|
|
321
|
+
const rel = path.relative(projectRoot, headerAbs).split(path.sep).join("/");
|
|
322
|
+
// Reject paths that escape the project root.
|
|
323
|
+
if (!rel.startsWith(".."))
|
|
324
|
+
out.push(rel);
|
|
325
|
+
// Pair foo.h with foo.{c,cpp,cc,cxx} in the same directory.
|
|
326
|
+
const ext = path.extname(headerAbs).toLowerCase();
|
|
327
|
+
if (HEADER_EXTS.includes(ext)) {
|
|
328
|
+
const base = headerAbs.slice(0, -ext.length);
|
|
329
|
+
for (const implExt of IMPL_EXTS) {
|
|
330
|
+
const implAbs = base + implExt;
|
|
331
|
+
if (existsFile(implAbs)) {
|
|
332
|
+
const implRel = path.relative(projectRoot, implAbs).split(path.sep).join("/");
|
|
333
|
+
if (!implRel.startsWith(".."))
|
|
334
|
+
out.push(implRel);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (isSystemHeader && out.length === 0)
|
|
340
|
+
return null;
|
|
341
|
+
return out.length > 0 ? out : null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Resolve an ImportRef in a non-relative-path language to a graph target.
|
|
345
|
+
* Returns null for unresolvable / external imports.
|
|
346
|
+
*/
|
|
347
|
+
export function resolveCrossLangTarget(imp, skel, fromAbs, projectRoot, index) {
|
|
348
|
+
if (skel.language === "java") {
|
|
349
|
+
if (imp.symbol === "*") {
|
|
350
|
+
// wildcard: pull all files in the package
|
|
351
|
+
const files = index.javaPackages.get(imp.from);
|
|
352
|
+
if (files && files.length > 0)
|
|
353
|
+
return { kind: "file", files: files.slice() };
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const targetFile = index.javaFqcn.get(imp.from);
|
|
357
|
+
if (targetFile)
|
|
358
|
+
return { kind: "symbol", file: targetFile, symbol: imp.symbol };
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
if (skel.language === "csharp") {
|
|
362
|
+
const files = index.csharpNamespaces.get(imp.from);
|
|
363
|
+
if (files && files.length > 0) {
|
|
364
|
+
// Exclude self — a file `using App;` while declaring in App shouldn't self-edge.
|
|
365
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
366
|
+
if (filtered.length > 0)
|
|
367
|
+
return { kind: "file", files: filtered };
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
if (skel.language === "rust") {
|
|
372
|
+
const abs = resolveRustModule(imp.from, fromAbs, projectRoot);
|
|
373
|
+
if (!abs)
|
|
374
|
+
return null;
|
|
375
|
+
const rel = path.relative(projectRoot, abs).split(path.sep).join("/");
|
|
376
|
+
return { kind: "symbol", file: rel, symbol: imp.symbol };
|
|
377
|
+
}
|
|
378
|
+
if (skel.language === "go") {
|
|
379
|
+
const files = resolveGoImport(imp.from, fromAbs, projectRoot);
|
|
380
|
+
if (!files || files.length === 0)
|
|
381
|
+
return null;
|
|
382
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
383
|
+
if (filtered.length === 0)
|
|
384
|
+
return null;
|
|
385
|
+
return { kind: "file", files: filtered };
|
|
386
|
+
}
|
|
387
|
+
if (skel.language === "kotlin") {
|
|
388
|
+
if (imp.symbol === "*") {
|
|
389
|
+
const files = index.kotlinPackages.get(imp.from);
|
|
390
|
+
if (files && files.length > 0) {
|
|
391
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
392
|
+
if (filtered.length > 0)
|
|
393
|
+
return { kind: "file", files: filtered };
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const targetFile = index.kotlinFqcn.get(imp.from);
|
|
398
|
+
if (targetFile && targetFile !== skel.file) {
|
|
399
|
+
return { kind: "symbol", file: targetFile, symbol: imp.symbol };
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
if (skel.language === "c" || skel.language === "cpp") {
|
|
404
|
+
const files = resolveCInclude(imp.from, fromAbs, projectRoot);
|
|
405
|
+
if (!files || files.length === 0)
|
|
406
|
+
return null;
|
|
407
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
408
|
+
if (filtered.length === 0)
|
|
409
|
+
return null;
|
|
410
|
+
return { kind: "file", files: filtered };
|
|
411
|
+
}
|
|
412
|
+
if (skel.language === "swift") {
|
|
413
|
+
// `import <Module>` brings in another in-project module's public symbols
|
|
414
|
+
// (no symbol named). Resolve to that module's files; unknown modules
|
|
415
|
+
// (Foundation, UIKit, …) are external.
|
|
416
|
+
const files = index.swiftModules.get(imp.from);
|
|
417
|
+
if (files && files.length > 0) {
|
|
418
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
419
|
+
if (filtered.length > 0)
|
|
420
|
+
return { kind: "file", files: filtered };
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
function safeJson(obj) {
|
|
2
|
+
return JSON.stringify(obj)
|
|
3
|
+
.replace(/</g, "\\u003c")
|
|
4
|
+
.replace(/>/g, "\\u003e")
|
|
5
|
+
.replace(/&/g, "\\u0026");
|
|
6
|
+
}
|
|
7
|
+
function flattenSymbols(skeletons) {
|
|
8
|
+
const out = [];
|
|
9
|
+
const walk = (syms, file) => {
|
|
10
|
+
for (const s of syms) {
|
|
11
|
+
out.push({ name: s.name, kind: s.kind, file, startLine: s.range.startLine, endLine: s.range.endLine, exported: s.exported ?? false });
|
|
12
|
+
if (s.children.length)
|
|
13
|
+
walk(s.children, file);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
for (const sk of skeletons)
|
|
17
|
+
walk(sk.symbols, sk.file);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
const KIND_COLORS = {
|
|
21
|
+
class: "#7c3aed", interface: "#0ea5e9", struct: "#0d9488",
|
|
22
|
+
function: "#2563eb", method: "#4f46e5", type: "#db2777",
|
|
23
|
+
enum: "#ea580c", const: "#65a30d", var: "#ca8a04",
|
|
24
|
+
field: "#64748b", namespace: "#9333ea",
|
|
25
|
+
};
|
|
26
|
+
export function buildDashboardHtml(reportHtml, skeletonHtml, explorerHtml, skeletons, title, liveReloadPort) {
|
|
27
|
+
const symbols = flattenSymbols(skeletons);
|
|
28
|
+
const totalSymbols = skeletons.reduce((n, s) => n + s.symbolCount, 0);
|
|
29
|
+
const tabsData = safeJson({ overview: reportHtml, files: skeletonHtml, graph: explorerHtml });
|
|
30
|
+
const symData = safeJson(symbols);
|
|
31
|
+
const kindColorsData = safeJson(KIND_COLORS);
|
|
32
|
+
const liveReloadScript = liveReloadPort
|
|
33
|
+
? `<script>
|
|
34
|
+
(function(){
|
|
35
|
+
var es=new EventSource('http://localhost:${liveReloadPort ?? 0}/events');
|
|
36
|
+
es.addEventListener('reload',function(){location.reload();});
|
|
37
|
+
es.onerror=function(){setTimeout(function(){location.reload();},2000);};
|
|
38
|
+
})();
|
|
39
|
+
</script>`
|
|
40
|
+
: "";
|
|
41
|
+
return `<!doctype html>
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="utf-8">
|
|
45
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
46
|
+
<title>AST Map Dashboard — ${title}</title>
|
|
47
|
+
<style>
|
|
48
|
+
:root{color-scheme:light dark;--bg:#0d1117;--bg2:#161b22;--fg:#e6edf3;--fg2:#7d8590;--bdr:#21262d;--accent:#6366f1;--accent2:#8b5cf6;--tab-h:44px;}
|
|
49
|
+
@media(prefers-color-scheme:light){:root{--bg:#f6f8fa;--bg2:#fff;--fg:#0f172a;--fg2:#64748b;--bdr:#e2e8f0;}}
|
|
50
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
51
|
+
body{font:13px/1.5 ui-sans-serif,system-ui,sans-serif;background:var(--bg);color:var(--fg);display:flex;flex-direction:column;height:100vh;overflow:hidden;}
|
|
52
|
+
/* Topbar */
|
|
53
|
+
.topbar{display:flex;align-items:center;gap:10px;padding:0 16px;height:48px;background:var(--bg2);border-bottom:1px solid var(--bdr);flex-shrink:0;box-shadow:0 1px 3px rgba(0,0,0,.15);}
|
|
54
|
+
.topbar-logo{display:flex;align-items:center;gap:7px;font-weight:700;font-size:14px;color:var(--accent);text-decoration:none;}
|
|
55
|
+
.topbar-sep{width:1px;height:18px;background:var(--bdr);}
|
|
56
|
+
.topbar-title{font-size:13px;font-weight:600;}
|
|
57
|
+
.topbar-meta{font-size:11px;color:var(--fg2);flex:1;}
|
|
58
|
+
.live-dot{width:7px;height:7px;border-radius:50%;background:#22c55e;display:${liveReloadPort ? "inline-block" : "none"};animation:pulse 2s infinite;}
|
|
59
|
+
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.4;}}
|
|
60
|
+
/* Tabs */
|
|
61
|
+
.tabs{display:flex;align-items:stretch;background:var(--bg2);border-bottom:1px solid var(--bdr);flex-shrink:0;height:var(--tab-h);padding:0 12px;gap:2px;}
|
|
62
|
+
.tab-btn{font:13px/1 ui-sans-serif,sans-serif;cursor:pointer;border:none;background:transparent;color:var(--fg2);padding:0 14px;border-bottom:2px solid transparent;transition:color .15s,border-color .15s;display:flex;align-items:center;gap:6px;font-weight:500;}
|
|
63
|
+
.tab-btn:hover{color:var(--fg);}
|
|
64
|
+
.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600;}
|
|
65
|
+
.tab-icon{font-size:14px;}
|
|
66
|
+
/* Content */
|
|
67
|
+
.tab-content{flex:1;display:none;min-height:0;}
|
|
68
|
+
.tab-content.active{display:flex;flex-direction:column;}
|
|
69
|
+
iframe{border:none;flex:1;width:100%;height:100%;}
|
|
70
|
+
/* Symbols tab */
|
|
71
|
+
.sym-pane{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden;}
|
|
72
|
+
.sym-toolbar{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid var(--bdr);background:var(--bg2);flex-shrink:0;flex-wrap:wrap;}
|
|
73
|
+
.sym-search{font:12px ui-monospace,monospace;padding:5px 10px;border:1px solid var(--bdr);border-radius:8px;background:var(--bg);color:var(--fg);outline:none;width:220px;transition:border-color .15s;}
|
|
74
|
+
.sym-search:focus{border-color:var(--accent);}
|
|
75
|
+
.kind-sel{font:12px ui-sans-serif,sans-serif;padding:4px 8px;border:1px solid var(--bdr);border-radius:8px;background:var(--bg);color:var(--fg);outline:none;cursor:pointer;}
|
|
76
|
+
.sym-count{font-size:11px;color:var(--fg2);margin-left:auto;}
|
|
77
|
+
.sym-table-wrap{flex:1;overflow-y:auto;scrollbar-width:thin;}
|
|
78
|
+
table{width:100%;border-collapse:collapse;font-size:12px;}
|
|
79
|
+
th{position:sticky;top:0;background:var(--bg2);border-bottom:2px solid var(--bdr);padding:7px 12px;text-align:left;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--fg2);cursor:pointer;user-select:none;white-space:nowrap;}
|
|
80
|
+
th:hover{color:var(--fg);}
|
|
81
|
+
th .sort-arrow{opacity:.5;margin-left:4px;}
|
|
82
|
+
td{padding:5px 12px;border-bottom:1px solid var(--bdr);vertical-align:middle;}
|
|
83
|
+
tr:hover td{background:color-mix(in srgb,var(--accent) 4%,var(--bg));}
|
|
84
|
+
.mono{font-family:ui-monospace,monospace;font-weight:600;}
|
|
85
|
+
.kind-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:999px;letter-spacing:.04em;}
|
|
86
|
+
.exp-badge{font-size:10px;color:#16a34a;background:#dcfce7;padding:1px 6px;border-radius:5px;}
|
|
87
|
+
.file-cell{color:var(--fg2);font-size:11px;font-family:ui-monospace,monospace;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
88
|
+
.line-cell{color:var(--fg2);font-family:ui-monospace,monospace;font-size:11px;white-space:nowrap;}
|
|
89
|
+
.no-results{padding:48px;text-align:center;color:var(--fg2);}
|
|
90
|
+
@media(prefers-color-scheme:dark){.exp-badge{background:#14532d;color:#4ade80;}}
|
|
91
|
+
</style>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
${liveReloadScript}
|
|
95
|
+
<header class="topbar">
|
|
96
|
+
<div class="topbar-logo">
|
|
97
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
|
98
|
+
AST Map
|
|
99
|
+
</div>
|
|
100
|
+
<div class="topbar-sep"></div>
|
|
101
|
+
<span class="topbar-title">${title}</span>
|
|
102
|
+
<span class="topbar-meta">${skeletons.length} files · ${totalSymbols} symbols</span>
|
|
103
|
+
<span class="live-dot" title="Live reload active"></span>
|
|
104
|
+
</header>
|
|
105
|
+
|
|
106
|
+
<nav class="tabs">
|
|
107
|
+
<button class="tab-btn active" data-tab="overview"><span class="tab-icon">📊</span>Overview</button>
|
|
108
|
+
<button class="tab-btn" data-tab="files"><span class="tab-icon">📁</span>Files</button>
|
|
109
|
+
<button class="tab-btn" data-tab="graph"><span class="tab-icon">🕸</span>Dependencies</button>
|
|
110
|
+
<button class="tab-btn" data-tab="symbols"><span class="tab-icon">⬡</span>Symbols</button>
|
|
111
|
+
</nav>
|
|
112
|
+
|
|
113
|
+
<div id="tc-overview" class="tab-content active">
|
|
114
|
+
<iframe id="fr-overview" title="Overview"></iframe>
|
|
115
|
+
</div>
|
|
116
|
+
<div id="tc-files" class="tab-content">
|
|
117
|
+
<iframe id="fr-files" title="Files"></iframe>
|
|
118
|
+
</div>
|
|
119
|
+
<div id="tc-graph" class="tab-content">
|
|
120
|
+
<iframe id="fr-graph" title="Dependencies"></iframe>
|
|
121
|
+
</div>
|
|
122
|
+
<div id="tc-symbols" class="tab-content">
|
|
123
|
+
<div class="sym-pane">
|
|
124
|
+
<div class="sym-toolbar">
|
|
125
|
+
<input class="sym-search" id="sym-q" type="search" placeholder="Search symbols…" autocomplete="off">
|
|
126
|
+
<select class="kind-sel" id="kind-filter">
|
|
127
|
+
<option value="">All kinds</option>
|
|
128
|
+
<option>class</option><option>interface</option><option>struct</option>
|
|
129
|
+
<option>function</option><option>method</option><option>type</option>
|
|
130
|
+
<option>enum</option><option>const</option><option>var</option>
|
|
131
|
+
<option>field</option><option>namespace</option>
|
|
132
|
+
</select>
|
|
133
|
+
<label style="font-size:11px;color:var(--fg2);display:flex;align-items:center;gap:5px;">
|
|
134
|
+
<input type="checkbox" id="exp-only"> Exported only
|
|
135
|
+
</label>
|
|
136
|
+
<span class="sym-count" id="sym-count"></span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="sym-table-wrap">
|
|
139
|
+
<table>
|
|
140
|
+
<thead>
|
|
141
|
+
<tr>
|
|
142
|
+
<th data-col="name">Symbol <span class="sort-arrow" id="sa-name"></span></th>
|
|
143
|
+
<th data-col="kind">Kind <span class="sort-arrow" id="sa-kind"></span></th>
|
|
144
|
+
<th data-col="file">File <span class="sort-arrow" id="sa-file"></span></th>
|
|
145
|
+
<th data-col="startLine">Line <span class="sort-arrow" id="sa-startLine"></span></th>
|
|
146
|
+
<th>Export</th>
|
|
147
|
+
</tr>
|
|
148
|
+
</thead>
|
|
149
|
+
<tbody id="sym-tbody"></tbody>
|
|
150
|
+
</table>
|
|
151
|
+
<div id="sym-empty" class="no-results" style="display:none">No symbols match.</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<script>
|
|
157
|
+
(function(){
|
|
158
|
+
'use strict';
|
|
159
|
+
const TABS=${tabsData};
|
|
160
|
+
const SYMS=${symData};
|
|
161
|
+
const KIND_COLORS=${kindColorsData};
|
|
162
|
+
|
|
163
|
+
// ── Tab switching ───────────────────────────────────────────
|
|
164
|
+
const loaded={};
|
|
165
|
+
|
|
166
|
+
function showTab(name){
|
|
167
|
+
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.toggle('active',b.dataset.tab===name));
|
|
168
|
+
document.querySelectorAll('.tab-content').forEach(tc=>tc.classList.toggle('active',tc.id==='tc-'+name));
|
|
169
|
+
if(name!=='symbols'){
|
|
170
|
+
if(!loaded[name]){
|
|
171
|
+
const fr=document.getElementById('fr-'+name);
|
|
172
|
+
if(fr){fr.srcdoc=TABS[name]||'';loaded[name]=true;}
|
|
173
|
+
}
|
|
174
|
+
}else{
|
|
175
|
+
renderSymbols();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
document.querySelectorAll('.tab-btn').forEach(btn=>{
|
|
180
|
+
btn.addEventListener('click',()=>showTab(btn.dataset.tab));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Load overview immediately
|
|
184
|
+
showTab('overview');
|
|
185
|
+
|
|
186
|
+
// ── Symbols table ───────────────────────────────────────────
|
|
187
|
+
let sortCol='name',sortAsc=true,filtered=[];
|
|
188
|
+
|
|
189
|
+
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
190
|
+
|
|
191
|
+
function renderSymbols(){
|
|
192
|
+
const q=document.getElementById('sym-q').value.trim().toLowerCase();
|
|
193
|
+
const kind=document.getElementById('kind-filter').value;
|
|
194
|
+
const expOnly=document.getElementById('exp-only').checked;
|
|
195
|
+
|
|
196
|
+
filtered=SYMS.filter(s=>{
|
|
197
|
+
if(kind&&s.kind!==kind)return false;
|
|
198
|
+
if(expOnly&&!s.exported)return false;
|
|
199
|
+
if(q&&!s.name.toLowerCase().includes(q)&&!s.file.toLowerCase().includes(q))return false;
|
|
200
|
+
return true;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
filtered.sort((a,b)=>{
|
|
204
|
+
const va=String(a[sortCol]),vb=String(b[sortCol]);
|
|
205
|
+
if(sortCol==='startLine')return sortAsc?(a.startLine-b.startLine):(b.startLine-a.startLine);
|
|
206
|
+
return sortAsc?va.localeCompare(vb):vb.localeCompare(va);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const tbody=document.getElementById('sym-tbody');
|
|
210
|
+
const c=KIND_COLORS;
|
|
211
|
+
tbody.innerHTML=filtered.slice(0,500).map(s=>{
|
|
212
|
+
const col=c[s.kind]||'#64748b';
|
|
213
|
+
return \`<tr>
|
|
214
|
+
<td class="mono">\${esc(s.name)}</td>
|
|
215
|
+
<td><span class="kind-badge" style="background:\${col}1a;color:\${col};border:1px solid \${col}44">\${s.kind}</span></td>
|
|
216
|
+
<td class="file-cell" title="\${esc(s.file)}">\${esc(s.file)}</td>
|
|
217
|
+
<td class="line-cell">L\${s.startLine}</td>
|
|
218
|
+
<td>\${s.exported?'<span class="exp-badge">exp</span>':''}</td>
|
|
219
|
+
</tr>\`;
|
|
220
|
+
}).join('');
|
|
221
|
+
|
|
222
|
+
const extra=filtered.length>500?' (showing 500 of '+filtered.length+')'+'':'';
|
|
223
|
+
document.getElementById('sym-count').textContent=filtered.length+' symbol(s)'+extra;
|
|
224
|
+
document.getElementById('sym-empty').style.display=filtered.length?'none':'block';
|
|
225
|
+
tbody.style.display=filtered.length?'':'none';
|
|
226
|
+
|
|
227
|
+
// Update sort arrows
|
|
228
|
+
['name','kind','file','startLine'].forEach(col=>{
|
|
229
|
+
const el=document.getElementById('sa-'+col);
|
|
230
|
+
if(el)el.textContent=col===sortCol?(sortAsc?'↑':'↓'):'';
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
document.getElementById('sym-q').addEventListener('input',renderSymbols);
|
|
235
|
+
document.getElementById('kind-filter').addEventListener('change',renderSymbols);
|
|
236
|
+
document.getElementById('exp-only').addEventListener('change',renderSymbols);
|
|
237
|
+
document.querySelectorAll('th[data-col]').forEach(th=>{
|
|
238
|
+
th.addEventListener('click',()=>{
|
|
239
|
+
const col=th.dataset.col;
|
|
240
|
+
if(sortCol===col)sortAsc=!sortAsc;else{sortCol=col;sortAsc=true;}
|
|
241
|
+
renderSymbols();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Keyboard shortcut
|
|
246
|
+
document.addEventListener('keydown',ev=>{
|
|
247
|
+
if(ev.key==='/'&&document.activeElement?.tagName!=='INPUT'&&document.activeElement?.tagName!=='SELECT'){
|
|
248
|
+
const q=document.getElementById('sym-q');
|
|
249
|
+
if(document.getElementById('tc-symbols').classList.contains('active')){
|
|
250
|
+
ev.preventDefault();q.focus();q.select();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
})();
|
|
256
|
+
</script>
|
|
257
|
+
</body>
|
|
258
|
+
</html>`;
|
|
259
|
+
}
|