universal-ast-mapper 2.0.0 → 2.0.1
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 +9 -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 +185 -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 +341 -0
- package/dist/worker.js +27 -0
- package/dist/workspace.js +330 -0
- package/package.json +2 -1
package/dist/serve.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { collectSourceFiles } from "./skeleton.js";
|
|
5
|
+
import { resolveOptions } from "./config.js";
|
|
6
|
+
import { buildSymbolGraph } from "./graph.js";
|
|
7
|
+
import { findDeadExports, findCircularDeps, getTopSymbols } from "./graph-analysis.js";
|
|
8
|
+
import { buildReport } from "./report.js";
|
|
9
|
+
import { loadHistory } from "./history.js";
|
|
10
|
+
import { detectSmells } from "./smells.js";
|
|
11
|
+
import { scanFileForSecurityIssues } from "./security.js";
|
|
12
|
+
import { buildSkeletonsBulk } from "./pool.js";
|
|
13
|
+
import { webAppHtml } from "./webapp.js";
|
|
14
|
+
export async function startServe(opts) {
|
|
15
|
+
const root = opts.root;
|
|
16
|
+
const scanDir = opts.scanDir ?? root;
|
|
17
|
+
const port = opts.port ?? 7337;
|
|
18
|
+
// SSE client registry
|
|
19
|
+
const sseClients = new Set();
|
|
20
|
+
function broadcastChange() {
|
|
21
|
+
for (const client of sseClients) {
|
|
22
|
+
try {
|
|
23
|
+
client.write("event: change\ndata: {}\n\n");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
sseClients.delete(client);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// fs.watch for live reload
|
|
31
|
+
if (opts.watch) {
|
|
32
|
+
try {
|
|
33
|
+
fs.watch(scanDir, { recursive: true }, (_event, filename) => {
|
|
34
|
+
if (!filename || filename.includes(".ast-map"))
|
|
35
|
+
return;
|
|
36
|
+
cache = null;
|
|
37
|
+
broadcastChange();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch { /* watch not supported on all platforms */ }
|
|
41
|
+
}
|
|
42
|
+
let cache = null;
|
|
43
|
+
const CACHE_TTL = 5000;
|
|
44
|
+
async function getSkeletons() {
|
|
45
|
+
if (cache && Date.now() - cache.ts < CACHE_TTL)
|
|
46
|
+
return cache.skeletons;
|
|
47
|
+
const skOpts = resolveOptions({ detail: "outline", emitHtml: false });
|
|
48
|
+
const files = collectSourceFiles(scanDir, skOpts);
|
|
49
|
+
const items = files.map((f) => ({
|
|
50
|
+
abs: f,
|
|
51
|
+
rel: path.relative(root, f).split(path.sep).join("/"),
|
|
52
|
+
}));
|
|
53
|
+
const built = await buildSkeletonsBulk(items, skOpts);
|
|
54
|
+
const skeletons = built.filter(Boolean).map((r) => r.skel);
|
|
55
|
+
cache = { skeletons, ts: Date.now() };
|
|
56
|
+
return skeletons;
|
|
57
|
+
}
|
|
58
|
+
const server = http.createServer(async (req, res) => {
|
|
59
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
60
|
+
const pathname = url.pathname;
|
|
61
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
62
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
63
|
+
try {
|
|
64
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
65
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
66
|
+
res.end(webAppHtml(port));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (pathname === "/api/report") {
|
|
70
|
+
const skeletons = await getSkeletons();
|
|
71
|
+
const graph = buildSymbolGraph(skeletons, root);
|
|
72
|
+
const history = loadHistory(root);
|
|
73
|
+
const report = await buildReport(scanDir, root);
|
|
74
|
+
const smellsAll = [];
|
|
75
|
+
const securityAll = [];
|
|
76
|
+
for (const skel of skeletons) {
|
|
77
|
+
const fileAbs = path.resolve(root, skel.file);
|
|
78
|
+
try {
|
|
79
|
+
const src = fs.readFileSync(fileAbs, "utf8");
|
|
80
|
+
smellsAll.push(...detectSmells(skel, src.split("\n").length));
|
|
81
|
+
securityAll.push(...scanFileForSecurityIssues(src, skel.file));
|
|
82
|
+
}
|
|
83
|
+
catch { /* skip */ }
|
|
84
|
+
}
|
|
85
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
86
|
+
res.end(JSON.stringify({ ...report, smells: smellsAll, security: securityAll, history }, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (pathname === "/api/graph") {
|
|
90
|
+
const skeletons = await getSkeletons();
|
|
91
|
+
const graph = buildSymbolGraph(skeletons, root);
|
|
92
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
93
|
+
res.end(JSON.stringify(graph, null, 2));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (pathname === "/api/dead") {
|
|
97
|
+
const skeletons = await getSkeletons();
|
|
98
|
+
const graph = buildSymbolGraph(skeletons, root);
|
|
99
|
+
const dead = findDeadExports(graph);
|
|
100
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
101
|
+
res.end(JSON.stringify(dead, null, 2));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (pathname === "/api/top") {
|
|
105
|
+
const skeletons = await getSkeletons();
|
|
106
|
+
const graph = buildSymbolGraph(skeletons, root);
|
|
107
|
+
const top = getTopSymbols(graph, 20);
|
|
108
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
109
|
+
res.end(JSON.stringify(top, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (pathname === "/api/cycles") {
|
|
113
|
+
const skeletons = await getSkeletons();
|
|
114
|
+
const graph = buildSymbolGraph(skeletons, root);
|
|
115
|
+
const cycles = findCircularDeps(graph);
|
|
116
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
117
|
+
res.end(JSON.stringify(cycles, null, 2));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (pathname === "/api/history") {
|
|
121
|
+
const history = loadHistory(root);
|
|
122
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
123
|
+
res.end(JSON.stringify(history, null, 2));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (pathname === "/api/skeletons") {
|
|
127
|
+
const skeletons = await getSkeletons();
|
|
128
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
129
|
+
res.end(JSON.stringify(skeletons, null, 2));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (pathname === "/events") {
|
|
133
|
+
res.writeHead(200, {
|
|
134
|
+
"Content-Type": "text/event-stream",
|
|
135
|
+
"Cache-Control": "no-cache",
|
|
136
|
+
"Connection": "keep-alive",
|
|
137
|
+
"Access-Control-Allow-Origin": "*",
|
|
138
|
+
});
|
|
139
|
+
res.write("event: connected\ndata: {}\n\n");
|
|
140
|
+
sseClients.add(res);
|
|
141
|
+
req.on("close", () => sseClients.delete(res));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (pathname === "/api/smells") {
|
|
145
|
+
const skeletons = await getSkeletons();
|
|
146
|
+
const all = [];
|
|
147
|
+
for (const skel of skeletons) {
|
|
148
|
+
const fileAbs = path.resolve(root, skel.file);
|
|
149
|
+
try {
|
|
150
|
+
const src = fs.readFileSync(fileAbs, "utf8");
|
|
151
|
+
all.push(...detectSmells(skel, src.split("\n").length));
|
|
152
|
+
}
|
|
153
|
+
catch { /* skip */ }
|
|
154
|
+
}
|
|
155
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
156
|
+
res.end(JSON.stringify(all, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (pathname === "/api/security") {
|
|
160
|
+
const skeletons = await getSkeletons();
|
|
161
|
+
const all = [];
|
|
162
|
+
for (const skel of skeletons) {
|
|
163
|
+
const fileAbs = path.resolve(root, skel.file);
|
|
164
|
+
try {
|
|
165
|
+
const src = fs.readFileSync(fileAbs, "utf8");
|
|
166
|
+
all.push(...scanFileForSecurityIssues(src, skel.file));
|
|
167
|
+
}
|
|
168
|
+
catch { /* skip */ }
|
|
169
|
+
}
|
|
170
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
171
|
+
res.end(JSON.stringify(all, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
175
|
+
res.end("Not found");
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
179
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
server.listen(port, () => resolve(server));
|
|
184
|
+
});
|
|
185
|
+
}
|
package/dist/sfc.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
2
|
+
export function isSfcExt(ext) {
|
|
3
|
+
return ext === ".vue" || ext === ".svelte";
|
|
4
|
+
}
|
|
5
|
+
export function extractScript(source) {
|
|
6
|
+
// Start from an all-blank canvas the same shape as the source (newlines kept,
|
|
7
|
+
// every other character turned into a space) to preserve line/column offsets.
|
|
8
|
+
let out = source.replace(/[^\n]/g, " ");
|
|
9
|
+
let hasScript = false;
|
|
10
|
+
let lang = "js";
|
|
11
|
+
let m;
|
|
12
|
+
SCRIPT_RE.lastIndex = 0;
|
|
13
|
+
while ((m = SCRIPT_RE.exec(source)) !== null) {
|
|
14
|
+
hasScript = true;
|
|
15
|
+
const attrs = m[1] ?? "";
|
|
16
|
+
if (/lang\s*=\s*["'](ts|typescript)["']/i.test(attrs))
|
|
17
|
+
lang = "ts";
|
|
18
|
+
const inner = m[2] ?? "";
|
|
19
|
+
const innerStart = m.index + m[0].indexOf(inner, m[1] ? m[1].length : 0);
|
|
20
|
+
out = out.slice(0, innerStart) + inner + out.slice(innerStart + inner.length);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
code: hasScript ? out : "",
|
|
24
|
+
grammar: lang === "ts" ? "typescript" : "javascript",
|
|
25
|
+
hasScript,
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/similar.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// ─── Fingerprinting ────────────────────────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* Build a structural fingerprint for a symbol.
|
|
4
|
+
* Two symbols with the same fingerprint are structurally similar (not textually identical).
|
|
5
|
+
*/
|
|
6
|
+
function fingerprint(sym) {
|
|
7
|
+
const kind = sym.kind;
|
|
8
|
+
if (!["function", "method", "class", "struct"].includes(kind))
|
|
9
|
+
return null;
|
|
10
|
+
const sig = sym.signature ?? "";
|
|
11
|
+
// Param count from signature
|
|
12
|
+
const paramMatch = sig.match(/\(([^)]*)\)/);
|
|
13
|
+
const paramStr = paramMatch?.[1]?.trim() ?? "";
|
|
14
|
+
const paramCount = paramStr === "" ? 0 : paramStr.split(",").length;
|
|
15
|
+
// Presence of return type annotation
|
|
16
|
+
const hasReturnType = sig.includes("): ") || sig.includes(") :") || sig.includes("->");
|
|
17
|
+
// Async?
|
|
18
|
+
const isAsync = sig.includes("async ") || sig.includes("suspend ") || sig.includes("async def ");
|
|
19
|
+
// Visibility
|
|
20
|
+
const visibility = sym.visibility;
|
|
21
|
+
// Child count bucket for classes
|
|
22
|
+
const childCount = sym.children.length;
|
|
23
|
+
const childBucket = childCount === 0 ? "0" : childCount <= 3 ? "1-3" : childCount <= 8 ? "4-8" : "9+";
|
|
24
|
+
// Line length bucket
|
|
25
|
+
const lineCount = sym.range.endLine - sym.range.startLine + 1;
|
|
26
|
+
const sizeBucket = lineCount <= 5 ? "xs" : lineCount <= 20 ? "sm" : lineCount <= 60 ? "md" : "lg";
|
|
27
|
+
// Has nested functions?
|
|
28
|
+
const hasNested = sym.children.some((c) => c.kind === "function" || c.kind === "method");
|
|
29
|
+
if (kind === "class" || kind === "struct") {
|
|
30
|
+
return `${kind}|children:${childBucket}|vis:${visibility}|size:${sizeBucket}`;
|
|
31
|
+
}
|
|
32
|
+
return `${kind}|params:${paramCount}|async:${isAsync}|ret:${hasReturnType}|vis:${visibility}|size:${sizeBucket}|nested:${hasNested}`;
|
|
33
|
+
}
|
|
34
|
+
// ─── Collector ────────────────────────────────────────────────────────────────
|
|
35
|
+
function collect(symbols, file, kinds, out) {
|
|
36
|
+
for (const sym of symbols) {
|
|
37
|
+
if (kinds.has(sym.kind)) {
|
|
38
|
+
const fp = fingerprint(sym);
|
|
39
|
+
if (fp) {
|
|
40
|
+
out.push({
|
|
41
|
+
file,
|
|
42
|
+
symbol: sym.name,
|
|
43
|
+
kind: sym.kind,
|
|
44
|
+
line: sym.range.startLine,
|
|
45
|
+
signature: sym.signature,
|
|
46
|
+
fingerprint: fp,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (sym.children.length > 0)
|
|
51
|
+
collect(sym.children, file, kinds, out);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ─── Human-readable description ───────────────────────────────────────────────
|
|
55
|
+
function describeFingerprint(fp) {
|
|
56
|
+
const parts = Object.fromEntries(fp.split("|").map((p) => p.split(":")));
|
|
57
|
+
const kind = fp.split("|")[0];
|
|
58
|
+
const segments = [`${kind}s`];
|
|
59
|
+
if (parts.params)
|
|
60
|
+
segments.push(`${parts.params} param(s)`);
|
|
61
|
+
if (parts.async === "true")
|
|
62
|
+
segments.push("async");
|
|
63
|
+
if (parts.ret === "true")
|
|
64
|
+
segments.push("typed return");
|
|
65
|
+
if (parts.children)
|
|
66
|
+
segments.push(`${parts.children} children`);
|
|
67
|
+
if (parts.size)
|
|
68
|
+
segments.push(`size:${parts.size}`);
|
|
69
|
+
if (parts.vis)
|
|
70
|
+
segments.push(parts.vis);
|
|
71
|
+
return segments.join(", ");
|
|
72
|
+
}
|
|
73
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
74
|
+
/** Find groups of structurally similar symbols across multiple skeleton files. */
|
|
75
|
+
export function findSimilar(skeletons, opts = {}) {
|
|
76
|
+
const minGroupSize = opts.minGroupSize ?? 2;
|
|
77
|
+
const kinds = new Set(opts.kinds ?? ["function", "method", "class", "struct"]);
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (const skel of skeletons) {
|
|
80
|
+
collect(skel.symbols, skel.file, kinds, entries);
|
|
81
|
+
}
|
|
82
|
+
// Group by fingerprint
|
|
83
|
+
const groups = new Map();
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const g = groups.get(entry.fingerprint) ?? [];
|
|
86
|
+
g.push(entry);
|
|
87
|
+
groups.set(entry.fingerprint, g);
|
|
88
|
+
}
|
|
89
|
+
return [...groups.entries()]
|
|
90
|
+
.filter(([, g]) => g.length >= minGroupSize)
|
|
91
|
+
.map(([fp, g]) => ({
|
|
92
|
+
fingerprint: fp,
|
|
93
|
+
description: describeFingerprint(fp),
|
|
94
|
+
count: g.length,
|
|
95
|
+
entries: g.sort((a, b) => a.file.localeCompare(b.file) || a.symbol.localeCompare(b.symbol)),
|
|
96
|
+
}))
|
|
97
|
+
.sort((a, b) => b.count - a.count);
|
|
98
|
+
}
|
package/dist/skeleton.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { detectLanguage, supportedExtensions } from "./registry.js";
|
|
4
|
+
import { parseSource } from "./parser.js";
|
|
5
|
+
import { countSymbols, toOutline } from "./extractors/common.js";
|
|
6
|
+
import { extractScript } from "./sfc.js";
|
|
7
|
+
import { diskCacheKey, diskCacheGet, diskCachePut } from "./diskcache.js";
|
|
8
|
+
export const SCHEMA_VERSION = "1.1";
|
|
9
|
+
export const GRAMMAR_SOURCE = "tree-sitter-wasms@0.1.13";
|
|
10
|
+
const parseCache = new Map();
|
|
11
|
+
function cacheKey(absPath, detail) { return `${absPath}|${detail}`; }
|
|
12
|
+
function getCached(absPath, detail) {
|
|
13
|
+
try {
|
|
14
|
+
const entry = parseCache.get(cacheKey(absPath, detail));
|
|
15
|
+
if (!entry)
|
|
16
|
+
return null;
|
|
17
|
+
const mtime = fs.statSync(absPath).mtimeMs;
|
|
18
|
+
return entry.mtime === mtime ? entry.result : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function setCached(absPath, detail, result) {
|
|
25
|
+
try {
|
|
26
|
+
const mtime = fs.statSync(absPath).mtimeMs;
|
|
27
|
+
parseCache.set(cacheKey(absPath, detail), { mtime, result });
|
|
28
|
+
}
|
|
29
|
+
catch { /* skip if stat fails */ }
|
|
30
|
+
}
|
|
31
|
+
export class UnsupportedLanguageError extends Error {
|
|
32
|
+
ext;
|
|
33
|
+
constructor(ext) {
|
|
34
|
+
super(`Unsupported file type "${ext}". Supported: ${supportedExtensions().join(", ")}`);
|
|
35
|
+
this.ext = ext;
|
|
36
|
+
this.name = "UnsupportedLanguageError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a skeleton for a single file.
|
|
41
|
+
* @param absPath absolute path on disk (already validated to be within root)
|
|
42
|
+
* @param relPath path relative to root, used as the displayed `file`
|
|
43
|
+
*/
|
|
44
|
+
export async function buildSkeleton(absPath, relPath, opts) {
|
|
45
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
46
|
+
const entry = detectLanguage(absPath);
|
|
47
|
+
if (!entry)
|
|
48
|
+
throw new UnsupportedLanguageError(ext);
|
|
49
|
+
const stat = fs.statSync(absPath);
|
|
50
|
+
if (stat.size > opts.maxFileBytes) {
|
|
51
|
+
throw new Error(`File is ${stat.size} bytes, exceeds maxFileBytes (${opts.maxFileBytes}). Increase the limit to parse it.`);
|
|
52
|
+
}
|
|
53
|
+
// Return cached result if file hasn't changed. The cached SkeletonFile's
|
|
54
|
+
// `.file` is whatever relPath the first caller used; the same absolute file
|
|
55
|
+
// can be requested under a different root (different relPath), so override
|
|
56
|
+
// `.file` per call to avoid leaking a stale rel path into callers/indexes.
|
|
57
|
+
const cached = getCached(absPath, opts.detail);
|
|
58
|
+
if (cached) {
|
|
59
|
+
const wantFile = relPath.split(path.sep).join("/");
|
|
60
|
+
return cached.file === wantFile ? cached : { ...cached, file: wantFile };
|
|
61
|
+
}
|
|
62
|
+
const rawSource = fs.readFileSync(absPath, "utf8");
|
|
63
|
+
// Persistent cache (content-hash keyed, see diskcache.ts). A hit skips
|
|
64
|
+
// parsing entirely; entries can never be stale because the key embeds the
|
|
65
|
+
// file's bytes + detail + schema/grammar versions.
|
|
66
|
+
const dKey = diskCacheKey(rawSource, opts.detail, SCHEMA_VERSION, GRAMMAR_SOURCE);
|
|
67
|
+
const fromDisk = diskCacheGet(dKey);
|
|
68
|
+
if (fromDisk) {
|
|
69
|
+
const wantFile = relPath.split(path.sep).join("/");
|
|
70
|
+
const hit = fromDisk.file === wantFile ? fromDisk : { ...fromDisk, file: wantFile };
|
|
71
|
+
setCached(absPath, opts.detail, hit);
|
|
72
|
+
return hit;
|
|
73
|
+
}
|
|
74
|
+
let source = rawSource;
|
|
75
|
+
let grammar = entry.grammar;
|
|
76
|
+
if (entry.sfc) {
|
|
77
|
+
const script = extractScript(source);
|
|
78
|
+
source = script.code; // blank-padded script-only source (offsets preserved)
|
|
79
|
+
grammar = script.grammar;
|
|
80
|
+
}
|
|
81
|
+
const root = await parseSource(grammar, source);
|
|
82
|
+
let symbols = entry.extract(root, source);
|
|
83
|
+
if (opts.detail === "outline")
|
|
84
|
+
symbols = toOutline(symbols);
|
|
85
|
+
const directives = entry.extractDirectives ? entry.extractDirectives(root, source) : [];
|
|
86
|
+
const imports = entry.extractImports ? entry.extractImports(root, source) : [];
|
|
87
|
+
const result = {
|
|
88
|
+
schemaVersion: SCHEMA_VERSION,
|
|
89
|
+
file: relPath.split(path.sep).join("/"),
|
|
90
|
+
language: entry.language,
|
|
91
|
+
generatedAt: new Date().toISOString(),
|
|
92
|
+
parser: { engine: "tree-sitter", grammar: `${grammar} (${GRAMMAR_SOURCE})` },
|
|
93
|
+
symbolCount: countSymbols(symbols),
|
|
94
|
+
...(directives.length > 0 ? { directives } : {}),
|
|
95
|
+
...(imports.length > 0 ? { imports } : {}),
|
|
96
|
+
symbols,
|
|
97
|
+
};
|
|
98
|
+
setCached(absPath, opts.detail, result);
|
|
99
|
+
diskCachePut(dKey, result);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
/** Recursively collect supported source files under a directory. */
|
|
103
|
+
export function collectSourceFiles(absDir, opts) {
|
|
104
|
+
const supported = new Set(supportedExtensions());
|
|
105
|
+
const ignore = new Set(opts.ignore);
|
|
106
|
+
const results = [];
|
|
107
|
+
const walk = (dir) => {
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const e of entries) {
|
|
116
|
+
if (e.name.startsWith(".")) {
|
|
117
|
+
if (e.isDirectory())
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const full = path.join(dir, e.name);
|
|
121
|
+
if (e.isDirectory()) {
|
|
122
|
+
if (!ignore.has(e.name))
|
|
123
|
+
walk(full);
|
|
124
|
+
}
|
|
125
|
+
else if (e.isFile() && supported.has(path.extname(e.name).toLowerCase())) {
|
|
126
|
+
results.push(full);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
walk(absDir);
|
|
131
|
+
return results.sort();
|
|
132
|
+
}
|