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
package/dist/report.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { collectSourceFiles } from "./skeleton.js";
|
|
3
|
+
import { buildSkeletonsBulk } from "./pool.js";
|
|
4
|
+
import { resolveOptions } from "./config.js";
|
|
5
|
+
import { buildSymbolGraph } from "./graph.js";
|
|
6
|
+
import { findDeadExports, findCircularDeps, getTopSymbols } from "./graph-analysis.js";
|
|
7
|
+
import { findLayerViolations } from "./layers.js";
|
|
8
|
+
import { computeModuleCoupling } from "./modulecoupling.js";
|
|
9
|
+
import { mapTestCoverage, isTestFile, isFixtureFile } from "./testmap.js";
|
|
10
|
+
function gradeFor(score) {
|
|
11
|
+
if (score >= 90)
|
|
12
|
+
return "A";
|
|
13
|
+
if (score >= 80)
|
|
14
|
+
return "B";
|
|
15
|
+
if (score >= 70)
|
|
16
|
+
return "C";
|
|
17
|
+
if (score >= 60)
|
|
18
|
+
return "D";
|
|
19
|
+
return "F";
|
|
20
|
+
}
|
|
21
|
+
export async function buildReport(absDir, root) {
|
|
22
|
+
const opts = resolveOptions({ detail: "outline", emitHtml: false });
|
|
23
|
+
const files = collectSourceFiles(absDir, opts);
|
|
24
|
+
const skeletons = [];
|
|
25
|
+
const langCount = new Map();
|
|
26
|
+
let symbolCount = 0;
|
|
27
|
+
const hotspots = [];
|
|
28
|
+
let cxSum = 0, cxN = 0, cxMax = 0;
|
|
29
|
+
const items = files.map((file) => ({
|
|
30
|
+
abs: file,
|
|
31
|
+
rel: path.relative(root, file).split(path.sep).join("/"),
|
|
32
|
+
}));
|
|
33
|
+
const built = await buildSkeletonsBulk(items, opts, true);
|
|
34
|
+
for (let i = 0; i < built.length; i++) {
|
|
35
|
+
const r = built[i];
|
|
36
|
+
if (!r)
|
|
37
|
+
continue; // skip unparsable
|
|
38
|
+
const rel = items[i].rel;
|
|
39
|
+
skeletons.push(r.skel);
|
|
40
|
+
symbolCount += r.skel.symbolCount;
|
|
41
|
+
langCount.set(r.skel.language, (langCount.get(r.skel.language) ?? 0) + 1);
|
|
42
|
+
if (r.complexity) {
|
|
43
|
+
for (const f of r.complexity.functions) {
|
|
44
|
+
hotspots.push({ ...f, file: rel });
|
|
45
|
+
cxSum += f.complexity;
|
|
46
|
+
cxN++;
|
|
47
|
+
cxMax = Math.max(cxMax, f.complexity);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const graph = buildSymbolGraph(skeletons, root);
|
|
52
|
+
const dead = findDeadExports(graph).filter((d) => d.confidence === "high");
|
|
53
|
+
const cycles = findCircularDeps(graph);
|
|
54
|
+
const god = getTopSymbols(graph, 8);
|
|
55
|
+
const layerViolations = findLayerViolations(graph);
|
|
56
|
+
const modules = computeModuleCoupling(graph).modules;
|
|
57
|
+
// Test coverage. If the scanned dir has no test files (common when reporting
|
|
58
|
+
// on src/ only), pull test files in from the project root so the map can
|
|
59
|
+
// still pair them with the scanned sources.
|
|
60
|
+
let covGraph = graph;
|
|
61
|
+
let rootFallback = false;
|
|
62
|
+
if (!skeletons.some((s) => isTestFile(s.file)) && path.resolve(absDir) !== path.resolve(root)) {
|
|
63
|
+
const have = new Set(items.map((i) => i.abs));
|
|
64
|
+
const testItems = collectSourceFiles(root, opts)
|
|
65
|
+
.filter((f) => !have.has(f))
|
|
66
|
+
.map((f) => ({ abs: f, rel: path.relative(root, f).split(path.sep).join("/") }))
|
|
67
|
+
.filter((i) => isTestFile(i.rel) && !isFixtureFile(i.rel));
|
|
68
|
+
if (testItems.length > 0) {
|
|
69
|
+
const builtTests = await buildSkeletonsBulk(testItems, opts);
|
|
70
|
+
const testSkels = builtTests.filter((r) => r !== null).map((r) => r.skel);
|
|
71
|
+
if (testSkels.length > 0) {
|
|
72
|
+
covGraph = buildSymbolGraph([...skeletons, ...testSkels], root);
|
|
73
|
+
rootFallback = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const cov = mapTestCoverage(covGraph);
|
|
78
|
+
hotspots.sort((a, b) => b.complexity - a.complexity);
|
|
79
|
+
const veryHigh = hotspots.filter((f) => f.complexity > 20).length;
|
|
80
|
+
const high = hotspots.filter((f) => f.complexity > 10 && f.complexity <= 20).length;
|
|
81
|
+
// Health score: start at 100, subtract weighted penalties.
|
|
82
|
+
let score = 100;
|
|
83
|
+
score -= Math.min(20, dead.length * 1.5);
|
|
84
|
+
score -= Math.min(22, cycles.length * 6);
|
|
85
|
+
score -= Math.min(28, veryHigh * 4 + high * 1);
|
|
86
|
+
score -= Math.min(12, god.filter((g) => g.importCount >= 8).length * 4);
|
|
87
|
+
score -= Math.min(10, layerViolations.length);
|
|
88
|
+
score -= Math.min(8, Math.round((1 - cov.coverageRatio) * 8)); // structural test coverage
|
|
89
|
+
score = Math.max(0, Math.round(score));
|
|
90
|
+
const languages = [...langCount.entries()]
|
|
91
|
+
.map(([lang, f]) => ({ lang, files: f }))
|
|
92
|
+
.sort((a, b) => b.files - a.files);
|
|
93
|
+
return {
|
|
94
|
+
project: absDir.split(/[\\/]/).filter(Boolean).pop() || "project",
|
|
95
|
+
generatedAt: new Date().toISOString(),
|
|
96
|
+
fileCount: skeletons.length,
|
|
97
|
+
symbolCount,
|
|
98
|
+
edgeCount: graph.edges.filter((e) => e.edgeType === "imports").length,
|
|
99
|
+
languages,
|
|
100
|
+
score,
|
|
101
|
+
grade: gradeFor(score),
|
|
102
|
+
dead: { count: dead.length, items: dead.slice(0, 25).map((d) => ({ file: d.file, symbol: d.symbol, kind: d.kind })) },
|
|
103
|
+
cycles: { count: cycles.length, items: cycles.slice(0, 12).map((c) => c.cycle) },
|
|
104
|
+
godNodes: god.map((g) => ({ symbol: g.symbol, file: g.file, importCount: g.importCount })),
|
|
105
|
+
complexity: { average: cxN ? Math.round((cxSum / cxN) * 10) / 10 : 0, max: cxMax, hotspots: hotspots.slice(0, 12) },
|
|
106
|
+
layerViolations: { count: layerViolations.length, items: layerViolations.slice(0, 12) },
|
|
107
|
+
modules: modules.slice(0, 10),
|
|
108
|
+
testCoverage: {
|
|
109
|
+
testFiles: cov.testFiles,
|
|
110
|
+
sourceFiles: cov.sourceFiles,
|
|
111
|
+
testedSources: cov.testedSources,
|
|
112
|
+
coverageRatio: cov.coverageRatio,
|
|
113
|
+
untestedCount: cov.untestedSources,
|
|
114
|
+
untested: cov.untested.slice(0, 12),
|
|
115
|
+
rootFallback,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/* ─── HTML dashboard ────────────────────────────────────────────────────────── */
|
|
120
|
+
const GRADE_COLOR = {
|
|
121
|
+
A: "#1d9e75", B: "#22c55e", C: "#ba7517", D: "#d85a30", F: "#e24b4a",
|
|
122
|
+
};
|
|
123
|
+
const GRADE_BG = {
|
|
124
|
+
A: "#dcfce7", B: "#dcfce7", C: "#fef9c3", D: "#ffedd5", F: "#fee2e2",
|
|
125
|
+
};
|
|
126
|
+
function esc(s) {
|
|
127
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
128
|
+
}
|
|
129
|
+
function ratingColor(r) {
|
|
130
|
+
return r === "very-high" ? "#e24b4a" : r === "high" ? "#d85a30" : r === "moderate" ? "#ba7517" : "#1d9e75";
|
|
131
|
+
}
|
|
132
|
+
function instColor(i) {
|
|
133
|
+
return i >= 0.8 ? "#e24b4a" : i <= 0.2 ? "#1d9e75" : "#ba7517";
|
|
134
|
+
}
|
|
135
|
+
function scoreRing(score, grade) {
|
|
136
|
+
const gc = GRADE_COLOR[grade] ?? "#888";
|
|
137
|
+
const r = 42, circ = 2 * Math.PI * r;
|
|
138
|
+
const dash = (score / 100) * circ;
|
|
139
|
+
return `<div class="score-ring-wrap">
|
|
140
|
+
<svg width="110" height="110" viewBox="0 0 110 110">
|
|
141
|
+
<circle cx="55" cy="55" r="${r}" fill="none" stroke="currentColor" stroke-width="9" opacity=".1"/>
|
|
142
|
+
<circle cx="55" cy="55" r="${r}" fill="none" stroke="${gc}" stroke-width="9"
|
|
143
|
+
stroke-dasharray="${dash.toFixed(1)} ${circ.toFixed(1)}"
|
|
144
|
+
stroke-linecap="round" transform="rotate(-90 55 55)"
|
|
145
|
+
style="transition:stroke-dasharray 1s ease"/>
|
|
146
|
+
<text x="55" y="50" text-anchor="middle" font-size="28" font-weight="700" fill="${gc}" font-family="system-ui,sans-serif">${grade}</text>
|
|
147
|
+
<text x="55" y="66" text-anchor="middle" font-size="12" fill="currentColor" opacity=".6" font-family="system-ui,sans-serif">${score}/100</text>
|
|
148
|
+
</svg>
|
|
149
|
+
</div>`;
|
|
150
|
+
}
|
|
151
|
+
function statCard(label, value, accent, sub) {
|
|
152
|
+
const accent_attr = accent ? ` style="color:${accent}"` : "";
|
|
153
|
+
const sub_html = sub ? `<div class="sl-sub">${sub}</div>` : "";
|
|
154
|
+
return `<div class="stat"><div class="sv"${accent_attr}>${value}</div><div class="sl">${label}</div>${sub_html}</div>`;
|
|
155
|
+
}
|
|
156
|
+
function bar(label, value, max, color, right, title) {
|
|
157
|
+
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
158
|
+
const titleAttr = title ? ` title="${esc(title)}"` : "";
|
|
159
|
+
return `<div class="row"${titleAttr}><div class="rl">${esc(label)}</div><div class="track"><div class="fill" style="width:${pct}%;background:${color}"></div></div><div class="rr">${right}</div></div>`;
|
|
160
|
+
}
|
|
161
|
+
function collapsibleCard(id, title, content, icon, open = true) {
|
|
162
|
+
return `<div class="card" id="card-${id}">
|
|
163
|
+
<div class="card-header" onclick="toggleCard('${id}')">
|
|
164
|
+
<span class="card-icon">${icon}</span>
|
|
165
|
+
<h2>${title}</h2>
|
|
166
|
+
<span class="card-arrow" id="arr-${id}">${open ? "▾" : "▸"}</span>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="card-body" id="body-${id}" style="${open ? "" : "display:none"}">
|
|
169
|
+
${content}
|
|
170
|
+
</div>
|
|
171
|
+
</div>`;
|
|
172
|
+
}
|
|
173
|
+
export function buildReportHtml(d, history = []) {
|
|
174
|
+
const gc = GRADE_COLOR[d.grade] ?? "#888";
|
|
175
|
+
const prev = history.length >= 2 ? history[history.length - 2] : null;
|
|
176
|
+
const scoreDelta = prev ? d.score - prev.score : null;
|
|
177
|
+
const deltaBadge = scoreDelta === null ? ""
|
|
178
|
+
: scoreDelta > 0 ? `<span class="delta up">↑ +${scoreDelta}</span>`
|
|
179
|
+
: scoreDelta < 0 ? `<span class="delta dn">↓ ${scoreDelta}</span>`
|
|
180
|
+
: `<span class="delta neu">→ 0</span>`;
|
|
181
|
+
const maxLang = d.languages[0]?.files ?? 1;
|
|
182
|
+
const langs = d.languages.map((l) => bar(l.lang, l.files, maxLang, "#534ab7", `${l.files}`)).join("");
|
|
183
|
+
const maxCx = d.complexity.hotspots[0]?.complexity ?? 1;
|
|
184
|
+
const hotspots = d.complexity.hotspots.length
|
|
185
|
+
? d.complexity.hotspots.map((h) => bar(`${h.name} · ${h.file}`, h.complexity, maxCx, ratingColor(h.rating), `<b>${h.complexity}</b>`, `${h.name} in ${h.file} — complexity ${h.complexity}`)).join("")
|
|
186
|
+
: `<div class="empty">No functions found.</div>`;
|
|
187
|
+
const god = d.godNodes.length
|
|
188
|
+
? d.godNodes.map((g) => `<div class="li">
|
|
189
|
+
<span class="kbadge">god</span>
|
|
190
|
+
<span class="mono">${esc(g.symbol)}</span>
|
|
191
|
+
<span class="dim">${esc(g.file)}</span>
|
|
192
|
+
<span class="pill pill-warn">${g.importCount} importers</span>
|
|
193
|
+
</div>`).join("")
|
|
194
|
+
: `<div class="ok">✓ No dominant god nodes</div>`;
|
|
195
|
+
const dead = d.dead.count
|
|
196
|
+
? d.dead.items.map((x) => `<div class="li">
|
|
197
|
+
<span class="kbadge">${esc(x.kind)}</span>
|
|
198
|
+
<span class="mono">${esc(x.symbol)}</span>
|
|
199
|
+
<span class="dim">${esc(x.file)}</span>
|
|
200
|
+
</div>`).join("")
|
|
201
|
+
+ (d.dead.count > d.dead.items.length ? `<div class="more">+${d.dead.count - d.dead.items.length} more…</div>` : "")
|
|
202
|
+
: `<div class="ok">✓ No high-confidence dead exports</div>`;
|
|
203
|
+
const cycles = d.cycles.count
|
|
204
|
+
? d.cycles.items.map((c) => `<div class="li cycle-li">
|
|
205
|
+
<span class="cycle-arrow">↻</span>
|
|
206
|
+
<span class="mono cycle-chain">${esc(c.join(" → "))}</span>
|
|
207
|
+
</div>`).join("")
|
|
208
|
+
: `<div class="ok">✓ No circular dependencies</div>`;
|
|
209
|
+
const modules = d.modules.length
|
|
210
|
+
? d.modules.map((m) => bar(`${m.module} · ${m.files} file(s)`, m.instability, 1, instColor(m.instability), `Ca ${m.afferent} · Ce ${m.efferent} · <b>I ${m.instability.toFixed(2)}</b>`)).join("")
|
|
211
|
+
: `<div class="empty">No cross-module imports.</div>`;
|
|
212
|
+
const covPct = Math.round(d.testCoverage.coverageRatio * 100);
|
|
213
|
+
const covC = d.testCoverage.coverageRatio >= 0.7 ? "#1d9e75" : d.testCoverage.coverageRatio >= 0.4 ? "#ba7517" : "#e24b4a";
|
|
214
|
+
const covRing = d.testCoverage.testFiles > 0 ? (() => {
|
|
215
|
+
const r = 28, circ = 2 * Math.PI * r;
|
|
216
|
+
const dash = (covPct / 100) * circ;
|
|
217
|
+
return `<div class="cov-ring-wrap">
|
|
218
|
+
<svg width="72" height="72" viewBox="0 0 72 72">
|
|
219
|
+
<circle cx="36" cy="36" r="${r}" fill="none" stroke="currentColor" stroke-width="7" opacity=".12"/>
|
|
220
|
+
<circle cx="36" cy="36" r="${r}" fill="none" stroke="${covC}" stroke-width="7"
|
|
221
|
+
stroke-dasharray="${dash.toFixed(1)} ${circ.toFixed(1)}"
|
|
222
|
+
stroke-linecap="round" transform="rotate(-90 36 36)"/>
|
|
223
|
+
<text x="36" y="40" text-anchor="middle" font-size="14" font-weight="700" fill="${covC}" font-family="system-ui,sans-serif">${covPct}%</text>
|
|
224
|
+
</svg>
|
|
225
|
+
</div>`;
|
|
226
|
+
})() : "";
|
|
227
|
+
const covSummary = d.testCoverage.testFiles > 0
|
|
228
|
+
? `<div class="cov-summary">
|
|
229
|
+
${covRing}
|
|
230
|
+
<div class="cov-text">
|
|
231
|
+
<div class="cov-pct" style="color:${covC}">${covPct}% covered</div>
|
|
232
|
+
<div class="cov-detail">${d.testCoverage.testedSources}/${d.testCoverage.sourceFiles} source files tested · ${d.testCoverage.testFiles} test file(s)${d.testCoverage.rootFallback ? " (from project root)" : ""}</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>` : "";
|
|
235
|
+
const covList = d.testCoverage.testFiles === 0
|
|
236
|
+
? `<div class="empty">No test files found in the scanned directory or project root.</div>`
|
|
237
|
+
: d.testCoverage.untested.length === 0
|
|
238
|
+
? `<div class="ok">✓ Every source file has at least one test</div>`
|
|
239
|
+
: `<div class="untested-header">Untested files (by risk)</div>`
|
|
240
|
+
+ d.testCoverage.untested.map((u) => `<div class="li"><span class="mono">${esc(u.file)}</span><span class="dim">${u.symbols} symbol(s)</span><span class="pill">Ca ${u.afferent}</span></div>`).join("")
|
|
241
|
+
+ (d.testCoverage.untestedCount > d.testCoverage.untested.length
|
|
242
|
+
? `<div class="more">+${d.testCoverage.untestedCount - d.testCoverage.untested.length} more untested…</div>` : "");
|
|
243
|
+
const sdp = d.layerViolations.count
|
|
244
|
+
? d.layerViolations.items.map((v) => `<div class="li">
|
|
245
|
+
<span class="mono">${esc(v.from)}</span>
|
|
246
|
+
<span class="dim">→ ${esc(v.to)}</span>
|
|
247
|
+
<span class="pill pill-err">+${v.severity.toFixed(2)}</span>
|
|
248
|
+
</div>`).join("")
|
|
249
|
+
+ (d.layerViolations.count > d.layerViolations.items.length
|
|
250
|
+
? `<div class="more">+${d.layerViolations.count - d.layerViolations.items.length} more…</div>` : "")
|
|
251
|
+
: `<div class="ok">✓ No stability inversions (SDP)</div>`;
|
|
252
|
+
const issues = [
|
|
253
|
+
d.cycles.count > 0 ? `<div class="issue-row issue-err">🔴 ${d.cycles.count} circular ${d.cycles.count === 1 ? "dependency" : "dependencies"} detected</div>` : "",
|
|
254
|
+
d.dead.count > 5 ? `<div class="issue-row issue-warn">🟠 ${d.dead.count} dead exports (potential dead code)</div>` : "",
|
|
255
|
+
d.complexity.max > 20 ? `<div class="issue-row issue-warn">🟠 Max complexity ${d.complexity.max} — consider refactoring</div>` : "",
|
|
256
|
+
d.testCoverage.testFiles === 0 ? `<div class="issue-row issue-warn">🟡 No test files found</div>` :
|
|
257
|
+
covPct < 40 ? `<div class="issue-row issue-warn">🟡 Low test coverage (${covPct}%)</div>` : "",
|
|
258
|
+
d.layerViolations.count > 5 ? `<div class="issue-row issue-info">🔵 ${d.layerViolations.count} layer violations (SDP)</div>` : "",
|
|
259
|
+
].filter(Boolean).join("");
|
|
260
|
+
return `<!doctype html>
|
|
261
|
+
<html lang="en">
|
|
262
|
+
<head>
|
|
263
|
+
<meta charset="utf-8">
|
|
264
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
265
|
+
<title>${esc(d.project)} — Code Health</title>
|
|
266
|
+
<style>
|
|
267
|
+
:root{
|
|
268
|
+
--bg:#f6f8fa;--card:#fff;--bd:#e2e8f0;--tx:#0f172a;--dim:#64748b;
|
|
269
|
+
--soft:#f1f5f9;--accent:#6366f1;
|
|
270
|
+
--shadow:0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);
|
|
271
|
+
}
|
|
272
|
+
@media(prefers-color-scheme:dark){
|
|
273
|
+
:root{--bg:#0d1117;--card:#161b22;--bd:#21262d;--tx:#e6edf3;--dim:#7d8590;--soft:#1c2128;}
|
|
274
|
+
}
|
|
275
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
276
|
+
body{background:var(--bg);color:var(--tx);font-family:system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.5;font-size:13px;}
|
|
277
|
+
.wrap{max-width:1000px;margin:0 auto;padding:28px 20px 60px;}
|
|
278
|
+
|
|
279
|
+
/* ── Topbar ── */
|
|
280
|
+
.topbar{background:var(--card);border-bottom:1px solid var(--bd);padding:0 20px;height:50px;display:flex;align-items:center;gap:10px;position:sticky;top:0;z-index:10;box-shadow:var(--shadow);}
|
|
281
|
+
.topbar-title{font-weight:700;font-size:13px;color:var(--accent);}
|
|
282
|
+
.topbar-sep{width:1px;height:18px;background:var(--bd);}
|
|
283
|
+
.topbar-meta{font-size:12px;color:var(--dim);flex:1;}
|
|
284
|
+
.topbar-grade{font-weight:700;font-size:13px;padding:3px 10px;border-radius:999px;}
|
|
285
|
+
.btn{font:12px system-ui,sans-serif;cursor:pointer;border:1px solid var(--bd);background:transparent;color:inherit;border-radius:7px;padding:4px 11px;transition:background .12s;}
|
|
286
|
+
.btn:hover{background:var(--soft);}
|
|
287
|
+
|
|
288
|
+
/* ── Hero ── */
|
|
289
|
+
.hero{display:flex;align-items:center;gap:20px;margin:24px 0 22px;background:var(--card);border:1px solid var(--bd);border-radius:16px;padding:20px 24px;box-shadow:var(--shadow);}
|
|
290
|
+
.hero-right{flex:1;min-width:0;}
|
|
291
|
+
.score-ring-wrap svg{display:block;}
|
|
292
|
+
.h1{font-size:22px;font-weight:700;margin-bottom:4px;}
|
|
293
|
+
.sub{color:var(--dim);font-size:12px;}
|
|
294
|
+
.issues{margin-top:12px;display:flex;flex-direction:column;gap:4px;}
|
|
295
|
+
.issue-row{font-size:12px;padding:5px 10px;border-radius:8px;}
|
|
296
|
+
.issue-err{background:#fee2e2;color:#991b1b;}
|
|
297
|
+
.issue-warn{background:#fef9c3;color:#854d0e;}
|
|
298
|
+
.issue-info{background:#dbeafe;color:#1e40af;}
|
|
299
|
+
@media(prefers-color-scheme:dark){
|
|
300
|
+
.issue-err{background:#450a0a;color:#fca5a5;}
|
|
301
|
+
.issue-warn{background:#422006;color:#fde68a;}
|
|
302
|
+
.issue-info{background:#0c1e40;color:#93c5fd;}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ── Stat grid ── */
|
|
306
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:10px;margin-bottom:20px;}
|
|
307
|
+
.stat{background:var(--card);border:1px solid var(--bd);border-radius:12px;padding:13px 15px;box-shadow:var(--shadow);transition:transform .12s,box-shadow .12s;}
|
|
308
|
+
.stat:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.1);}
|
|
309
|
+
.sv{font-size:22px;font-weight:700;line-height:1.2;}
|
|
310
|
+
.sl{font-size:11px;color:var(--dim);margin-top:3px;}
|
|
311
|
+
.sl-sub{font-size:10px;color:var(--dim);margin-top:2px;opacity:.7;}
|
|
312
|
+
|
|
313
|
+
/* ── Cards ── */
|
|
314
|
+
.card{background:var(--card);border:1px solid var(--bd);border-radius:14px;margin-bottom:14px;box-shadow:var(--shadow);overflow:hidden;}
|
|
315
|
+
.card-header{display:flex;align-items:center;gap:8px;padding:14px 18px;cursor:pointer;user-select:none;transition:background .1s;}
|
|
316
|
+
.card-header:hover{background:var(--soft);}
|
|
317
|
+
.card-icon{font-size:15px;flex-shrink:0;}
|
|
318
|
+
.card-header h2{font-size:13px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--dim);flex:1;margin:0;}
|
|
319
|
+
.card-arrow{color:var(--dim);font-size:12px;transition:transform .15s;}
|
|
320
|
+
.card-body{padding:6px 18px 16px;border-top:1px solid var(--bd);}
|
|
321
|
+
|
|
322
|
+
/* ── Bars ── */
|
|
323
|
+
.row{display:flex;align-items:center;gap:10px;margin:7px 0;font-size:12px;}
|
|
324
|
+
.rl{flex:0 0 42%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tx);}
|
|
325
|
+
.track{flex:1;height:7px;background:var(--soft);border-radius:4px;overflow:hidden;}
|
|
326
|
+
.fill{height:100%;border-radius:4px;transition:width .5s ease;}
|
|
327
|
+
.rr{flex:0 0 auto;color:var(--dim);min-width:40px;text-align:right;font-size:11px;}
|
|
328
|
+
|
|
329
|
+
/* ── List items ── */
|
|
330
|
+
.li{display:flex;align-items:center;gap:8px;padding:6px 0;font-size:12px;border-top:1px solid var(--bd);}
|
|
331
|
+
.li:first-child{border-top:none;}
|
|
332
|
+
.mono{font-family:ui-monospace,monospace;font-weight:600;font-size:11px;}
|
|
333
|
+
.dim{color:var(--dim);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;}
|
|
334
|
+
.pill{margin-left:auto;background:var(--soft);border-radius:20px;padding:2px 9px;font-size:10px;color:var(--dim);flex-shrink:0;white-space:nowrap;}
|
|
335
|
+
.pill-warn{background:#fef9c3;color:#854d0e;}
|
|
336
|
+
.pill-err{background:#fee2e2;color:#991b1b;}
|
|
337
|
+
@media(prefers-color-scheme:dark){.pill-warn{background:#422006;color:#fde68a;}.pill-err{background:#450a0a;color:#fca5a5;}}
|
|
338
|
+
.kbadge{font-size:10px;color:var(--dim);background:var(--soft);border-radius:5px;padding:1px 6px;flex-shrink:0;}
|
|
339
|
+
.ok{color:#16a34a;font-size:12px;padding:4px 0;}
|
|
340
|
+
.empty{color:var(--dim);font-size:12px;padding:4px 0;}
|
|
341
|
+
.more{color:var(--dim);font-size:11px;padding-top:4px;}
|
|
342
|
+
.tag{font-size:10px;background:var(--soft);border-radius:5px;padding:1px 6px;color:var(--dim);}
|
|
343
|
+
|
|
344
|
+
/* ── Two col ── */
|
|
345
|
+
.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
|
346
|
+
@media(max-width:740px){.two{grid-template-columns:1fr;}.rl{flex-basis:38%;}}
|
|
347
|
+
|
|
348
|
+
/* ── Coverage ── */
|
|
349
|
+
.cov-summary{display:flex;align-items:center;gap:14px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid var(--bd);}
|
|
350
|
+
.cov-ring-wrap svg{display:block;}
|
|
351
|
+
.cov-pct{font-size:18px;font-weight:700;}
|
|
352
|
+
.cov-detail{font-size:11px;color:var(--dim);margin-top:3px;}
|
|
353
|
+
.untested-header{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dim);margin-bottom:4px;}
|
|
354
|
+
|
|
355
|
+
/* ── Cycles ── */
|
|
356
|
+
.cycle-li{align-items:flex-start;}
|
|
357
|
+
.cycle-arrow{color:#e24b4a;font-size:16px;flex-shrink:0;line-height:1.4;}
|
|
358
|
+
.cycle-chain{font-size:11px;word-break:break-all;flex:1;}
|
|
359
|
+
|
|
360
|
+
.delta{font-size:13px;font-weight:600;padding:3px 10px;border-radius:999px;margin-left:8px;}
|
|
361
|
+
.delta.up{background:#dcfce7;color:#16a34a;}.delta.dn{background:#fee2e2;color:#dc2626;}.delta.neu{background:var(--soft);color:var(--dim);}
|
|
362
|
+
@media(prefers-color-scheme:dark){.delta.up{background:#14532d;color:#4ade80;}.delta.dn{background:#7f1d1d;color:#f87171;}}
|
|
363
|
+
.sparkline{display:flex;align-items:flex-end;gap:2px;height:28px;margin-top:8px;}
|
|
364
|
+
.spark-bar{width:8px;border-radius:2px 2px 0 0;min-height:2px;transition:opacity .2s;}
|
|
365
|
+
.spark-bar:hover{opacity:.75;}
|
|
366
|
+
|
|
367
|
+
/* ── Footer ── */
|
|
368
|
+
.foot{color:var(--dim);font-size:11px;text-align:center;margin-top:20px;padding-top:14px;border-top:1px solid var(--bd);}
|
|
369
|
+
.foot a{color:var(--dim);}
|
|
370
|
+
</style>
|
|
371
|
+
</head>
|
|
372
|
+
<body>
|
|
373
|
+
<div class="topbar">
|
|
374
|
+
<span class="topbar-title">AST Map</span>
|
|
375
|
+
<div class="topbar-sep"></div>
|
|
376
|
+
<span class="topbar-meta">${esc(d.project)} · Code Health Report · ${esc(d.generatedAt.slice(0, 10))}</span>
|
|
377
|
+
<span class="topbar-grade" style="background:${gc}22;color:${gc};border:1px solid ${gc}44">${d.grade} · ${d.score}/100</span>
|
|
378
|
+
<button class="btn" onclick="window.print()">Print</button>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="wrap">
|
|
381
|
+
<div class="hero">
|
|
382
|
+
${scoreRing(d.score, d.grade)}
|
|
383
|
+
<div class="hero-right">
|
|
384
|
+
<h1 class="h1">${esc(d.project)}</h1>
|
|
385
|
+
<div class="sub">${d.fileCount} files · ${d.symbolCount} symbols · ${d.languages.length} language(s) · generated ${esc(d.generatedAt.slice(0, 10))}${deltaBadge}</div>
|
|
386
|
+
${issues ? `<div class="issues">${issues}</div>` : `<div class="issues"><div class="issue-row issue-info">✅ No critical issues detected</div></div>`}
|
|
387
|
+
${history.length > 1 ? (() => {
|
|
388
|
+
const max = Math.max(...history.map(h => h.score), 1);
|
|
389
|
+
const bars = history.map(h => {
|
|
390
|
+
const pct = Math.round((h.score / max) * 100);
|
|
391
|
+
const gc2 = GRADE_COLOR[h.grade] ?? "#888";
|
|
392
|
+
return `<div class="spark-bar" style="height:${pct}%;background:${gc2}" title="${h.date.slice(0, 10)}: ${h.score}/100 (${h.grade})"></div>`;
|
|
393
|
+
}).join("");
|
|
394
|
+
return `<div class="sparkline" title="Score history (last ${history.length} runs)">${bars}</div>`;
|
|
395
|
+
})() : ""}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div class="grid">
|
|
400
|
+
${statCard("Files", d.fileCount)}
|
|
401
|
+
${statCard("Symbols", d.symbolCount)}
|
|
402
|
+
${statCard("Import edges", d.edgeCount)}
|
|
403
|
+
${statCard("Avg complexity", d.complexity.average)}
|
|
404
|
+
${statCard("Max complexity", d.complexity.max, ratingColor(d.complexity.max > 20 ? "very-high" : d.complexity.max > 10 ? "high" : "low"))}
|
|
405
|
+
${statCard("Dead exports", d.dead.count, d.dead.count > 5 ? "#d85a30" : d.dead.count > 0 ? "#ba7517" : "#1d9e75")}
|
|
406
|
+
${statCard("Cycles", d.cycles.count, d.cycles.count ? "#e24b4a" : "#1d9e75")}
|
|
407
|
+
${statCard("SDP violations", d.layerViolations.count, d.layerViolations.count > 5 ? "#d85a30" : d.layerViolations.count > 0 ? "#ba7517" : "#1d9e75")}
|
|
408
|
+
${statCard("Test coverage", covPct + "%", covC)}
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
${collapsibleCard("langs", "Language breakdown", langs || `<div class="empty">No data.</div>`, "🌐")}
|
|
412
|
+
${collapsibleCard("cx", "Complexity hotspots", hotspots, "🔥")}
|
|
413
|
+
|
|
414
|
+
<div class="two">
|
|
415
|
+
${collapsibleCard("god", "God nodes (most imported)", god, "👑")}
|
|
416
|
+
${collapsibleCard("cycles", "Circular dependencies", cycles, "🔄")}
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div class="two">
|
|
420
|
+
${collapsibleCard("modules", "Module coupling (instability)", modules, "📦")}
|
|
421
|
+
${collapsibleCard("sdp", "Layer violations (SDP)", sdp, "🏗️")}
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
${collapsibleCard("cov", "Test coverage", covSummary + covList, "🧪")}
|
|
425
|
+
${collapsibleCard("dead", "Dead exports (high confidence)", dead, "💀", false)}
|
|
426
|
+
|
|
427
|
+
<div class="foot">Generated by <strong>AST-MCP</strong> · universal-ast-mapper · <a href="https://github.com/6ixthxense/ast-mcp">github</a></div>
|
|
428
|
+
</div>
|
|
429
|
+
<script>
|
|
430
|
+
function toggleCard(id){
|
|
431
|
+
const body=document.getElementById('body-'+id);
|
|
432
|
+
const arr=document.getElementById('arr-'+id);
|
|
433
|
+
if(!body||!arr)return;
|
|
434
|
+
const open=body.style.display!=='none';
|
|
435
|
+
body.style.display=open?'none':'';
|
|
436
|
+
arr.textContent=open?'▸':'▾';
|
|
437
|
+
}
|
|
438
|
+
</script>
|
|
439
|
+
</body>
|
|
440
|
+
</html>`;
|
|
441
|
+
}
|
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildSkeleton, collectSourceFiles } from "./skeleton.js";
|
|
4
|
+
import { resolveOptions } from "./config.js";
|
|
5
|
+
import { findSymbol } from "./analysis.js";
|
|
6
|
+
import { buildCrossLangIndex, resolveCrossLangTarget, } from "./crosslang.js";
|
|
7
|
+
import { resolveWorkspaceImportCached } from "./workspace.js";
|
|
8
|
+
import { aliasCandidates } from "./tsconfig.js";
|
|
9
|
+
const SRC_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs", ".vue", ".svelte"];
|
|
10
|
+
function extractParams(sig) {
|
|
11
|
+
const start = sig.indexOf("(");
|
|
12
|
+
if (start === -1)
|
|
13
|
+
return null;
|
|
14
|
+
let depth = 0;
|
|
15
|
+
for (let i = start; i < sig.length; i++) {
|
|
16
|
+
if (sig[i] === "(")
|
|
17
|
+
depth++;
|
|
18
|
+
else if (sig[i] === ")") {
|
|
19
|
+
depth--;
|
|
20
|
+
if (depth === 0)
|
|
21
|
+
return sig.slice(start, i + 1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const JS_TO_TS = {
|
|
27
|
+
".js": [".ts", ".tsx", ".js"],
|
|
28
|
+
".jsx": [".tsx", ".jsx"],
|
|
29
|
+
".mjs": [".mts", ".mjs"],
|
|
30
|
+
".cjs": [".cts", ".cjs"],
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a TS/JS-style relative import path to an absolute file path.
|
|
34
|
+
* Returns null for external packages or when the file cannot be found.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveImportPath(importFrom, fromAbs) {
|
|
37
|
+
if (!importFrom.startsWith("."))
|
|
38
|
+
return null;
|
|
39
|
+
const fromDir = path.dirname(fromAbs);
|
|
40
|
+
const candidate = path.resolve(fromDir, importFrom);
|
|
41
|
+
const declaredExt = path.extname(candidate).toLowerCase();
|
|
42
|
+
if (declaredExt && JS_TO_TS[declaredExt]) {
|
|
43
|
+
const base = candidate.slice(0, candidate.length - declaredExt.length);
|
|
44
|
+
for (const ext of JS_TO_TS[declaredExt]) {
|
|
45
|
+
const p = base + ext;
|
|
46
|
+
if (fs.existsSync(p))
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return probeCandidate(candidate);
|
|
51
|
+
}
|
|
52
|
+
/** Probe a path base: exact file → +extensions → /index.<ext>. */
|
|
53
|
+
function probeCandidate(candidate) {
|
|
54
|
+
try {
|
|
55
|
+
const stat = fs.statSync(candidate);
|
|
56
|
+
if (stat.isFile())
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
catch { /* not found */ }
|
|
60
|
+
for (const ext of SRC_EXTS) {
|
|
61
|
+
const p = candidate + ext;
|
|
62
|
+
if (fs.existsSync(p))
|
|
63
|
+
return p;
|
|
64
|
+
}
|
|
65
|
+
for (const ext of SRC_EXTS) {
|
|
66
|
+
const p = path.join(candidate, `index${ext}`);
|
|
67
|
+
if (fs.existsSync(p))
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a tsconfig/jsconfig path-aliased bare import (e.g. `@/components/X`
|
|
74
|
+
* with `"@/*": ["./src/*"]`) to an absolute file path, using the nearest
|
|
75
|
+
* config above the importing file. Returns null when not an alias.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveAliasedImport(importFrom, fromAbs) {
|
|
78
|
+
for (const base of aliasCandidates(importFrom, fromAbs)) {
|
|
79
|
+
const declaredExt = path.extname(base).toLowerCase();
|
|
80
|
+
if (declaredExt && JS_TO_TS[declaredExt]) {
|
|
81
|
+
const stem = base.slice(0, base.length - declaredExt.length);
|
|
82
|
+
for (const ext of JS_TO_TS[declaredExt]) {
|
|
83
|
+
const p = stem + ext;
|
|
84
|
+
if (fs.existsSync(p))
|
|
85
|
+
return p;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const hit = probeCandidate(base);
|
|
89
|
+
if (hit)
|
|
90
|
+
return hit;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/* ─── Cross-language index cache ──────────────────────────────────────────── */
|
|
95
|
+
// Java/C# need a project-wide index to resolve fully-qualified imports.
|
|
96
|
+
// Built lazily on first cross-language resolve, then reused for the process
|
|
97
|
+
// lifetime (the MCP server is per-root, so this is safe).
|
|
98
|
+
const indexCache = new Map();
|
|
99
|
+
export async function getOrBuildCrossLangIndex(root) {
|
|
100
|
+
const key = path.resolve(root);
|
|
101
|
+
let p = indexCache.get(key);
|
|
102
|
+
if (p)
|
|
103
|
+
return p;
|
|
104
|
+
p = (async () => {
|
|
105
|
+
const opts = resolveOptions({ detail: "outline", emitHtml: false });
|
|
106
|
+
const files = collectSourceFiles(key, opts);
|
|
107
|
+
const skels = [];
|
|
108
|
+
for (const abs of files) {
|
|
109
|
+
const ext = path.extname(abs).toLowerCase();
|
|
110
|
+
// Only Java/C# contribute to the index (Rust resolves via direct
|
|
111
|
+
// module-path walk against the filesystem, no index needed).
|
|
112
|
+
if (ext !== ".java" && ext !== ".cs" && ext !== ".kt" && ext !== ".kts" && ext !== ".swift")
|
|
113
|
+
continue;
|
|
114
|
+
const rel = path.relative(key, abs).split(path.sep).join("/");
|
|
115
|
+
try {
|
|
116
|
+
skels.push(await buildSkeleton(abs, rel, opts));
|
|
117
|
+
}
|
|
118
|
+
catch { /* skip unparsable files */ }
|
|
119
|
+
}
|
|
120
|
+
return buildCrossLangIndex(skels);
|
|
121
|
+
})();
|
|
122
|
+
indexCache.set(key, p);
|
|
123
|
+
return p;
|
|
124
|
+
}
|
|
125
|
+
/** Test/debug hook: drop the cached index (rebuilds on next call). */
|
|
126
|
+
export function clearCrossLangIndexCache() {
|
|
127
|
+
indexCache.clear();
|
|
128
|
+
}
|
|
129
|
+
async function lookupSymbolInTarget(targetAbs, targetRel, symbol) {
|
|
130
|
+
const opts = resolveOptions({ detail: "full", emitHtml: false });
|
|
131
|
+
try {
|
|
132
|
+
const targetSkel = await buildSkeleton(targetAbs, targetRel, opts);
|
|
133
|
+
const sym = findSymbol(targetSkel.symbols, symbol);
|
|
134
|
+
if (sym) {
|
|
135
|
+
const signature = sym.signature ?? null;
|
|
136
|
+
const out = { found: true, kind: sym.kind };
|
|
137
|
+
if (signature !== undefined)
|
|
138
|
+
out.signature = signature;
|
|
139
|
+
if (signature) {
|
|
140
|
+
const params = extractParams(signature);
|
|
141
|
+
if (params)
|
|
142
|
+
out.params = params;
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch { /* unresolvable / parse error */ }
|
|
148
|
+
return { found: false };
|
|
149
|
+
}
|
|
150
|
+
async function enrichRelativeImport(imp, fromAbs, root) {
|
|
151
|
+
const isBare = !imp.from.startsWith(".");
|
|
152
|
+
// Relative import → path resolve; bare specifier → try monorepo workspace.
|
|
153
|
+
let resolvedAbs = isBare ? null : resolveImportPath(imp.from, fromAbs);
|
|
154
|
+
if (!resolvedAbs && isBare)
|
|
155
|
+
resolvedAbs = resolveAliasedImport(imp.from, fromAbs);
|
|
156
|
+
if (!resolvedAbs && isBare)
|
|
157
|
+
resolvedAbs = resolveWorkspaceImportCached(imp.from, root);
|
|
158
|
+
const treatedExternal = isBare && !resolvedAbs;
|
|
159
|
+
const resolvedRel = resolvedAbs
|
|
160
|
+
? path.relative(root, resolvedAbs).split(path.sep).join("/")
|
|
161
|
+
: null;
|
|
162
|
+
let enrichment = { found: false };
|
|
163
|
+
if (resolvedAbs && !imp.isSideEffect && !imp.isNamespaceImport && imp.symbol !== "*") {
|
|
164
|
+
enrichment = await lookupSymbolInTarget(resolvedAbs, resolvedRel, imp.symbol);
|
|
165
|
+
}
|
|
166
|
+
else if (resolvedAbs) {
|
|
167
|
+
enrichment = { found: true };
|
|
168
|
+
}
|
|
169
|
+
return assembleResolved(imp, resolvedAbs, resolvedRel, treatedExternal, enrichment);
|
|
170
|
+
}
|
|
171
|
+
async function enrichCrossLangImport(imp, skel, fromAbs, root, index) {
|
|
172
|
+
const target = resolveCrossLangTarget(imp, skel, fromAbs, root, index);
|
|
173
|
+
if (!target) {
|
|
174
|
+
return assembleResolved(imp, null, null, true, { found: false });
|
|
175
|
+
}
|
|
176
|
+
if (target.kind === "file") {
|
|
177
|
+
// Namespace-style (Java wildcard / C# using). Point to the first file —
|
|
178
|
+
// useful for navigation; the symbol itself isn't a specific declaration.
|
|
179
|
+
const firstRel = target.files[0];
|
|
180
|
+
const firstAbs = path.resolve(root, firstRel);
|
|
181
|
+
return assembleResolved(imp, firstAbs, firstRel, false, { found: true });
|
|
182
|
+
}
|
|
183
|
+
// Symbol-level (Java FQCN, Rust crate::path::Item)
|
|
184
|
+
const targetAbs = path.resolve(root, target.file);
|
|
185
|
+
const enrichment = await lookupSymbolInTarget(targetAbs, target.file, target.symbol);
|
|
186
|
+
return assembleResolved(imp, targetAbs, target.file, false, enrichment);
|
|
187
|
+
}
|
|
188
|
+
function assembleResolved(imp, resolvedAbs, resolvedRel, isExternal, enrichment) {
|
|
189
|
+
const out = {
|
|
190
|
+
...imp,
|
|
191
|
+
resolvedPath: resolvedAbs,
|
|
192
|
+
resolvedRel,
|
|
193
|
+
found: enrichment.found,
|
|
194
|
+
importKind: isExternal ? "external" : "relative",
|
|
195
|
+
};
|
|
196
|
+
if (enrichment.kind !== undefined)
|
|
197
|
+
out.kind = enrichment.kind;
|
|
198
|
+
if (enrichment.signature !== undefined)
|
|
199
|
+
out.signature = enrichment.signature;
|
|
200
|
+
if (enrichment.params !== undefined)
|
|
201
|
+
out.params = enrichment.params;
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
/* ─── Public entry point ──────────────────────────────────────────────────── */
|
|
205
|
+
const CROSS_LANG = new Set(["java", "csharp", "rust", "go", "kotlin", "c", "cpp", "swift"]);
|
|
206
|
+
export async function resolveFileImports(skel, absPath, root) {
|
|
207
|
+
if (!skel.imports || skel.imports.length === 0)
|
|
208
|
+
return [];
|
|
209
|
+
const results = [];
|
|
210
|
+
// Lazy-build the cross-lang index only when actually needed.
|
|
211
|
+
let indexPromise = null;
|
|
212
|
+
const getIndex = () => (indexPromise ??= getOrBuildCrossLangIndex(root));
|
|
213
|
+
for (const imp of skel.imports) {
|
|
214
|
+
if (CROSS_LANG.has(skel.language)) {
|
|
215
|
+
results.push(await enrichCrossLangImport(imp, skel, absPath, root, await getIndex()));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
results.push(await enrichRelativeImport(imp, absPath, root));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return results;
|
|
222
|
+
}
|