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.
Files changed (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +261 -12
  3. package/dist/ai-refactor.js +185 -0
  4. package/dist/ai-testgen.js +105 -0
  5. package/dist/analysis.js +134 -0
  6. package/dist/arch-rules.js +82 -0
  7. package/dist/callgraph.js +467 -0
  8. package/dist/check.js +112 -0
  9. package/dist/cli.js +2284 -0
  10. package/dist/complexity.js +98 -0
  11. package/dist/config.js +53 -0
  12. package/dist/contextpack.js +79 -0
  13. package/dist/coupling.js +35 -0
  14. package/dist/covmerge.js +176 -0
  15. package/dist/crosslang.js +425 -0
  16. package/dist/dashboard.js +259 -0
  17. package/dist/diagram.js +264 -0
  18. package/dist/diskcache.js +97 -0
  19. package/dist/docgen.js +156 -0
  20. package/dist/embeddings.js +136 -0
  21. package/dist/explain.js +123 -0
  22. package/dist/explorer.js +123 -0
  23. package/dist/extractors/c.js +204 -0
  24. package/dist/extractors/common.js +56 -0
  25. package/dist/extractors/cpp.js +272 -0
  26. package/dist/extractors/csharp.js +209 -0
  27. package/dist/extractors/go.js +212 -0
  28. package/dist/extractors/java.js +152 -0
  29. package/dist/extractors/kotlin.js +159 -0
  30. package/dist/extractors/php.js +208 -0
  31. package/dist/extractors/python.js +153 -0
  32. package/dist/extractors/ruby.js +146 -0
  33. package/dist/extractors/rust.js +249 -0
  34. package/dist/extractors/swift.js +192 -0
  35. package/dist/extractors/typescript.js +577 -0
  36. package/dist/fix.js +92 -0
  37. package/dist/gitdiff.js +178 -0
  38. package/dist/graph-analysis.js +279 -0
  39. package/dist/graph.js +165 -0
  40. package/dist/history.js +36 -0
  41. package/dist/html.js +658 -0
  42. package/dist/incremental.js +122 -0
  43. package/dist/index.js +1945 -0
  44. package/dist/indexstore.js +105 -0
  45. package/dist/layers.js +36 -0
  46. package/dist/lsp.js +238 -0
  47. package/dist/modulecoupling.js +0 -0
  48. package/dist/parser.js +84 -0
  49. package/dist/patch.js +199 -0
  50. package/dist/plugins.js +88 -0
  51. package/dist/pool.js +114 -0
  52. package/dist/prompts.js +67 -0
  53. package/dist/registry.js +87 -0
  54. package/dist/report.js +441 -0
  55. package/dist/resolver.js +222 -0
  56. package/dist/roots.js +47 -0
  57. package/dist/search.js +68 -0
  58. package/dist/security.js +178 -0
  59. package/dist/semantic.js +365 -0
  60. package/dist/serve.js +328 -0
  61. package/dist/sfc.js +27 -0
  62. package/dist/similar.js +98 -0
  63. package/dist/skeleton.js +132 -0
  64. package/dist/smells.js +285 -0
  65. package/dist/sourcemap.js +60 -0
  66. package/dist/testgen.js +280 -0
  67. package/dist/testmap.js +167 -0
  68. package/dist/tsconfig.js +212 -0
  69. package/dist/typeflow.js +124 -0
  70. package/dist/types.js +5 -0
  71. package/dist/unused-params.js +127 -0
  72. package/dist/webapp.js +646 -0
  73. package/dist/worker.js +27 -0
  74. package/dist/workspace.js +330 -0
  75. package/package.json +2 -1
@@ -0,0 +1,98 @@
1
+ import fs from "node:fs";
2
+ import { parseSource } from "./parser.js";
3
+ import { detectLanguage } from "./registry.js";
4
+ import { buildSkeleton } from "./skeleton.js";
5
+ import { resolveOptions } from "./config.js";
6
+ // ─── Decision points ───────────────────────────────────────────────────────────
7
+ /**
8
+ * Node types that introduce a branch (each adds 1 to cyclomatic complexity).
9
+ * This is a deliberately broad cross-language union; languages that use a node
10
+ * type not listed here simply undercount rather than miscount.
11
+ */
12
+ const DECISION_TYPES = new Set([
13
+ // conditionals
14
+ "if_statement", "if_expression", "elif_clause", "else_if_clause",
15
+ // loops
16
+ "for_statement", "for_in_statement", "for_of_statement", "enhanced_for_statement",
17
+ "for_expression", "while_statement", "while_expression", "do_statement", "loop_statement",
18
+ // switch / match arms (the default/else arm is intentionally excluded)
19
+ "switch_case", "expression_case", "type_case", "case_clause", "when_entry",
20
+ "when_clause", "match_arm", "case_statement",
21
+ // exception handlers
22
+ "catch_clause", "except_clause", "rescue_clause",
23
+ // ternary
24
+ "ternary_expression", "conditional_expression",
25
+ // python `and` / `or`
26
+ "boolean_operator",
27
+ ]);
28
+ const FN_KINDS = new Set(["function", "method"]);
29
+ function rate(c) {
30
+ if (c <= 5)
31
+ return "low";
32
+ if (c <= 10)
33
+ return "moderate";
34
+ if (c <= 20)
35
+ return "high";
36
+ return "very-high";
37
+ }
38
+ /** Collect the start line of every decision point in the tree. */
39
+ function collectDecisionLines(node, out) {
40
+ const t = node.type;
41
+ if (DECISION_TYPES.has(t)) {
42
+ out.push(node.startPosition.row + 1);
43
+ }
44
+ else if (t === "binary_expression") {
45
+ // Short-circuit operators add a branch; arithmetic operators do not.
46
+ const op = node.childForFieldName("operator");
47
+ if (op && (op.text === "&&" || op.text === "||"))
48
+ out.push(node.startPosition.row + 1);
49
+ }
50
+ for (let i = 0; i < node.namedChildCount; i++) {
51
+ const c = node.namedChild(i);
52
+ if (c)
53
+ collectDecisionLines(c, out);
54
+ }
55
+ }
56
+ function flatten(symbols, acc = []) {
57
+ for (const s of symbols) {
58
+ acc.push(s);
59
+ flatten(s.children, acc);
60
+ }
61
+ return acc;
62
+ }
63
+ /**
64
+ * Compute cyclomatic complexity for every function/method in a file.
65
+ * Complexity is attributed by line range, so a function's score includes the
66
+ * control flow of any closures/nested functions declared inside it.
67
+ */
68
+ export async function computeFileComplexity(absPath, relPath) {
69
+ const lang = detectLanguage(absPath);
70
+ if (!lang)
71
+ return null;
72
+ const source = fs.readFileSync(absPath, "utf8");
73
+ const root = await parseSource(lang.grammar, source);
74
+ const decisionLines = [];
75
+ collectDecisionLines(root, decisionLines);
76
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
77
+ const skel = await buildSkeleton(absPath, relPath, opts);
78
+ const functions = flatten(skel.symbols)
79
+ .filter((s) => FN_KINDS.has(s.kind))
80
+ .map((s) => {
81
+ const count = decisionLines.filter((l) => l >= s.range.startLine && l <= s.range.endLine).length;
82
+ const complexity = 1 + count;
83
+ return {
84
+ name: s.name,
85
+ kind: s.kind,
86
+ startLine: s.range.startLine,
87
+ endLine: s.range.endLine,
88
+ complexity,
89
+ rating: rate(complexity),
90
+ };
91
+ })
92
+ .sort((a, b) => b.complexity - a.complexity);
93
+ const maxComplexity = functions.reduce((m, f) => Math.max(m, f.complexity), 0);
94
+ const averageComplexity = functions.length === 0
95
+ ? 0
96
+ : Math.round((functions.reduce((s, f) => s + f.complexity, 0) / functions.length) * 10) / 10;
97
+ return { file: skel.file, functions, maxComplexity, averageComplexity };
98
+ }
package/dist/config.js ADDED
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export const DEFAULT_IGNORE = [
4
+ "node_modules",
5
+ "vendor",
6
+ ".git",
7
+ "dist",
8
+ "build",
9
+ ".next",
10
+ "out",
11
+ "__pycache__",
12
+ ".venv",
13
+ "venv",
14
+ ".ast-map",
15
+ ];
16
+ let _configCache = null;
17
+ /**
18
+ * Load .ast-map.config.json from the project root.
19
+ * Cached per root path + file mtime so live edits are picked up without restarting.
20
+ * Returns an empty config if no file is found.
21
+ */
22
+ export function loadProjectConfig(root) {
23
+ const configPath = path.join(root, ".ast-map.config.json");
24
+ let mtime = 0;
25
+ try {
26
+ mtime = fs.statSync(configPath).mtimeMs;
27
+ }
28
+ catch { /* file absent */ }
29
+ if (_configCache?.root === root && _configCache.mtime === mtime)
30
+ return _configCache.config;
31
+ let config = {};
32
+ try {
33
+ const raw = fs.readFileSync(configPath, "utf8");
34
+ config = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ // No config file or parse error — use defaults
38
+ }
39
+ _configCache = { root, mtime, config };
40
+ return config;
41
+ }
42
+ export function resolveOptions(opts = {}, projectConfig = {}) {
43
+ const extraIgnore = projectConfig.ignore ?? [];
44
+ const mergedIgnore = [...new Set([...DEFAULT_IGNORE, ...extraIgnore])];
45
+ return {
46
+ detail: opts.detail ?? "outline",
47
+ emitHtml: opts.emitHtml ?? true,
48
+ combineHtml: opts.combineHtml ?? false,
49
+ outputDir: opts.outputDir ?? projectConfig.outputDir,
50
+ ignore: opts.ignore ?? mergedIgnore,
51
+ maxFileBytes: opts.maxFileBytes ?? projectConfig.maxFileBytes ?? 2_000_000,
52
+ };
53
+ }
@@ -0,0 +1,79 @@
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 { resolveFileImports } from "./resolver.js";
6
+ import { buildCallGraph } from "./callgraph.js";
7
+ function findSym(syms, name) {
8
+ for (const s of syms) {
9
+ if (s.name === name)
10
+ return s;
11
+ const n = findSym(s.children, name);
12
+ if (n)
13
+ return n;
14
+ }
15
+ return null;
16
+ }
17
+ const tok = (s) => Math.round(s.length / 4);
18
+ /**
19
+ * Assemble the minimal context an agent needs to understand or change a symbol:
20
+ * the symbol's own source, the signatures of what it depends on (resolved
21
+ * imports), and the files that depend on it — instead of reading whole files.
22
+ */
23
+ export async function packContext(absFile, relFile, root, symbolName, scanDir) {
24
+ const opts = resolveOptions({ detail: "full", emitHtml: false });
25
+ const skel = await buildSkeleton(absFile, relFile, opts);
26
+ const lines = fs.readFileSync(absFile, "utf8").split(/\r?\n/);
27
+ let startLine = 1, endLine = lines.length;
28
+ if (symbolName) {
29
+ const sym = findSym(skel.symbols, symbolName);
30
+ if (sym) {
31
+ startLine = sym.range.startLine;
32
+ endLine = sym.range.endLine;
33
+ }
34
+ }
35
+ const source = lines.slice(startLine - 1, endLine).join("\n");
36
+ // Dependencies: resolved in-project imports + the target symbol signatures.
37
+ const refs = await resolveFileImports(skel, absFile, root);
38
+ const byFile = new Map();
39
+ for (const r of refs) {
40
+ if (!r.found || !r.resolvedRel)
41
+ continue;
42
+ const arr = byFile.get(r.resolvedRel) ?? [];
43
+ if (!arr.some((x) => x.name === r.symbol))
44
+ arr.push({ name: r.symbol, signature: r.signature ?? null });
45
+ byFile.set(r.resolvedRel, arr);
46
+ }
47
+ const dependencies = [...byFile.entries()].map(([file, symbols]) => ({ file, symbols }));
48
+ // Dependents: who calls the seed symbol (needs a directory scan).
49
+ let dependents = [];
50
+ if (symbolName && scanDir) {
51
+ const sopts = resolveOptions({ detail: "outline", emitHtml: false });
52
+ const skels = [];
53
+ for (const f of collectSourceFiles(scanDir, sopts)) {
54
+ const rr = path.relative(root, f).split(path.sep).join("/");
55
+ try {
56
+ skels.push(await buildSkeleton(f, rr, sopts));
57
+ }
58
+ catch { /* skip */ }
59
+ }
60
+ const cg = await buildCallGraph(absFile, symbolName, root, skels);
61
+ if (cg) {
62
+ const seen = new Set();
63
+ for (const c of cg.calledBy)
64
+ if (!seen.has(c.file)) {
65
+ seen.add(c.file);
66
+ dependents.push({ file: c.file });
67
+ }
68
+ }
69
+ }
70
+ const depTok = dependencies.reduce((a, d) => a + d.symbols.reduce((b, s) => b + tok(s.signature || s.name), 0), 0);
71
+ return {
72
+ seed: { file: relFile, ...(symbolName ? { symbol: symbolName } : {}) },
73
+ primary: { file: relFile, ...(symbolName ? { symbol: symbolName } : {}), startLine, endLine, source },
74
+ dependencies,
75
+ dependents,
76
+ tokenEstimate: tok(source) + depTok,
77
+ note: "Read primary.source in full; for dependencies you usually only need the listed signatures, not the whole files.",
78
+ };
79
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Compute Robert C. Martin's coupling metrics per file from the symbol graph's
3
+ * file-level import edges: afferent (fan-in), efferent (fan-out), and instability.
4
+ */
5
+ export function computeCoupling(graph) {
6
+ const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
7
+ const out = new Map(); // file -> set of files it imports
8
+ const inc = new Map(); // file -> set of files that import it
9
+ const files = new Set();
10
+ for (const n of graph.nodes)
11
+ if (n.nodeType === "file")
12
+ files.add(n.id);
13
+ for (const e of graph.edges) {
14
+ if (e.edgeType !== "imports")
15
+ continue;
16
+ const to = nodeMap.get(e.to);
17
+ const toFile = to ? (to.nodeType === "file" ? to.id : to.file) : null;
18
+ const fromFile = e.from;
19
+ if (!toFile || fromFile === toFile)
20
+ continue;
21
+ files.add(fromFile);
22
+ files.add(toFile);
23
+ (out.get(fromFile) ?? out.set(fromFile, new Set()).get(fromFile)).add(toFile);
24
+ (inc.get(toFile) ?? inc.set(toFile, new Set()).get(toFile)).add(fromFile);
25
+ }
26
+ const metrics = [];
27
+ for (const f of files) {
28
+ const ce = out.get(f)?.size ?? 0;
29
+ const ca = inc.get(f)?.size ?? 0;
30
+ const instability = ca + ce === 0 ? 0 : Math.round((ce / (ca + ce)) * 100) / 100;
31
+ metrics.push({ file: f, afferent: ca, efferent: ce, instability });
32
+ }
33
+ // Sort by total coupling desc (most-connected first).
34
+ return metrics.sort((a, b) => (b.afferent + b.efferent) - (a.afferent + a.efferent) || a.file.localeCompare(b.file));
35
+ }
@@ -0,0 +1,176 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ // ─── Format detection ─────────────────────────────────────────────────────────
4
+ export function detectFormat(reportPath) {
5
+ const ext = path.extname(reportPath).toLowerCase();
6
+ const base = path.basename(reportPath).toLowerCase();
7
+ if (ext === ".json") {
8
+ try {
9
+ const raw = JSON.parse(fs.readFileSync(reportPath, "utf8"));
10
+ if ("total" in raw && typeof raw.total === "object")
11
+ return "istanbul";
12
+ if ("version" in raw)
13
+ return "clover";
14
+ }
15
+ catch { /* fall through */ }
16
+ return "istanbul";
17
+ }
18
+ if (ext === ".lcov" || base.endsWith(".info") || base === "lcov.info")
19
+ return "lcov";
20
+ if (base.includes("clover"))
21
+ return "clover";
22
+ if (base.includes("cobertura"))
23
+ return "cobertura";
24
+ return "istanbul";
25
+ }
26
+ function parseIstanbul(reportPath) {
27
+ const raw = JSON.parse(fs.readFileSync(reportPath, "utf8"));
28
+ const results = [];
29
+ for (const [file, data] of Object.entries(raw)) {
30
+ if (file === "total")
31
+ continue;
32
+ const d = data;
33
+ results.push({
34
+ file: normalizeFile(file),
35
+ lineCoverage: (d.lines?.pct ?? 0) / 100,
36
+ branchCoverage: d.branches ? d.branches.pct / 100 : undefined,
37
+ functionCoverage: d.functions ? d.functions.pct / 100 : undefined,
38
+ lines: d.lines?.total,
39
+ coveredLines: d.lines?.covered,
40
+ });
41
+ }
42
+ return results;
43
+ }
44
+ // ─── lcov parser ─────────────────────────────────────────────────────────────
45
+ function parseLcov(reportPath) {
46
+ const text = fs.readFileSync(reportPath, "utf8");
47
+ const results = [];
48
+ let file = "";
49
+ let linesFound = 0, linesHit = 0, branchFound = 0, branchHit = 0;
50
+ for (const line of text.split("\n")) {
51
+ const l = line.trim();
52
+ if (l.startsWith("SF:")) {
53
+ file = normalizeFile(l.slice(3));
54
+ }
55
+ else if (l.startsWith("LF:")) {
56
+ linesFound = parseInt(l.slice(3), 10) || 0;
57
+ }
58
+ else if (l.startsWith("LH:")) {
59
+ linesHit = parseInt(l.slice(3), 10) || 0;
60
+ }
61
+ else if (l.startsWith("BRF:")) {
62
+ branchFound = parseInt(l.slice(4), 10) || 0;
63
+ }
64
+ else if (l.startsWith("BRH:")) {
65
+ branchHit = parseInt(l.slice(4), 10) || 0;
66
+ }
67
+ else if (l === "end_of_record" && file) {
68
+ results.push({
69
+ file,
70
+ lineCoverage: linesFound > 0 ? linesHit / linesFound : 0,
71
+ branchCoverage: branchFound > 0 ? branchHit / branchFound : undefined,
72
+ lines: linesFound,
73
+ coveredLines: linesHit,
74
+ });
75
+ file = "";
76
+ linesFound = 0;
77
+ linesHit = 0;
78
+ branchFound = 0;
79
+ branchHit = 0;
80
+ }
81
+ }
82
+ return results;
83
+ }
84
+ // ─── Cobertura / Clover XML parser (minimal) ──────────────────────────────────
85
+ function parseXmlCoverage(reportPath) {
86
+ const text = fs.readFileSync(reportPath, "utf8");
87
+ const results = [];
88
+ // Match <class filename="..." line-rate="..." branch-rate="...">
89
+ const classRe = /(?:filename|name)="([^"]+)"[^>]*(?:line-rate|lineRate)="([^"]+)"(?:[^>]*(?:branch-rate|branchRate)="([^"]+)")?/g;
90
+ let m;
91
+ while ((m = classRe.exec(text)) !== null) {
92
+ results.push({
93
+ file: normalizeFile(m[1]),
94
+ lineCoverage: parseFloat(m[2]) || 0,
95
+ branchCoverage: m[3] ? parseFloat(m[3]) : undefined,
96
+ });
97
+ }
98
+ return results;
99
+ }
100
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
101
+ function normalizeFile(f) {
102
+ return f.replace(/\\/g, "/").replace(/^\.\//, "");
103
+ }
104
+ function parseReport(reportPath, format) {
105
+ const effectiveFormat = format === "auto" ? detectFormat(reportPath) : format;
106
+ if (effectiveFormat === "lcov")
107
+ return parseLcov(reportPath);
108
+ if (effectiveFormat === "clover" || effectiveFormat === "cobertura")
109
+ return parseXmlCoverage(reportPath);
110
+ return parseIstanbul(reportPath);
111
+ }
112
+ // ─── Merge ────────────────────────────────────────────────────────────────────
113
+ export function mergeCoverage(reportPath, structuralMap, root, format = "auto") {
114
+ const actual = parseReport(reportPath, format === "auto" ? detectFormat(reportPath) : format);
115
+ const effectiveFormat = format === "auto" ? detectFormat(reportPath) : format;
116
+ // Index actual by normalised file path
117
+ const actualByFile = new Map();
118
+ for (const fc of actual) {
119
+ // Try multiple key forms: absolute, root-relative, basename
120
+ actualByFile.set(fc.file, fc);
121
+ actualByFile.set(path.relative(root, fc.file).replace(/\\/g, "/"), fc);
122
+ actualByFile.set(path.basename(fc.file), fc);
123
+ }
124
+ const testedSet = new Set(structuralMap.tested.map((f) => f.file));
125
+ const untestedSet = new Set(structuralMap.untested.map((f) => f.file));
126
+ const enriched = [];
127
+ const deadTests = [];
128
+ const uncovered = [];
129
+ for (const src of structuralMap.tested) {
130
+ const fc = actualByFile.get(src.file)
131
+ ?? actualByFile.get(path.relative(root, src.file))
132
+ ?? actualByFile.get(path.basename(src.file));
133
+ const lineCov = fc?.lineCoverage ?? 0;
134
+ if (lineCov === 0 && fc)
135
+ deadTests.push(src.file);
136
+ enriched.push({
137
+ file: src.file,
138
+ hasTests: true,
139
+ lineCoverage: lineCov,
140
+ branchCoverage: fc?.branchCoverage,
141
+ });
142
+ }
143
+ for (const src of structuralMap.untested) {
144
+ const fc = actualByFile.get(src.file)
145
+ ?? actualByFile.get(path.relative(root, src.file))
146
+ ?? actualByFile.get(path.basename(src.file));
147
+ enriched.push({
148
+ file: src.file,
149
+ hasTests: false,
150
+ lineCoverage: fc?.lineCoverage ?? 0,
151
+ branchCoverage: fc?.branchCoverage,
152
+ });
153
+ if (!fc || fc.lineCoverage === 0)
154
+ uncovered.push(src.file);
155
+ }
156
+ const totalLines = actual.reduce((s, f) => s + (f.lineCoverage ?? 0), 0);
157
+ const avgLineCoverage = actual.length > 0 ? totalLines / actual.length : 0;
158
+ const branchEntries = actual.filter((f) => f.branchCoverage !== undefined);
159
+ const avgBranchCoverage = branchEntries.length > 0
160
+ ? branchEntries.reduce((s, f) => s + (f.branchCoverage ?? 0), 0) / branchEntries.length
161
+ : undefined;
162
+ return {
163
+ format: effectiveFormat,
164
+ reportPath,
165
+ actual,
166
+ summary: {
167
+ totalFiles: actual.length,
168
+ coveredFiles: actual.filter((f) => f.lineCoverage > 0).length,
169
+ avgLineCoverage,
170
+ avgBranchCoverage,
171
+ },
172
+ enriched,
173
+ deadTests,
174
+ uncovered,
175
+ };
176
+ }