universal-ast-mapper 1.27.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BLUEPRINT.md +230 -230
- package/CHANGELOG.md +466 -321
- package/README.md +878 -877
- package/package.json +48 -47
- package/scripts/install-skill.mjs +187 -187
- package/dist/analysis.js +0 -134
- package/dist/callgraph.js +0 -467
- package/dist/check.js +0 -112
- package/dist/cli.js +0 -1275
- package/dist/complexity.js +0 -98
- package/dist/config.js +0 -53
- package/dist/contextpack.js +0 -79
- package/dist/coupling.js +0 -35
- package/dist/crosslang.js +0 -425
- package/dist/diskcache.js +0 -97
- package/dist/explorer.js +0 -123
- package/dist/extractors/c.js +0 -204
- package/dist/extractors/common.js +0 -56
- package/dist/extractors/cpp.js +0 -272
- package/dist/extractors/csharp.js +0 -209
- package/dist/extractors/go.js +0 -212
- package/dist/extractors/java.js +0 -152
- package/dist/extractors/kotlin.js +0 -159
- package/dist/extractors/php.js +0 -208
- package/dist/extractors/python.js +0 -153
- package/dist/extractors/ruby.js +0 -146
- package/dist/extractors/rust.js +0 -249
- package/dist/extractors/swift.js +0 -192
- package/dist/extractors/typescript.js +0 -577
- package/dist/gitdiff.js +0 -178
- package/dist/graph-analysis.js +0 -279
- package/dist/graph.js +0 -165
- package/dist/html.js +0 -326
- package/dist/index.js +0 -1407
- package/dist/layers.js +0 -36
- package/dist/modulecoupling.js +0 -0
- package/dist/parser.js +0 -84
- package/dist/pool.js +0 -114
- package/dist/prompts.js +0 -67
- package/dist/registry.js +0 -87
- package/dist/report.js +0 -187
- package/dist/resolver.js +0 -222
- package/dist/roots.js +0 -47
- package/dist/search.js +0 -68
- package/dist/semantic.js +0 -365
- package/dist/sfc.js +0 -27
- package/dist/skeleton.js +0 -132
- package/dist/sourcemap.js +0 -60
- package/dist/testmap.js +0 -167
- package/dist/tsconfig.js +0 -212
- package/dist/typeflow.js +0 -124
- package/dist/types.js +0 -5
- package/dist/unused-params.js +0 -127
- package/dist/worker.js +0 -27
- package/dist/workspace.js +0 -330
package/dist/resolver.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/roots.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
export function parseRootsFromEnv(env = process.env) {
|
|
4
|
-
const raw = env.AST_MAP_ROOT ?? process.cwd();
|
|
5
|
-
const roots = raw
|
|
6
|
-
.split(path.delimiter)
|
|
7
|
-
.map((p) => p.trim())
|
|
8
|
-
.filter((p) => p.length > 0)
|
|
9
|
-
.map((p) => path.resolve(p));
|
|
10
|
-
return {
|
|
11
|
-
roots: roots.length > 0 ? roots : [path.resolve(process.cwd())],
|
|
12
|
-
unlocked: env.AST_MAP_UNLOCKED === "1",
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
function within(root, abs) {
|
|
16
|
-
const rel = path.relative(root, abs);
|
|
17
|
-
if (rel === "")
|
|
18
|
-
return path.basename(abs);
|
|
19
|
-
if (rel.startsWith("..") || path.isAbsolute(rel))
|
|
20
|
-
return null;
|
|
21
|
-
return rel;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Resolve a client-supplied path against the allowed roots.
|
|
25
|
-
* Throws when the path escapes every root and unlocked mode is off.
|
|
26
|
-
*/
|
|
27
|
-
export function resolvePathInRoots(input, cfg) {
|
|
28
|
-
const primary = cfg.roots[0];
|
|
29
|
-
const abs = path.resolve(primary, input);
|
|
30
|
-
for (const root of cfg.roots) {
|
|
31
|
-
const rel = within(root, abs);
|
|
32
|
-
if (rel !== null)
|
|
33
|
-
return { abs, rel, root };
|
|
34
|
-
}
|
|
35
|
-
if (cfg.unlocked) {
|
|
36
|
-
if (!fs.existsSync(abs)) {
|
|
37
|
-
throw new Error(`Path "${input}" does not exist (resolved to ${abs}).`);
|
|
38
|
-
}
|
|
39
|
-
const stat = fs.statSync(abs);
|
|
40
|
-
const root = stat.isDirectory() ? abs : path.dirname(abs);
|
|
41
|
-
return { abs, rel: path.basename(abs), root };
|
|
42
|
-
}
|
|
43
|
-
throw new Error(`Path "${input}" is outside the allowed root${cfg.roots.length > 1 ? "s" : ""} ` +
|
|
44
|
-
`(${cfg.roots.join(", ")}). Either set AST_MAP_ROOT to that project ` +
|
|
45
|
-
`(multiple roots allowed, separated by "${path.delimiter}"), or set ` +
|
|
46
|
-
`AST_MAP_UNLOCKED=1 to allow any absolute path.`);
|
|
47
|
-
}
|
package/dist/search.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { buildSkeleton, collectSourceFiles } from "./skeleton.js";
|
|
3
|
-
import { resolveOptions, loadProjectConfig } from "./config.js";
|
|
4
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
5
|
-
/** Recursively yield every symbol in a file, including nested ones. */
|
|
6
|
-
function* flattenSymbols(symbols, file, parentName) {
|
|
7
|
-
for (const sym of symbols) {
|
|
8
|
-
const fullName = parentName ? `${parentName}.${sym.name}` : sym.name;
|
|
9
|
-
yield {
|
|
10
|
-
file,
|
|
11
|
-
symbol: fullName,
|
|
12
|
-
kind: sym.kind,
|
|
13
|
-
exported: sym.exported ?? false,
|
|
14
|
-
range: sym.range,
|
|
15
|
-
...(sym.signature ? { signature: sym.signature } : {}),
|
|
16
|
-
};
|
|
17
|
-
if (sym.children.length > 0) {
|
|
18
|
-
yield* flattenSymbols(sym.children, file, fullName);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function makeMatcher(pattern, matchType) {
|
|
23
|
-
if (matchType === "exact") {
|
|
24
|
-
return (name) => name === pattern || name.endsWith(`.${pattern}`);
|
|
25
|
-
}
|
|
26
|
-
if (matchType === "regex") {
|
|
27
|
-
const re = new RegExp(pattern, "i");
|
|
28
|
-
return (name) => re.test(name);
|
|
29
|
-
}
|
|
30
|
-
// contains (default) — case-insensitive
|
|
31
|
-
const lower = pattern.toLowerCase();
|
|
32
|
-
return (name) => name.toLowerCase().includes(lower);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Search for symbols by name pattern across all source files in a directory.
|
|
36
|
-
* Traverses nested symbols (methods inside classes, etc.) with dot-notation names.
|
|
37
|
-
*
|
|
38
|
-
* @param dirAbs Absolute path of directory to scan.
|
|
39
|
-
* @param pattern Name to search for (matched per `matchType`).
|
|
40
|
-
* @param root Project root (for relative paths in results).
|
|
41
|
-
* @param options matchType, kind filter, exportedOnly, detail level.
|
|
42
|
-
*/
|
|
43
|
-
export async function searchSymbols(dirAbs, pattern, root, options = {}) {
|
|
44
|
-
const { matchType = "contains", kind, exportedOnly = false, detail = "outline" } = options;
|
|
45
|
-
const test = makeMatcher(pattern, matchType);
|
|
46
|
-
const opts = resolveOptions({ detail, emitHtml: false }, loadProjectConfig(root));
|
|
47
|
-
const files = collectSourceFiles(dirAbs, opts);
|
|
48
|
-
const results = [];
|
|
49
|
-
for (const file of files) {
|
|
50
|
-
const fileRel = path.relative(root, file).split(path.sep).join("/");
|
|
51
|
-
try {
|
|
52
|
-
const skel = await buildSkeleton(file, fileRel, opts);
|
|
53
|
-
for (const match of flattenSymbols(skel.symbols, skel.file)) {
|
|
54
|
-
if (!test(match.symbol))
|
|
55
|
-
continue;
|
|
56
|
-
if (kind && match.kind !== kind)
|
|
57
|
-
continue;
|
|
58
|
-
if (exportedOnly && !match.exported)
|
|
59
|
-
continue;
|
|
60
|
-
results.push(match);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// skip unreadable / unparseable files
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return results;
|
|
68
|
-
}
|