universal-ast-mapper 1.28.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 -338
- package/README.md +878 -878
- 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 -1408
- 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 -232
- 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/callgraph.js
DELETED
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { parseSource } from "./parser.js";
|
|
4
|
-
import { buildSkeleton } from "./skeleton.js";
|
|
5
|
-
import { resolveOptions, loadProjectConfig } from "./config.js";
|
|
6
|
-
import { detectLanguage } from "./registry.js";
|
|
7
|
-
import { resolveImportPath, resolveAliasedImport, getOrBuildCrossLangIndex } from "./resolver.js";
|
|
8
|
-
import { resolveCrossLangTarget } from "./crosslang.js";
|
|
9
|
-
const CROSS_LANG = new Set(["java", "csharp", "rust", "go", "kotlin", "c", "cpp", "swift"]);
|
|
10
|
-
function pushCall(out, callee, anchor) {
|
|
11
|
-
if (callee && anchor)
|
|
12
|
-
out.push({ callee, line: anchor.startPosition.row + 1 });
|
|
13
|
-
}
|
|
14
|
-
function collectCalls(node, out) {
|
|
15
|
-
const t = node.type;
|
|
16
|
-
// ── call_expression: TS/JS (member_expression) | Python "call" (attribute) |
|
|
17
|
-
// Go (selector_expression) | Rust (field_expression, scoped_identifier)
|
|
18
|
-
if (t === "call_expression" || t === "call") {
|
|
19
|
-
const fn = node.childForFieldName("function");
|
|
20
|
-
if (fn) {
|
|
21
|
-
let callee = null;
|
|
22
|
-
switch (fn.type) {
|
|
23
|
-
case "identifier":
|
|
24
|
-
callee = fn.text;
|
|
25
|
-
break;
|
|
26
|
-
case "member_expression":
|
|
27
|
-
case "attribute": {
|
|
28
|
-
const obj = fn.childForFieldName("object");
|
|
29
|
-
const prop = fn.childForFieldName("property") ?? fn.childForFieldName("attribute");
|
|
30
|
-
if (prop)
|
|
31
|
-
callee = obj ? `${obj.text}.${prop.text}` : prop.text;
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
case "field_expression": {
|
|
35
|
-
// Rust: inv.reserve — fields are `value` and `field`
|
|
36
|
-
const obj = fn.childForFieldName("value");
|
|
37
|
-
const fld = fn.childForFieldName("field");
|
|
38
|
-
if (fld)
|
|
39
|
-
callee = obj ? `${obj.text}.${fld.text}` : fld.text;
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
case "scoped_identifier":
|
|
43
|
-
// Rust: String::from / helpers::format — keep full path
|
|
44
|
-
callee = fn.text;
|
|
45
|
-
break;
|
|
46
|
-
case "selector_expression": {
|
|
47
|
-
// Go: pkg.Func
|
|
48
|
-
const obj = fn.childForFieldName("operand");
|
|
49
|
-
const fld = fn.childForFieldName("field");
|
|
50
|
-
if (fld)
|
|
51
|
-
callee = obj ? `${obj.text}.${fld.text}` : fld.text;
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
pushCall(out, callee, fn);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
// Kotlin: call_expression has no `function` field — the callee is the
|
|
59
|
-
// first named child (a simple_identifier for `Foo(...)` / a bare call,
|
|
60
|
-
// or a navigation_expression for `obj.method(...)`).
|
|
61
|
-
const callee0 = node.namedChild(0);
|
|
62
|
-
if (callee0) {
|
|
63
|
-
if (callee0.type === "simple_identifier" || callee0.type === "identifier") {
|
|
64
|
-
pushCall(out, callee0.text, callee0);
|
|
65
|
-
}
|
|
66
|
-
else if (callee0.type === "navigation_expression") {
|
|
67
|
-
pushCall(out, callee0.text.replace(/\s+/g, ""), callee0);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// ── Java method invocation
|
|
73
|
-
else if (t === "method_invocation") {
|
|
74
|
-
const name = node.childForFieldName("name");
|
|
75
|
-
const obj = node.childForFieldName("object");
|
|
76
|
-
if (name)
|
|
77
|
-
pushCall(out, obj ? `${obj.text}.${name.text}` : name.text, name);
|
|
78
|
-
}
|
|
79
|
-
// ── C# invocation expression
|
|
80
|
-
else if (t === "invocation_expression") {
|
|
81
|
-
const fn = node.childForFieldName("function");
|
|
82
|
-
if (fn)
|
|
83
|
-
pushCall(out, fn.text, fn);
|
|
84
|
-
}
|
|
85
|
-
// ── Java + C# constructor call: new Foo(...)
|
|
86
|
-
else if (t === "object_creation_expression") {
|
|
87
|
-
let typeNode = node.childForFieldName("type");
|
|
88
|
-
if (!typeNode) {
|
|
89
|
-
for (let i = 0; i < node.namedChildCount; i++) {
|
|
90
|
-
const c = node.namedChild(i);
|
|
91
|
-
if (c &&
|
|
92
|
-
(c.type === "identifier" ||
|
|
93
|
-
c.type === "type_identifier" ||
|
|
94
|
-
c.type === "scoped_identifier" ||
|
|
95
|
-
c.type === "qualified_name" ||
|
|
96
|
-
c.type === "generic_type")) {
|
|
97
|
-
typeNode = c;
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (typeNode)
|
|
103
|
-
pushCall(out, `new ${typeNode.text}`, typeNode);
|
|
104
|
-
}
|
|
105
|
-
for (let i = 0; i < node.namedChildCount; i++) {
|
|
106
|
-
const c = node.namedChild(i);
|
|
107
|
-
if (c)
|
|
108
|
-
collectCalls(c, out);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// ─── Function-node finder ─────────────────────────────────────────────────────
|
|
112
|
-
const FUNCTION_NODE_TYPES = new Set([
|
|
113
|
-
"function_declaration", // TS / JS / Go
|
|
114
|
-
"generator_function_declaration",
|
|
115
|
-
"method_definition", // TS / JS class member
|
|
116
|
-
"method_signature",
|
|
117
|
-
"abstract_method_signature",
|
|
118
|
-
"function_definition", // Python
|
|
119
|
-
"async_function_definition", // Python async
|
|
120
|
-
"method_declaration", // Go / Java / C#
|
|
121
|
-
"constructor_declaration", // Java / C#
|
|
122
|
-
"function_item", // Rust
|
|
123
|
-
]);
|
|
124
|
-
function findFunctionNode(root, name) {
|
|
125
|
-
function walk(node) {
|
|
126
|
-
if (FUNCTION_NODE_TYPES.has(node.type)) {
|
|
127
|
-
const named = node.childForFieldName("name");
|
|
128
|
-
if (named?.text === name)
|
|
129
|
-
return node;
|
|
130
|
-
// Kotlin: function_declaration exposes its name as a simple_identifier
|
|
131
|
-
// child, not via a `name` field.
|
|
132
|
-
if (!named && node.type === "function_declaration") {
|
|
133
|
-
const id = node.namedChild(0);
|
|
134
|
-
if (id?.type === "simple_identifier" && id.text === name)
|
|
135
|
-
return node;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// const foo = () => ... | const foo = function() ...
|
|
139
|
-
if (node.type === "variable_declarator") {
|
|
140
|
-
const declName = node.childForFieldName("name")?.text;
|
|
141
|
-
const value = node.childForFieldName("value");
|
|
142
|
-
if (declName === name &&
|
|
143
|
-
value &&
|
|
144
|
-
(value.type === "arrow_function" ||
|
|
145
|
-
value.type === "function" ||
|
|
146
|
-
value.type === "function_expression")) {
|
|
147
|
-
return value;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
for (let i = 0; i < node.namedChildCount; i++) {
|
|
151
|
-
const c = node.namedChild(i);
|
|
152
|
-
if (c) {
|
|
153
|
-
const found = walk(c);
|
|
154
|
-
if (found)
|
|
155
|
-
return found;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
return walk(root);
|
|
161
|
-
}
|
|
162
|
-
// ─── Destructuring alias tracker (TS/JS only) ─────────────────────────────────
|
|
163
|
-
function collectDestructuredAliases(node, importMap) {
|
|
164
|
-
const aliases = new Map();
|
|
165
|
-
function walk(n) {
|
|
166
|
-
if (n.type === "variable_declarator") {
|
|
167
|
-
const nameNode = n.childForFieldName("name");
|
|
168
|
-
const valueNode = n.childForFieldName("value");
|
|
169
|
-
if (nameNode && valueNode && nameNode.type === "object_pattern") {
|
|
170
|
-
const baseName = valueNode.text.split(".")[0];
|
|
171
|
-
const originRef = importMap.get(baseName);
|
|
172
|
-
const origin = originRef?.from ?? aliases.get(baseName);
|
|
173
|
-
if (origin) {
|
|
174
|
-
for (let i = 0; i < nameNode.namedChildCount; i++) {
|
|
175
|
-
const prop = nameNode.namedChild(i);
|
|
176
|
-
if (!prop)
|
|
177
|
-
continue;
|
|
178
|
-
if (prop.type === "shorthand_property_identifier_pattern" ||
|
|
179
|
-
prop.type === "shorthand_property_identifier") {
|
|
180
|
-
aliases.set(prop.text, origin);
|
|
181
|
-
}
|
|
182
|
-
if (prop.type === "pair_pattern") {
|
|
183
|
-
const val = prop.childForFieldName("value");
|
|
184
|
-
if (val)
|
|
185
|
-
aliases.set(val.text, origin);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
for (let i = 0; i < n.namedChildCount; i++) {
|
|
192
|
-
const c = n.namedChild(i);
|
|
193
|
-
if (c)
|
|
194
|
-
walk(c);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
walk(node);
|
|
198
|
-
return aliases;
|
|
199
|
-
}
|
|
200
|
-
// ─── Base identifier of a callee expression ───────────────────────────────────
|
|
201
|
-
/** Take the leftmost identifier from "obj.method" / "Pkg::func" / "new Foo". */
|
|
202
|
-
function baseNameOf(callee) {
|
|
203
|
-
let s = callee;
|
|
204
|
-
if (s.startsWith("new "))
|
|
205
|
-
s = s.slice(4);
|
|
206
|
-
return s.split(/::|\./)[0];
|
|
207
|
-
}
|
|
208
|
-
// ─── Cross-language calledBy scan helper ──────────────────────────────────────
|
|
209
|
-
/** Last segment of a member-style callee — "Helper.fmt" -> "fmt", "compute" -> null. */
|
|
210
|
-
function memberOf(callee) {
|
|
211
|
-
const noNew = callee.startsWith("new ") ? callee.slice(4) : callee;
|
|
212
|
-
const parts = noNew.split(/::|\./);
|
|
213
|
-
return parts.length > 1 ? parts[parts.length - 1] : null;
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Open a file, parse it, and check whether any call expression references
|
|
217
|
-
* `funcName` — either as a bare call `funcName(...)` or as the trailing
|
|
218
|
-
* member of a qualified call `X.funcName(...)` / `X::funcName(...)`.
|
|
219
|
-
* Used for C# / Go reverse calledBy where namespace/package imports do not
|
|
220
|
-
* name the called symbol.
|
|
221
|
-
*/
|
|
222
|
-
async function fileCallsSymbol(fileAbs, funcName) {
|
|
223
|
-
const lang = detectLanguage(fileAbs);
|
|
224
|
-
if (!lang)
|
|
225
|
-
return false;
|
|
226
|
-
let src;
|
|
227
|
-
try {
|
|
228
|
-
src = fs.readFileSync(fileAbs, "utf8");
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
const root = await parseSource(lang.grammar, src);
|
|
234
|
-
const calls = [];
|
|
235
|
-
collectCalls(root, calls);
|
|
236
|
-
for (const c of calls) {
|
|
237
|
-
if (c.callee === funcName)
|
|
238
|
-
return true;
|
|
239
|
-
const m = memberOf(c.callee);
|
|
240
|
-
if (m === funcName)
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
246
|
-
/** Recursively find the first symbol with the given name and return its decorators. */
|
|
247
|
-
function findDecorators(symbols, name) {
|
|
248
|
-
for (const s of symbols) {
|
|
249
|
-
if (s.name === name && s.decorators && s.decorators.length > 0)
|
|
250
|
-
return s.decorators;
|
|
251
|
-
const nested = findDecorators(s.children, name);
|
|
252
|
-
if (nested)
|
|
253
|
-
return nested;
|
|
254
|
-
}
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
export async function buildCallGraph(filePath, funcName, root, allSkeletons) {
|
|
258
|
-
const langEntry = detectLanguage(filePath);
|
|
259
|
-
if (!langEntry)
|
|
260
|
-
return null;
|
|
261
|
-
const source = fs.readFileSync(filePath, "utf8");
|
|
262
|
-
const relPath = path.relative(root, filePath).split(path.sep).join("/");
|
|
263
|
-
const rootNode = await parseSource(langEntry.grammar, source);
|
|
264
|
-
const funcNode = findFunctionNode(rootNode, funcName);
|
|
265
|
-
if (!funcNode)
|
|
266
|
-
return null;
|
|
267
|
-
const body = funcNode.childForFieldName("body") ?? funcNode;
|
|
268
|
-
const rawCalls = [];
|
|
269
|
-
collectCalls(body, rawCalls);
|
|
270
|
-
const opts = resolveOptions({ detail: "outline", emitHtml: false }, loadProjectConfig(root));
|
|
271
|
-
const skel = await buildSkeleton(filePath, relPath, opts);
|
|
272
|
-
// localName -> full ImportRef (so cross-lang resolution has the flags it needs)
|
|
273
|
-
const importMap = new Map();
|
|
274
|
-
for (const imp of skel.imports ?? []) {
|
|
275
|
-
if (imp.symbol !== "*" && !imp.isSideEffect) {
|
|
276
|
-
importMap.set(imp.alias ?? imp.symbol, imp);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
const localNames = new Set(skel.symbols.map((s) => s.name));
|
|
280
|
-
const destructuredAliases = collectDestructuredAliases(body, importMap);
|
|
281
|
-
// Build cross-lang index lazily — needed for Java/C#/Rust dispatch.
|
|
282
|
-
const isCrossLang = CROSS_LANG.has(skel.language);
|
|
283
|
-
const crossIndex = isCrossLang ? await getOrBuildCrossLangIndex(root) : null;
|
|
284
|
-
const calls = [];
|
|
285
|
-
const seen = new Set();
|
|
286
|
-
for (const { callee, line } of rawCalls) {
|
|
287
|
-
const key = `${callee}:${line}`;
|
|
288
|
-
if (seen.has(key))
|
|
289
|
-
continue;
|
|
290
|
-
seen.add(key);
|
|
291
|
-
const base = baseNameOf(callee);
|
|
292
|
-
const importRef = importMap.get(base);
|
|
293
|
-
const aliasOrigin = destructuredAliases.get(base);
|
|
294
|
-
const call = { callee, line };
|
|
295
|
-
if (importRef) {
|
|
296
|
-
if (isCrossLang && crossIndex) {
|
|
297
|
-
const target = resolveCrossLangTarget(importRef, skel, filePath, root, crossIndex);
|
|
298
|
-
if (target) {
|
|
299
|
-
if (target.kind === "symbol")
|
|
300
|
-
call.calleeFileRel = target.file;
|
|
301
|
-
else if (target.files.length > 0)
|
|
302
|
-
call.calleeFileRel = target.files[0];
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
call.isExternal = true;
|
|
306
|
-
call.calleeFileRel = importRef.from;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
else if (importRef.from.startsWith(".")) {
|
|
310
|
-
const resolvedAbs = resolveImportPath(importRef.from, filePath);
|
|
311
|
-
if (resolvedAbs) {
|
|
312
|
-
call.calleeFileRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
const aliasAbs = resolveAliasedImport(importRef.from, filePath);
|
|
317
|
-
if (aliasAbs) {
|
|
318
|
-
call.calleeFileRel = path.relative(root, aliasAbs).split(path.sep).join("/");
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
call.isExternal = true;
|
|
322
|
-
call.calleeFileRel = importRef.from;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
else if (aliasOrigin) {
|
|
327
|
-
// Destructured aliases are TS/JS only (always relative or external).
|
|
328
|
-
if (aliasOrigin.startsWith(".")) {
|
|
329
|
-
const resolvedAbs = resolveImportPath(aliasOrigin, filePath);
|
|
330
|
-
if (resolvedAbs) {
|
|
331
|
-
call.calleeFileRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
const aliasAbs = resolveAliasedImport(aliasOrigin, filePath);
|
|
336
|
-
if (aliasAbs) {
|
|
337
|
-
call.calleeFileRel = path.relative(root, aliasAbs).split(path.sep).join("/");
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
call.isExternal = true;
|
|
341
|
-
call.calleeFileRel = aliasOrigin;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
else if (crossIndex && skel.language === "csharp") {
|
|
346
|
-
// C# `using App.Models;` makes types visible without naming them.
|
|
347
|
-
// Try `<usingNs>.<base>` against the type-by-fqn index.
|
|
348
|
-
for (const ns of skel.imports ?? []) {
|
|
349
|
-
if (!ns.isNamespaceImport)
|
|
350
|
-
continue;
|
|
351
|
-
const f = crossIndex.csharpTypes.get(`${ns.from}.${base}`);
|
|
352
|
-
if (f && f !== skel.file) {
|
|
353
|
-
call.calleeFileRel = f;
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (!call.calleeFileRel && localNames.has(base))
|
|
358
|
-
call.isLocal = true;
|
|
359
|
-
}
|
|
360
|
-
else if (crossIndex && skel.language === "java") {
|
|
361
|
-
// Java wildcard import: `import com.example.*;` doesn't name the type.
|
|
362
|
-
for (const wc of skel.imports ?? []) {
|
|
363
|
-
if (wc.symbol !== "*")
|
|
364
|
-
continue;
|
|
365
|
-
const f = crossIndex.javaFqcn.get(`${wc.from}.${base}`);
|
|
366
|
-
if (f && f !== skel.file) {
|
|
367
|
-
call.calleeFileRel = f;
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (!call.calleeFileRel && localNames.has(base))
|
|
372
|
-
call.isLocal = true;
|
|
373
|
-
}
|
|
374
|
-
else if (localNames.has(base)) {
|
|
375
|
-
call.isLocal = true;
|
|
376
|
-
}
|
|
377
|
-
calls.push(call);
|
|
378
|
-
}
|
|
379
|
-
// ── calledBy: who imports this function? ────────────────────────────────
|
|
380
|
-
const calledBy = [];
|
|
381
|
-
if (allSkeletons) {
|
|
382
|
-
for (const otherSkel of allSkeletons) {
|
|
383
|
-
if (otherSkel.file === relPath)
|
|
384
|
-
continue;
|
|
385
|
-
const otherIsCrossLang = CROSS_LANG.has(otherSkel.language);
|
|
386
|
-
const otherAbs = path.resolve(root, otherSkel.file);
|
|
387
|
-
for (const imp of otherSkel.imports ?? []) {
|
|
388
|
-
const importedName = imp.alias ?? imp.symbol;
|
|
389
|
-
if (importedName !== funcName && imp.symbol !== funcName)
|
|
390
|
-
continue;
|
|
391
|
-
if (otherIsCrossLang) {
|
|
392
|
-
// Symbol-level cross-lang match only — file/namespace edges are too
|
|
393
|
-
// broad to claim "this file calls funcName".
|
|
394
|
-
if (!crossIndex)
|
|
395
|
-
continue;
|
|
396
|
-
const target = resolveCrossLangTarget(imp, otherSkel, otherAbs, root, crossIndex);
|
|
397
|
-
if (target && target.kind === "symbol" && target.file === relPath && target.symbol === funcName) {
|
|
398
|
-
calledBy.push({ file: otherSkel.file });
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
const resolvedAbs = imp.from.startsWith(".")
|
|
404
|
-
? resolveImportPath(imp.from, otherAbs)
|
|
405
|
-
: resolveAliasedImport(imp.from, otherAbs);
|
|
406
|
-
if (!resolvedAbs)
|
|
407
|
-
continue;
|
|
408
|
-
const resolvedRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
|
|
409
|
-
if (resolvedRel === relPath) {
|
|
410
|
-
calledBy.push({ file: otherSkel.file });
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
// Extra pass: for C# / Go, the cross-lang resolver gives file-level targets
|
|
418
|
-
// (namespace / package) so the loop above misses callers that only show up
|
|
419
|
-
// via name-resolution at the call site. Scan candidate files' call sites.
|
|
420
|
-
if (allSkeletons &&
|
|
421
|
-
crossIndex &&
|
|
422
|
-
(skel.language === "csharp" || skel.language === "go")) {
|
|
423
|
-
const seenFiles = new Set(calledBy.map((c) => c.file));
|
|
424
|
-
for (const otherSkel of allSkeletons) {
|
|
425
|
-
if (otherSkel.file === relPath)
|
|
426
|
-
continue;
|
|
427
|
-
if (otherSkel.language !== skel.language)
|
|
428
|
-
continue;
|
|
429
|
-
if (seenFiles.has(otherSkel.file))
|
|
430
|
-
continue;
|
|
431
|
-
const otherAbs = path.resolve(root, otherSkel.file);
|
|
432
|
-
// Confirm this other file imports / uses something that resolves to us.
|
|
433
|
-
let importsUs = false;
|
|
434
|
-
for (const imp of otherSkel.imports ?? []) {
|
|
435
|
-
const target = resolveCrossLangTarget(imp, otherSkel, otherAbs, root, crossIndex);
|
|
436
|
-
if (!target)
|
|
437
|
-
continue;
|
|
438
|
-
if (target.kind === "file" && target.files.includes(relPath)) {
|
|
439
|
-
importsUs = true;
|
|
440
|
-
break;
|
|
441
|
-
}
|
|
442
|
-
if (target.kind === "symbol" && target.file === relPath) {
|
|
443
|
-
importsUs = true;
|
|
444
|
-
break;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
if (!importsUs)
|
|
448
|
-
continue;
|
|
449
|
-
if (await fileCallsSymbol(otherAbs, funcName)) {
|
|
450
|
-
calledBy.push({ file: otherSkel.file });
|
|
451
|
-
seenFiles.add(otherSkel.file);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
const decorators = findDecorators(skel.symbols, funcName);
|
|
456
|
-
return {
|
|
457
|
-
file: relPath,
|
|
458
|
-
function: funcName,
|
|
459
|
-
functionRange: {
|
|
460
|
-
startLine: funcNode.startPosition.row + 1,
|
|
461
|
-
endLine: funcNode.endPosition.row + 1,
|
|
462
|
-
},
|
|
463
|
-
...(decorators ? { decorators } : {}),
|
|
464
|
-
calls,
|
|
465
|
-
calledBy,
|
|
466
|
-
};
|
|
467
|
-
}
|
package/dist/check.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { buildReport } from "./report.js";
|
|
4
|
-
export const BASELINE_FILENAME = ".ast-map.baseline.json";
|
|
5
|
-
export function metricsFromReport(r) {
|
|
6
|
-
return {
|
|
7
|
-
fileCount: r.fileCount,
|
|
8
|
-
symbolCount: r.symbolCount,
|
|
9
|
-
cycles: r.cycles.count,
|
|
10
|
-
deadExports: r.dead.count,
|
|
11
|
-
sdpViolations: r.layerViolations.count,
|
|
12
|
-
veryHighComplexity: r.complexity.hotspots.filter((h) => h.complexity > 20).length,
|
|
13
|
-
maxComplexity: r.complexity.max,
|
|
14
|
-
score: r.score,
|
|
15
|
-
grade: r.grade,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
function readBaseline(file) {
|
|
19
|
-
try {
|
|
20
|
-
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
21
|
-
return raw.metrics ?? null;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function checkThresholds(m, t, out) {
|
|
28
|
-
const rules = [
|
|
29
|
-
["maxCycles", "cycles", "max"],
|
|
30
|
-
["maxDeadExports", "deadExports", "max"],
|
|
31
|
-
["maxSdpViolations", "sdpViolations", "max"],
|
|
32
|
-
["maxVeryHighComplexity", "veryHighComplexity", "max"],
|
|
33
|
-
["maxComplexity", "maxComplexity", "max"],
|
|
34
|
-
["minScore", "score", "min"],
|
|
35
|
-
];
|
|
36
|
-
for (const [tKey, mKey, dir] of rules) {
|
|
37
|
-
const limit = t[tKey];
|
|
38
|
-
if (limit === undefined)
|
|
39
|
-
continue;
|
|
40
|
-
const actual = m[mKey];
|
|
41
|
-
const bad = dir === "max" ? actual > limit : actual < limit;
|
|
42
|
-
if (bad) {
|
|
43
|
-
out.push({
|
|
44
|
-
kind: "threshold",
|
|
45
|
-
metric: mKey,
|
|
46
|
-
limit,
|
|
47
|
-
actual,
|
|
48
|
-
message: `${mKey} is ${actual}, ${dir === "max" ? "exceeds max" : "below min"} ${limit}`,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/** Metrics where an increase vs the baseline is a regression. */
|
|
54
|
-
const RATCHET_UP = [
|
|
55
|
-
"cycles",
|
|
56
|
-
"deadExports",
|
|
57
|
-
"sdpViolations",
|
|
58
|
-
"veryHighComplexity",
|
|
59
|
-
];
|
|
60
|
-
function checkBaseline(m, base, out) {
|
|
61
|
-
for (const key of RATCHET_UP) {
|
|
62
|
-
const was = base[key];
|
|
63
|
-
const now = m[key];
|
|
64
|
-
if (now > was) {
|
|
65
|
-
out.push({
|
|
66
|
-
kind: "regression",
|
|
67
|
-
metric: key,
|
|
68
|
-
limit: was,
|
|
69
|
-
actual: now,
|
|
70
|
-
message: `${key} regressed: ${was} → ${now} (baseline ratchet)`,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (m.score < base.score) {
|
|
75
|
-
out.push({
|
|
76
|
-
kind: "regression",
|
|
77
|
-
metric: "score",
|
|
78
|
-
limit: base.score,
|
|
79
|
-
actual: m.score,
|
|
80
|
-
message: `health score regressed: ${base.score} → ${m.score}`,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
export async function runQualityGate(absDir, root, opts = {}) {
|
|
85
|
-
const report = await buildReport(absDir, root);
|
|
86
|
-
const metrics = metricsFromReport(report);
|
|
87
|
-
const baselinePath = path.resolve(root, opts.baselinePath ?? BASELINE_FILENAME);
|
|
88
|
-
const baseline = readBaseline(baselinePath);
|
|
89
|
-
const failures = [];
|
|
90
|
-
if (opts.thresholds)
|
|
91
|
-
checkThresholds(metrics, opts.thresholds, failures);
|
|
92
|
-
if (baseline)
|
|
93
|
-
checkBaseline(metrics, baseline, failures);
|
|
94
|
-
let baselineUpdated = false;
|
|
95
|
-
if (opts.updateBaseline) {
|
|
96
|
-
const doc = {
|
|
97
|
-
tool: "universal-ast-mapper",
|
|
98
|
-
updatedAt: new Date().toISOString(),
|
|
99
|
-
metrics,
|
|
100
|
-
};
|
|
101
|
-
fs.writeFileSync(baselinePath, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
102
|
-
baselineUpdated = true;
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
passed: failures.length === 0,
|
|
106
|
-
metrics,
|
|
107
|
-
baseline,
|
|
108
|
-
baselinePath,
|
|
109
|
-
baselineUpdated,
|
|
110
|
-
failures,
|
|
111
|
-
};
|
|
112
|
-
}
|