@snapback/cli 1.1.12 → 1.1.15
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/README.md +79 -18
- package/dist/SkippedTestDetector-AXTMWWHC.js +5 -0
- package/dist/SkippedTestDetector-QLSQV7K7.js +5 -0
- package/dist/analysis-6WTBZJH3.js +6 -0
- package/dist/analysis-C472LUGW.js +2475 -0
- package/dist/auth-HFJRXXG2.js +1446 -0
- package/dist/auto-provision-organization-SF6XM7X4.js +161 -0
- package/dist/chunk-23G5VYA3.js +4259 -0
- package/dist/{chunk-QAKFE3NE.js → chunk-4YTE4JEW.js} +3 -4
- package/dist/chunk-5EOPYJ4Y.js +12 -0
- package/dist/{chunk-G7QXHNGB.js → chunk-5SQA44V7.js} +1125 -32
- package/dist/{chunk-BW7RALUZ.js → chunk-7ADPL4Q3.js} +11 -4
- package/dist/chunk-CBGOC6RV.js +293 -0
- package/dist/chunk-DNEADD2G.js +3499 -0
- package/dist/{chunk-NKBZIXCN.js → chunk-DPWFZNMY.js} +122 -15
- package/dist/chunk-GQ73B37K.js +314 -0
- package/dist/chunk-HR34NJP7.js +6133 -0
- package/dist/chunk-ICKSHS3A.js +2264 -0
- package/dist/{chunk-KPETDXQO.js → chunk-OI2HNNT6.js} +565 -50
- package/dist/chunk-PL4HF4M2.js +593 -0
- package/dist/chunk-WS36HDEU.js +3735 -0
- package/dist/chunk-XYU5FFE3.js +111 -0
- package/dist/chunk-ZBQDE6WJ.js +108 -0
- package/dist/client-WIO6W447.js +8 -0
- package/dist/dist-E7E2T3DQ.js +9 -0
- package/dist/dist-TEWNOZYS.js +5 -0
- package/dist/dist-YZBJAYEJ.js +12 -0
- package/dist/index.js +65215 -26627
- package/dist/local-service-adapter-3JHN6G4O.js +6 -0
- package/dist/pioneer-oauth-hook-V2JKEXM7.js +12 -0
- package/dist/{secure-credentials-6UMEU22H.js → secure-credentials-UEPG7GWW.js} +15 -8
- package/dist/snapback-dir-MG7DTRMF.js +6 -0
- package/package.json +8 -42
- package/scripts/postinstall.mjs +2 -3
- package/dist/SkippedTestDetector-B3JZUE5G.js +0 -5
- package/dist/SkippedTestDetector-B3JZUE5G.js.map +0 -1
- package/dist/analysis-Z53F5FT2.js +0 -6
- package/dist/analysis-Z53F5FT2.js.map +0 -1
- package/dist/chunk-6MR2TINI.js +0 -27
- package/dist/chunk-6MR2TINI.js.map +0 -1
- package/dist/chunk-BW7RALUZ.js.map +0 -1
- package/dist/chunk-G7QXHNGB.js.map +0 -1
- package/dist/chunk-ISVRGBWT.js +0 -16223
- package/dist/chunk-ISVRGBWT.js.map +0 -1
- package/dist/chunk-KPETDXQO.js.map +0 -1
- package/dist/chunk-NKBZIXCN.js.map +0 -1
- package/dist/chunk-QAKFE3NE.js.map +0 -1
- package/dist/chunk-YOVA65PS.js +0 -12745
- package/dist/chunk-YOVA65PS.js.map +0 -1
- package/dist/dist-7UKXVKH3.js +0 -5
- package/dist/dist-7UKXVKH3.js.map +0 -1
- package/dist/dist-VDK7WEF4.js +0 -5
- package/dist/dist-VDK7WEF4.js.map +0 -1
- package/dist/dist-WKLJSPJT.js +0 -8
- package/dist/dist-WKLJSPJT.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/secure-credentials-6UMEU22H.js.map +0 -1
- package/dist/snapback-dir-T3CRQRY6.js +0 -6
- package/dist/snapback-dir-T3CRQRY6.js.map +0 -1
|
@@ -1,10 +1,900 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { __name } from './chunk-
|
|
1
|
+
#!/usr/bin/env node --no-warnings=ExperimentalWarning
|
|
2
|
+
import { __name } from './chunk-7ADPL4Q3.js';
|
|
3
|
+
import { parseSync } from 'oxc-parser';
|
|
4
|
+
import { dirname, resolve, relative, basename } from 'path';
|
|
3
5
|
import * as eslintParser from '@typescript-eslint/parser';
|
|
4
6
|
import { parse } from '@babel/parser';
|
|
5
7
|
import traverse from '@babel/traverse';
|
|
6
|
-
import { dirname, relative, basename } from 'path';
|
|
7
8
|
|
|
9
|
+
process.env.SNAPBACK_CLI='true';
|
|
10
|
+
var TS_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
11
|
+
".ts",
|
|
12
|
+
".tsx",
|
|
13
|
+
".mts",
|
|
14
|
+
".cts"
|
|
15
|
+
]);
|
|
16
|
+
var JSX_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
17
|
+
".tsx",
|
|
18
|
+
".jsx"
|
|
19
|
+
]);
|
|
20
|
+
var ALL_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
21
|
+
".ts",
|
|
22
|
+
".tsx",
|
|
23
|
+
".js",
|
|
24
|
+
".jsx",
|
|
25
|
+
".mts",
|
|
26
|
+
".cts",
|
|
27
|
+
".mjs",
|
|
28
|
+
".cjs"
|
|
29
|
+
]);
|
|
30
|
+
function isSupportedFile(filePath) {
|
|
31
|
+
const ext = getExtension(filePath);
|
|
32
|
+
return ALL_EXTENSIONS.has(ext);
|
|
33
|
+
}
|
|
34
|
+
__name(isSupportedFile, "isSupportedFile");
|
|
35
|
+
function parseSource(content, filePath) {
|
|
36
|
+
const ext = getExtension(filePath);
|
|
37
|
+
try {
|
|
38
|
+
const lang = JSX_EXTENSIONS.has(ext) ? TS_EXTENSIONS.has(ext) ? "tsx" : "jsx" : TS_EXTENSIONS.has(ext) ? "ts" : "js";
|
|
39
|
+
const result = parseSync(filePath, content, {
|
|
40
|
+
sourceType: "module",
|
|
41
|
+
lang
|
|
42
|
+
});
|
|
43
|
+
const errors = Array.isArray(result.errors) ? result.errors.map((e) => normalizeError(e)) : [];
|
|
44
|
+
return {
|
|
45
|
+
program: result.program,
|
|
46
|
+
errors,
|
|
47
|
+
success: errors.length === 0
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
program: {
|
|
52
|
+
type: "Program",
|
|
53
|
+
body: [],
|
|
54
|
+
sourceType: "module"
|
|
55
|
+
},
|
|
56
|
+
errors: [
|
|
57
|
+
{
|
|
58
|
+
message: error instanceof Error ? error.message : String(error),
|
|
59
|
+
severity: "error"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
success: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
__name(parseSource, "parseSource");
|
|
67
|
+
function walkAST(node, visitor, parent) {
|
|
68
|
+
if (!node || typeof node !== "object") {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const n = node;
|
|
72
|
+
if (typeof n.type === "string") {
|
|
73
|
+
visitor(n, parent);
|
|
74
|
+
}
|
|
75
|
+
for (const key of Object.keys(n)) {
|
|
76
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc") {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const value = n[key];
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
for (const item of value) {
|
|
82
|
+
if (item && typeof item === "object" && typeof item.type === "string") {
|
|
83
|
+
walkAST(item, visitor, n);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else if (value && typeof value === "object" && typeof value.type === "string") {
|
|
87
|
+
walkAST(value, visitor, n);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
__name(walkAST, "walkAST");
|
|
92
|
+
function countASTNodes(program) {
|
|
93
|
+
let count = 0;
|
|
94
|
+
walkAST(program, () => {
|
|
95
|
+
count++;
|
|
96
|
+
});
|
|
97
|
+
return count;
|
|
98
|
+
}
|
|
99
|
+
__name(countASTNodes, "countASTNodes");
|
|
100
|
+
function offsetToLine(source, offset) {
|
|
101
|
+
if (offset < 0 || offset > source.length) {
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
let line = 1;
|
|
105
|
+
for (let i = 0; i < offset; i++) {
|
|
106
|
+
if (source[i] === "\n") {
|
|
107
|
+
line++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return line;
|
|
111
|
+
}
|
|
112
|
+
__name(offsetToLine, "offsetToLine");
|
|
113
|
+
function getExtension(filePath) {
|
|
114
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
115
|
+
return lastDot >= 0 ? filePath.substring(lastDot).toLowerCase() : "";
|
|
116
|
+
}
|
|
117
|
+
__name(getExtension, "getExtension");
|
|
118
|
+
function normalizeError(e) {
|
|
119
|
+
if (typeof e === "object" && e !== null) {
|
|
120
|
+
const err = e;
|
|
121
|
+
return {
|
|
122
|
+
message: String(err.message ?? err),
|
|
123
|
+
severity: String(err.severity ?? "error"),
|
|
124
|
+
labels: Array.isArray(err.labels) ? err.labels : void 0
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
message: String(e),
|
|
129
|
+
severity: "error"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
__name(normalizeError, "normalizeError");
|
|
133
|
+
|
|
134
|
+
// ../../packages/core/dist/analysis/ast/ComplexityAnalyzer.js
|
|
135
|
+
var THRESHOLDS = {
|
|
136
|
+
/** Cyclomatic complexity per function */
|
|
137
|
+
maxCyclomaticPerFunction: 15,
|
|
138
|
+
/** Maximum nesting depth per function */
|
|
139
|
+
maxNestingDepth: 5,
|
|
140
|
+
/** Maximum parameters per function */
|
|
141
|
+
maxParameters: 5,
|
|
142
|
+
/** Maximum functions per file */
|
|
143
|
+
maxFunctionsPerFile: 30,
|
|
144
|
+
/** File-level aggregate cyclomatic complexity */
|
|
145
|
+
maxCyclomaticPerFile: 50
|
|
146
|
+
};
|
|
147
|
+
var BRANCH_NODES = /* @__PURE__ */ new Set([
|
|
148
|
+
"IfStatement",
|
|
149
|
+
"ConditionalExpression",
|
|
150
|
+
"SwitchCase",
|
|
151
|
+
"ForStatement",
|
|
152
|
+
"ForInStatement",
|
|
153
|
+
"ForOfStatement",
|
|
154
|
+
"WhileStatement",
|
|
155
|
+
"DoWhileStatement",
|
|
156
|
+
"CatchClause"
|
|
157
|
+
]);
|
|
158
|
+
var LOGICAL_OPERATORS = /* @__PURE__ */ new Set([
|
|
159
|
+
"&&",
|
|
160
|
+
"||",
|
|
161
|
+
"??"
|
|
162
|
+
]);
|
|
163
|
+
var FUNCTION_NODES = /* @__PURE__ */ new Set([
|
|
164
|
+
"FunctionDeclaration",
|
|
165
|
+
"FunctionExpression",
|
|
166
|
+
"ArrowFunctionExpression",
|
|
167
|
+
"MethodDefinition"
|
|
168
|
+
]);
|
|
169
|
+
var ComplexityAnalyzer = class {
|
|
170
|
+
static {
|
|
171
|
+
__name(this, "ComplexityAnalyzer");
|
|
172
|
+
}
|
|
173
|
+
id = "complexity";
|
|
174
|
+
name = "Complexity Analysis";
|
|
175
|
+
filePatterns = [
|
|
176
|
+
"*.ts",
|
|
177
|
+
"*.tsx",
|
|
178
|
+
"*.js",
|
|
179
|
+
"*.jsx"
|
|
180
|
+
];
|
|
181
|
+
async analyze(context) {
|
|
182
|
+
const startTime = performance.now();
|
|
183
|
+
const issues = [];
|
|
184
|
+
let filesAnalyzed = 0;
|
|
185
|
+
let totalNodesVisited = 0;
|
|
186
|
+
const parseErrors = [];
|
|
187
|
+
for (const [file, content] of context.contents) {
|
|
188
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
filesAnalyzed++;
|
|
192
|
+
const fileComplexity = this.analyzeFile(content, file);
|
|
193
|
+
if (!fileComplexity) {
|
|
194
|
+
parseErrors.push(`${file}: Failed to parse`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
totalNodesVisited += fileComplexity.functions.length;
|
|
198
|
+
for (const fn of fileComplexity.functions) {
|
|
199
|
+
if (fn.cyclomatic > THRESHOLDS.maxCyclomaticPerFunction) {
|
|
200
|
+
issues.push({
|
|
201
|
+
id: `complexity/cyclomatic/${file}/${fn.line}`,
|
|
202
|
+
severity: fn.cyclomatic > THRESHOLDS.maxCyclomaticPerFunction * 2 ? "high" : "medium",
|
|
203
|
+
type: "HIGH_CYCLOMATIC_COMPLEXITY",
|
|
204
|
+
message: `Function "${fn.name}" has cyclomatic complexity ${fn.cyclomatic} (max: ${THRESHOLDS.maxCyclomaticPerFunction})`,
|
|
205
|
+
file,
|
|
206
|
+
line: fn.line,
|
|
207
|
+
fix: "Extract helper functions or simplify branching logic"
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (fn.maxNesting > THRESHOLDS.maxNestingDepth) {
|
|
211
|
+
issues.push({
|
|
212
|
+
id: `complexity/nesting/${file}/${fn.line}`,
|
|
213
|
+
severity: "medium",
|
|
214
|
+
type: "DEEP_NESTING",
|
|
215
|
+
message: `Function "${fn.name}" has nesting depth ${fn.maxNesting} (max: ${THRESHOLDS.maxNestingDepth})`,
|
|
216
|
+
file,
|
|
217
|
+
line: fn.line,
|
|
218
|
+
fix: "Use early returns or extract nested logic into helper functions"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (fn.parameters > THRESHOLDS.maxParameters) {
|
|
222
|
+
issues.push({
|
|
223
|
+
id: `complexity/parameters/${file}/${fn.line}`,
|
|
224
|
+
severity: "low",
|
|
225
|
+
type: "TOO_MANY_PARAMETERS",
|
|
226
|
+
message: `Function "${fn.name}" has ${fn.parameters} parameters (max: ${THRESHOLDS.maxParameters})`,
|
|
227
|
+
file,
|
|
228
|
+
line: fn.line,
|
|
229
|
+
fix: "Use an options object pattern to reduce parameter count"
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (fileComplexity.functionCount > THRESHOLDS.maxFunctionsPerFile) {
|
|
234
|
+
issues.push({
|
|
235
|
+
id: `complexity/function-count/${file}`,
|
|
236
|
+
severity: "low",
|
|
237
|
+
type: "TOO_MANY_FUNCTIONS",
|
|
238
|
+
message: `${file} has ${fileComplexity.functionCount} functions (max: ${THRESHOLDS.maxFunctionsPerFile})`,
|
|
239
|
+
file,
|
|
240
|
+
fix: "Split into multiple focused modules"
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (fileComplexity.totalCyclomatic > THRESHOLDS.maxCyclomaticPerFile) {
|
|
244
|
+
issues.push({
|
|
245
|
+
id: `complexity/file-complexity/${file}`,
|
|
246
|
+
severity: "medium",
|
|
247
|
+
type: "HIGH_FILE_COMPLEXITY",
|
|
248
|
+
message: `${file} has total cyclomatic complexity ${fileComplexity.totalCyclomatic} (max: ${THRESHOLDS.maxCyclomaticPerFile})`,
|
|
249
|
+
file,
|
|
250
|
+
fix: "Consider splitting this file into smaller modules"
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
analyzer: this.id,
|
|
256
|
+
success: true,
|
|
257
|
+
issues,
|
|
258
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
259
|
+
duration: performance.now() - startTime,
|
|
260
|
+
metadata: {
|
|
261
|
+
filesAnalyzed,
|
|
262
|
+
nodesVisited: totalNodesVisited,
|
|
263
|
+
patternsChecked: [
|
|
264
|
+
"HIGH_CYCLOMATIC_COMPLEXITY",
|
|
265
|
+
"DEEP_NESTING",
|
|
266
|
+
"TOO_MANY_PARAMETERS",
|
|
267
|
+
"TOO_MANY_FUNCTIONS",
|
|
268
|
+
"HIGH_FILE_COMPLEXITY"
|
|
269
|
+
],
|
|
270
|
+
parseErrors
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
shouldRun(context) {
|
|
275
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Analyze a single file and return complexity metrics.
|
|
279
|
+
* Useful for external callers that just want metrics, not issues.
|
|
280
|
+
*/
|
|
281
|
+
analyzeFile(content, filePath) {
|
|
282
|
+
if (!isSupportedFile(filePath)) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
const { program, success } = parseSource(content, filePath);
|
|
286
|
+
if (!success && program.body.length === 0) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const functions = [];
|
|
290
|
+
let fileMaxNesting = 0;
|
|
291
|
+
walkAST(program, (node) => {
|
|
292
|
+
if (!FUNCTION_NODES.has(node.type)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const fn = this.analyzeFunctionNode(node);
|
|
296
|
+
functions.push(fn);
|
|
297
|
+
if (fn.maxNesting > fileMaxNesting) {
|
|
298
|
+
fileMaxNesting = fn.maxNesting;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
const totalCyclomatic = functions.reduce((sum, fn) => sum + fn.cyclomatic, 0);
|
|
302
|
+
return {
|
|
303
|
+
filePath,
|
|
304
|
+
functions,
|
|
305
|
+
totalCyclomatic,
|
|
306
|
+
maxNesting: fileMaxNesting,
|
|
307
|
+
functionCount: functions.length,
|
|
308
|
+
averageCyclomatic: functions.length > 0 ? totalCyclomatic / functions.length : 0
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// -----------------------------------------------------------------------
|
|
312
|
+
// Per-function analysis
|
|
313
|
+
// -----------------------------------------------------------------------
|
|
314
|
+
analyzeFunctionNode(node) {
|
|
315
|
+
const name = this.getFunctionName(node);
|
|
316
|
+
const line = node.start ?? 0;
|
|
317
|
+
const params = this.getParameterCount(node);
|
|
318
|
+
let cyclomatic = 1;
|
|
319
|
+
let maxNesting = 0;
|
|
320
|
+
const body = this.getFunctionBody(node);
|
|
321
|
+
if (body) {
|
|
322
|
+
this.walkForComplexity(body, (n, depth) => {
|
|
323
|
+
if (BRANCH_NODES.has(n.type)) {
|
|
324
|
+
cyclomatic++;
|
|
325
|
+
}
|
|
326
|
+
if (n.type === "LogicalExpression") {
|
|
327
|
+
const operator = n.operator;
|
|
328
|
+
if (LOGICAL_OPERATORS.has(operator)) {
|
|
329
|
+
cyclomatic++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (depth > maxNesting) {
|
|
333
|
+
maxNesting = depth;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
name,
|
|
339
|
+
line,
|
|
340
|
+
cyclomatic,
|
|
341
|
+
maxNesting,
|
|
342
|
+
parameters: params
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Walk AST nodes counting nesting depth for complexity metrics
|
|
347
|
+
*/
|
|
348
|
+
walkForComplexity(node, callback, depth = 0) {
|
|
349
|
+
if (!node || typeof node !== "object") {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const n = node;
|
|
353
|
+
if (typeof n.type !== "string") {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const nestingNodes = /* @__PURE__ */ new Set([
|
|
357
|
+
"IfStatement",
|
|
358
|
+
"ForStatement",
|
|
359
|
+
"ForInStatement",
|
|
360
|
+
"ForOfStatement",
|
|
361
|
+
"WhileStatement",
|
|
362
|
+
"DoWhileStatement",
|
|
363
|
+
"SwitchStatement",
|
|
364
|
+
"TryStatement"
|
|
365
|
+
]);
|
|
366
|
+
const newDepth = nestingNodes.has(n.type) ? depth + 1 : depth;
|
|
367
|
+
callback(n, newDepth);
|
|
368
|
+
for (const key of Object.keys(n)) {
|
|
369
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc") {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const value = n[key];
|
|
373
|
+
if (Array.isArray(value)) {
|
|
374
|
+
for (const item of value) {
|
|
375
|
+
this.walkForComplexity(item, callback, newDepth);
|
|
376
|
+
}
|
|
377
|
+
} else if (value && typeof value === "object" && typeof value.type === "string") {
|
|
378
|
+
this.walkForComplexity(value, callback, newDepth);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// -----------------------------------------------------------------------
|
|
383
|
+
// Helpers
|
|
384
|
+
// -----------------------------------------------------------------------
|
|
385
|
+
getFunctionName(node) {
|
|
386
|
+
if (node.id && typeof node.id === "object") {
|
|
387
|
+
const id = node.id;
|
|
388
|
+
if (typeof id.name === "string") {
|
|
389
|
+
return id.name;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (node.key && typeof node.key === "object") {
|
|
393
|
+
const key = node.key;
|
|
394
|
+
if (typeof key.name === "string") {
|
|
395
|
+
return key.name;
|
|
396
|
+
}
|
|
397
|
+
if (typeof key.value === "string") {
|
|
398
|
+
return key.value;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return "<anonymous>";
|
|
402
|
+
}
|
|
403
|
+
getParameterCount(node) {
|
|
404
|
+
if (node.type === "MethodDefinition") {
|
|
405
|
+
const value = node.value;
|
|
406
|
+
if (value && Array.isArray(value.params)) {
|
|
407
|
+
return value.params.length;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (Array.isArray(node.params)) {
|
|
411
|
+
return node.params.length;
|
|
412
|
+
}
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
getFunctionBody(node) {
|
|
416
|
+
if (node.type === "MethodDefinition") {
|
|
417
|
+
const value = node.value;
|
|
418
|
+
if (value?.body) {
|
|
419
|
+
return value.body;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (node.body) {
|
|
423
|
+
return node.body;
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
shouldAnalyzeFile(file) {
|
|
428
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
429
|
+
return [
|
|
430
|
+
"ts",
|
|
431
|
+
"tsx",
|
|
432
|
+
"js",
|
|
433
|
+
"jsx",
|
|
434
|
+
"mts",
|
|
435
|
+
"cts"
|
|
436
|
+
].includes(ext ?? "");
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// ../../packages/core/dist/analysis/ast/import-extractor.js
|
|
441
|
+
function extractImports(content, filePath) {
|
|
442
|
+
if (!isSupportedFile(filePath)) {
|
|
443
|
+
return {
|
|
444
|
+
filePath,
|
|
445
|
+
imports: [],
|
|
446
|
+
parseSuccess: false,
|
|
447
|
+
parseErrors: [
|
|
448
|
+
`Unsupported file type: ${filePath}`
|
|
449
|
+
]
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const { program, errors, success } = parseSource(content, filePath);
|
|
453
|
+
const imports = [];
|
|
454
|
+
walkAST(program, (node) => {
|
|
455
|
+
switch (node.type) {
|
|
456
|
+
// import ... from 'source'
|
|
457
|
+
case "ImportDeclaration":
|
|
458
|
+
handleImportDeclaration(node, imports);
|
|
459
|
+
break;
|
|
460
|
+
// export { ... } from 'source' OR export * from 'source'
|
|
461
|
+
case "ExportNamedDeclaration":
|
|
462
|
+
case "ExportAllDeclaration":
|
|
463
|
+
handleExportDeclaration(node, imports);
|
|
464
|
+
break;
|
|
465
|
+
// import('source') — ESTree ImportExpression
|
|
466
|
+
case "ImportExpression":
|
|
467
|
+
handleImportExpression(node, imports);
|
|
468
|
+
break;
|
|
469
|
+
// require('source') OR legacy import('source') as CallExpression
|
|
470
|
+
case "CallExpression":
|
|
471
|
+
handleCallExpression(node, imports);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
filePath,
|
|
477
|
+
imports,
|
|
478
|
+
parseSuccess: success,
|
|
479
|
+
parseErrors: errors.map((e) => e.message)
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
__name(extractImports, "extractImports");
|
|
483
|
+
function extractImportSources(content, filePath) {
|
|
484
|
+
const result = extractImports(content, filePath);
|
|
485
|
+
const sources = /* @__PURE__ */ new Set();
|
|
486
|
+
for (const imp of result.imports) {
|
|
487
|
+
sources.add(imp.source);
|
|
488
|
+
}
|
|
489
|
+
return [
|
|
490
|
+
...sources
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
__name(extractImportSources, "extractImportSources");
|
|
494
|
+
function extractImportsBatch(files) {
|
|
495
|
+
const results = /* @__PURE__ */ new Map();
|
|
496
|
+
for (const [filePath, content] of files) {
|
|
497
|
+
results.set(filePath, extractImports(content, filePath));
|
|
498
|
+
}
|
|
499
|
+
return results;
|
|
500
|
+
}
|
|
501
|
+
__name(extractImportsBatch, "extractImportsBatch");
|
|
502
|
+
function handleImportDeclaration(node, imports) {
|
|
503
|
+
const source = getStringValue(node.source);
|
|
504
|
+
if (!source) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const specifiers = [];
|
|
508
|
+
if (Array.isArray(node.specifiers)) {
|
|
509
|
+
for (const spec of node.specifiers) {
|
|
510
|
+
const s = spec;
|
|
511
|
+
if (s.type === "ImportSpecifier") {
|
|
512
|
+
const imported = s.imported;
|
|
513
|
+
specifiers.push(getIdentifierName(imported) ?? "unknown");
|
|
514
|
+
} else if (s.type === "ImportDefaultSpecifier") {
|
|
515
|
+
specifiers.push("default");
|
|
516
|
+
} else if (s.type === "ImportNamespaceSpecifier") {
|
|
517
|
+
specifiers.push("*");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
imports.push({
|
|
522
|
+
source,
|
|
523
|
+
kind: "static",
|
|
524
|
+
typeOnly: node.importKind === "type" || Boolean(node.isTypeOnly),
|
|
525
|
+
line: node.start != null ? node.start : void 0,
|
|
526
|
+
specifiers
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
__name(handleImportDeclaration, "handleImportDeclaration");
|
|
530
|
+
function handleExportDeclaration(node, imports) {
|
|
531
|
+
const source = getStringValue(node.source);
|
|
532
|
+
if (!source) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const specifiers = [];
|
|
536
|
+
if (node.type === "ExportAllDeclaration") {
|
|
537
|
+
specifiers.push("*");
|
|
538
|
+
} else if (Array.isArray(node.specifiers)) {
|
|
539
|
+
for (const spec of node.specifiers) {
|
|
540
|
+
const s = spec;
|
|
541
|
+
const local = s.local;
|
|
542
|
+
specifiers.push(getIdentifierName(local) ?? "unknown");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
imports.push({
|
|
546
|
+
source,
|
|
547
|
+
kind: "re-export",
|
|
548
|
+
typeOnly: node.exportKind === "type" || Boolean(node.isTypeOnly),
|
|
549
|
+
line: node.start != null ? node.start : void 0,
|
|
550
|
+
specifiers
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
__name(handleExportDeclaration, "handleExportDeclaration");
|
|
554
|
+
function handleImportExpression(node, imports) {
|
|
555
|
+
const source = getStringValue(node.source);
|
|
556
|
+
if (source) {
|
|
557
|
+
imports.push({
|
|
558
|
+
source,
|
|
559
|
+
kind: "dynamic",
|
|
560
|
+
typeOnly: false,
|
|
561
|
+
line: node.start != null ? node.start : void 0,
|
|
562
|
+
specifiers: []
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
__name(handleImportExpression, "handleImportExpression");
|
|
567
|
+
function handleCallExpression(node, imports) {
|
|
568
|
+
const callee = node.callee;
|
|
569
|
+
if (!callee) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (node.type === "CallExpression" && callee.type === "Import") {
|
|
573
|
+
const args = node.arguments;
|
|
574
|
+
if (args && args.length > 0) {
|
|
575
|
+
const source = getStringValue(args[0]);
|
|
576
|
+
if (source) {
|
|
577
|
+
imports.push({
|
|
578
|
+
source,
|
|
579
|
+
kind: "dynamic",
|
|
580
|
+
typeOnly: false,
|
|
581
|
+
line: node.start != null ? node.start : void 0,
|
|
582
|
+
specifiers: []
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (callee.type === "Identifier" && callee.name === "require") {
|
|
589
|
+
const args = node.arguments;
|
|
590
|
+
if (args && args.length > 0) {
|
|
591
|
+
const source = getStringValue(args[0]);
|
|
592
|
+
if (source) {
|
|
593
|
+
imports.push({
|
|
594
|
+
source,
|
|
595
|
+
kind: "require",
|
|
596
|
+
typeOnly: false,
|
|
597
|
+
line: node.start != null ? node.start : void 0,
|
|
598
|
+
specifiers: []
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
__name(handleCallExpression, "handleCallExpression");
|
|
605
|
+
function getStringValue(node) {
|
|
606
|
+
if (!node || typeof node !== "object") {
|
|
607
|
+
return void 0;
|
|
608
|
+
}
|
|
609
|
+
const n = node;
|
|
610
|
+
if (n.type === "StringLiteral" || n.type === "Literal") {
|
|
611
|
+
return typeof n.value === "string" ? n.value : void 0;
|
|
612
|
+
}
|
|
613
|
+
return void 0;
|
|
614
|
+
}
|
|
615
|
+
__name(getStringValue, "getStringValue");
|
|
616
|
+
function getIdentifierName(node) {
|
|
617
|
+
if (!node || typeof node !== "object") {
|
|
618
|
+
return void 0;
|
|
619
|
+
}
|
|
620
|
+
const n = node;
|
|
621
|
+
if (n.type === "Identifier" || n.type === "IdentifierName" || n.type === "IdentifierReference") {
|
|
622
|
+
return typeof n.name === "string" ? n.name : void 0;
|
|
623
|
+
}
|
|
624
|
+
return void 0;
|
|
625
|
+
}
|
|
626
|
+
__name(getIdentifierName, "getIdentifierName");
|
|
627
|
+
|
|
628
|
+
// ../../packages/core/dist/analysis/ast/ImportGraphAnalyzer.js
|
|
629
|
+
var THRESHOLDS2 = {
|
|
630
|
+
/** Max files that can import a single file before it's flagged as high fan-in */
|
|
631
|
+
highFanIn: 15,
|
|
632
|
+
/** Max imports a single file can have before it's flagged as high fan-out */
|
|
633
|
+
highFanOut: 20,
|
|
634
|
+
/** Minimum cycle length to report (avoids noise from self-imports) */
|
|
635
|
+
minCycleLength: 2
|
|
636
|
+
};
|
|
637
|
+
var ImportGraphAnalyzer = class {
|
|
638
|
+
static {
|
|
639
|
+
__name(this, "ImportGraphAnalyzer");
|
|
640
|
+
}
|
|
641
|
+
id = "import-graph";
|
|
642
|
+
name = "Import Graph Analysis";
|
|
643
|
+
filePatterns = [
|
|
644
|
+
"*.ts",
|
|
645
|
+
"*.tsx",
|
|
646
|
+
"*.js",
|
|
647
|
+
"*.jsx"
|
|
648
|
+
];
|
|
649
|
+
async analyze(context) {
|
|
650
|
+
const startTime = performance.now();
|
|
651
|
+
const issues = [];
|
|
652
|
+
let filesAnalyzed = 0;
|
|
653
|
+
const parseErrors = [];
|
|
654
|
+
const extractions = /* @__PURE__ */ new Map();
|
|
655
|
+
for (const [file, content] of context.contents) {
|
|
656
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
filesAnalyzed++;
|
|
660
|
+
const result = extractImports(content, file);
|
|
661
|
+
extractions.set(file, result);
|
|
662
|
+
if (!result.parseSuccess) {
|
|
663
|
+
parseErrors.push(...result.parseErrors);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const graph = this.buildGraph(extractions, context.workspaceRoot);
|
|
667
|
+
const cycles = this.detectCycles(graph.edges);
|
|
668
|
+
graph.cycles = cycles;
|
|
669
|
+
for (const cycle of cycles) {
|
|
670
|
+
issues.push({
|
|
671
|
+
id: `import-graph/circular/${cycle.join("->")}`,
|
|
672
|
+
severity: "high",
|
|
673
|
+
type: "CIRCULAR_DEPENDENCY",
|
|
674
|
+
message: `Circular dependency: ${cycle.join(" \u2192 ")}`,
|
|
675
|
+
file: cycle[0],
|
|
676
|
+
fix: "Break the cycle by extracting shared code or using dependency injection"
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
for (const [file, node] of graph.nodes) {
|
|
680
|
+
if (node.importedBy.length > THRESHOLDS2.highFanIn) {
|
|
681
|
+
issues.push({
|
|
682
|
+
id: `import-graph/high-fan-in/${file}`,
|
|
683
|
+
severity: "medium",
|
|
684
|
+
type: "HIGH_FAN_IN",
|
|
685
|
+
message: `${file} is imported by ${node.importedBy.length} files \u2014 changes here have high blast radius`,
|
|
686
|
+
file,
|
|
687
|
+
fix: "Consider splitting into smaller, more focused modules"
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const [file, node] of graph.nodes) {
|
|
692
|
+
const runtimeImports = node.imports.filter((imp) => !node.typeOnlyImports.includes(imp));
|
|
693
|
+
if (runtimeImports.length > THRESHOLDS2.highFanOut) {
|
|
694
|
+
issues.push({
|
|
695
|
+
id: `import-graph/high-fan-out/${file}`,
|
|
696
|
+
severity: "low",
|
|
697
|
+
type: "HIGH_FAN_OUT",
|
|
698
|
+
message: `${file} imports ${runtimeImports.length} modules \u2014 high coupling`,
|
|
699
|
+
file,
|
|
700
|
+
fix: "Consider using a facade or consolidating related imports"
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
for (const [file, node] of graph.nodes) {
|
|
705
|
+
if (node.importedBy.length === 0 && !this.isEntryPoint(file)) {
|
|
706
|
+
issues.push({
|
|
707
|
+
id: `import-graph/orphan/${file}`,
|
|
708
|
+
severity: "info",
|
|
709
|
+
type: "ORPHAN_FILE",
|
|
710
|
+
message: `${file} is not imported by any other analyzed file`,
|
|
711
|
+
file,
|
|
712
|
+
fix: "Verify this file is needed \u2014 it may be dead code"
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
analyzer: this.id,
|
|
718
|
+
success: true,
|
|
719
|
+
issues,
|
|
720
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
721
|
+
duration: performance.now() - startTime,
|
|
722
|
+
metadata: {
|
|
723
|
+
filesAnalyzed,
|
|
724
|
+
nodesVisited: graph.nodes.size,
|
|
725
|
+
patternsChecked: [
|
|
726
|
+
"CIRCULAR_DEPENDENCY",
|
|
727
|
+
"HIGH_FAN_IN",
|
|
728
|
+
"HIGH_FAN_OUT",
|
|
729
|
+
"ORPHAN_FILE"
|
|
730
|
+
],
|
|
731
|
+
parseErrors
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
shouldRun(context) {
|
|
736
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Build the import graph and return it for external consumption.
|
|
740
|
+
* Useful for other tools (momentum scoring, risk propagation, etc.).
|
|
741
|
+
*/
|
|
742
|
+
buildGraphFromContext(context) {
|
|
743
|
+
const extractions = /* @__PURE__ */ new Map();
|
|
744
|
+
for (const [file, content] of context.contents) {
|
|
745
|
+
if (this.shouldAnalyzeFile(file)) {
|
|
746
|
+
extractions.set(file, extractImports(content, file));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const graph = this.buildGraph(extractions, context.workspaceRoot);
|
|
750
|
+
graph.cycles = this.detectCycles(graph.edges);
|
|
751
|
+
return graph;
|
|
752
|
+
}
|
|
753
|
+
// -----------------------------------------------------------------------
|
|
754
|
+
// Graph construction
|
|
755
|
+
// -----------------------------------------------------------------------
|
|
756
|
+
buildGraph(extractions, workspaceRoot) {
|
|
757
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
758
|
+
const edges = /* @__PURE__ */ new Map();
|
|
759
|
+
const reverseEdges = /* @__PURE__ */ new Map();
|
|
760
|
+
for (const filePath of extractions.keys()) {
|
|
761
|
+
const normalized = this.normalizePath(filePath);
|
|
762
|
+
nodes.set(normalized, {
|
|
763
|
+
filePath: normalized,
|
|
764
|
+
imports: [],
|
|
765
|
+
importedBy: [],
|
|
766
|
+
typeOnlyImports: []
|
|
767
|
+
});
|
|
768
|
+
edges.set(normalized, /* @__PURE__ */ new Set());
|
|
769
|
+
}
|
|
770
|
+
for (const [filePath, extraction] of extractions) {
|
|
771
|
+
const normalized = this.normalizePath(filePath);
|
|
772
|
+
for (const imp of extraction.imports) {
|
|
773
|
+
const resolved = this.resolveImport(imp.source, filePath, workspaceRoot);
|
|
774
|
+
if (!resolved) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const resolvedNorm = this.normalizePath(resolved);
|
|
778
|
+
edges.get(normalized)?.add(resolvedNorm);
|
|
779
|
+
const node = nodes.get(normalized);
|
|
780
|
+
if (node && !node.imports.includes(resolvedNorm)) {
|
|
781
|
+
node.imports.push(resolvedNorm);
|
|
782
|
+
if (imp.typeOnly) {
|
|
783
|
+
node.typeOnlyImports.push(resolvedNorm);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!reverseEdges.has(resolvedNorm)) {
|
|
787
|
+
reverseEdges.set(resolvedNorm, /* @__PURE__ */ new Set());
|
|
788
|
+
}
|
|
789
|
+
reverseEdges.get(resolvedNorm)?.add(normalized);
|
|
790
|
+
if (!nodes.has(resolvedNorm)) {
|
|
791
|
+
nodes.set(resolvedNorm, {
|
|
792
|
+
filePath: resolvedNorm,
|
|
793
|
+
imports: [],
|
|
794
|
+
importedBy: [],
|
|
795
|
+
typeOnlyImports: []
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const [file, importers] of reverseEdges) {
|
|
801
|
+
const node = nodes.get(file);
|
|
802
|
+
if (node) {
|
|
803
|
+
node.importedBy = [
|
|
804
|
+
...importers
|
|
805
|
+
];
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
nodes,
|
|
810
|
+
edges,
|
|
811
|
+
reverseEdges,
|
|
812
|
+
cycles: []
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
// -----------------------------------------------------------------------
|
|
816
|
+
// Cycle detection (Tarjan's SCC adapted for cycles)
|
|
817
|
+
// -----------------------------------------------------------------------
|
|
818
|
+
detectCycles(edges) {
|
|
819
|
+
const cycles = [];
|
|
820
|
+
const visited = /* @__PURE__ */ new Set();
|
|
821
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
822
|
+
const stack = [];
|
|
823
|
+
const dfs = /* @__PURE__ */ __name((node) => {
|
|
824
|
+
if (inStack.has(node)) {
|
|
825
|
+
const cycleStart = stack.indexOf(node);
|
|
826
|
+
if (cycleStart >= 0) {
|
|
827
|
+
const cycle = stack.slice(cycleStart);
|
|
828
|
+
if (cycle.length >= THRESHOLDS2.minCycleLength) {
|
|
829
|
+
cycles.push([
|
|
830
|
+
...cycle,
|
|
831
|
+
node
|
|
832
|
+
]);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (visited.has(node)) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
visited.add(node);
|
|
841
|
+
inStack.add(node);
|
|
842
|
+
stack.push(node);
|
|
843
|
+
const neighbors = edges.get(node) ?? /* @__PURE__ */ new Set();
|
|
844
|
+
for (const neighbor of neighbors) {
|
|
845
|
+
dfs(neighbor);
|
|
846
|
+
}
|
|
847
|
+
stack.pop();
|
|
848
|
+
inStack.delete(node);
|
|
849
|
+
}, "dfs");
|
|
850
|
+
for (const node of edges.keys()) {
|
|
851
|
+
dfs(node);
|
|
852
|
+
}
|
|
853
|
+
return cycles;
|
|
854
|
+
}
|
|
855
|
+
// -----------------------------------------------------------------------
|
|
856
|
+
// Import resolution
|
|
857
|
+
// -----------------------------------------------------------------------
|
|
858
|
+
resolveImport(importSource, fromFile, _workspaceRoot) {
|
|
859
|
+
if (!importSource.startsWith(".") && !importSource.startsWith("/")) {
|
|
860
|
+
if (importSource.startsWith("@")) {
|
|
861
|
+
const parts = importSource.split("/");
|
|
862
|
+
if (parts.length >= 2) {
|
|
863
|
+
const pkg = parts[1];
|
|
864
|
+
return `packages/${pkg}/src/index.ts`;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
const fromDir = dirname(fromFile);
|
|
870
|
+
let resolved = resolve(fromDir, importSource);
|
|
871
|
+
if (!resolved.match(/\.(ts|tsx|js|jsx|mts|cts|mjs|cjs)$/)) {
|
|
872
|
+
resolved += ".ts";
|
|
873
|
+
}
|
|
874
|
+
resolved = resolved.replace(/\.js$/, ".ts").replace(/\.jsx$/, ".tsx");
|
|
875
|
+
return resolved;
|
|
876
|
+
}
|
|
877
|
+
// -----------------------------------------------------------------------
|
|
878
|
+
// Helpers
|
|
879
|
+
// -----------------------------------------------------------------------
|
|
880
|
+
shouldAnalyzeFile(file) {
|
|
881
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
882
|
+
return [
|
|
883
|
+
"ts",
|
|
884
|
+
"tsx",
|
|
885
|
+
"js",
|
|
886
|
+
"jsx",
|
|
887
|
+
"mts",
|
|
888
|
+
"cts"
|
|
889
|
+
].includes(ext ?? "");
|
|
890
|
+
}
|
|
891
|
+
isEntryPoint(file) {
|
|
892
|
+
return file.includes("index.") || file.includes("main.") || file.includes("entry.") || file.includes("server.") || file.includes("app.") || file.endsWith("/page.tsx") || file.endsWith("/layout.tsx") || file.endsWith("/route.ts") || file.includes("__tests__") || file.includes(".test.") || file.includes(".spec.");
|
|
893
|
+
}
|
|
894
|
+
normalizePath(filePath) {
|
|
895
|
+
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
896
|
+
}
|
|
897
|
+
};
|
|
8
898
|
var SyntaxAnalyzer = class {
|
|
9
899
|
static {
|
|
10
900
|
__name(this, "SyntaxAnalyzer");
|
|
@@ -24,7 +914,9 @@ var SyntaxAnalyzer = class {
|
|
|
24
914
|
let nodesVisited = 0;
|
|
25
915
|
const parseErrors = [];
|
|
26
916
|
for (const [file, content] of context.contents) {
|
|
27
|
-
if (!this.shouldAnalyzeFile(file))
|
|
917
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
28
920
|
filesAnalyzed++;
|
|
29
921
|
try {
|
|
30
922
|
const ast = eslintParser.parse(content, {
|
|
@@ -107,7 +999,9 @@ var SyntaxAnalyzer = class {
|
|
|
107
999
|
* Count AST nodes for coverage metrics
|
|
108
1000
|
*/
|
|
109
1001
|
countNodes(node) {
|
|
110
|
-
if (!node || typeof node !== "object")
|
|
1002
|
+
if (!node || typeof node !== "object") {
|
|
1003
|
+
return 0;
|
|
1004
|
+
}
|
|
111
1005
|
let count = 1;
|
|
112
1006
|
for (const key of Object.keys(node)) {
|
|
113
1007
|
const value = node[key];
|
|
@@ -213,7 +1107,9 @@ var CompletenessAnalyzer = class {
|
|
|
213
1107
|
let nodesVisited = 0;
|
|
214
1108
|
const parseErrors = [];
|
|
215
1109
|
for (const [file, content] of context.contents) {
|
|
216
|
-
if (!this.shouldAnalyzeFile(file))
|
|
1110
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
217
1113
|
filesAnalyzed++;
|
|
218
1114
|
this.checkTodoComments(content, file, issues);
|
|
219
1115
|
this.checkPlaceholderPatterns(content, file, issues);
|
|
@@ -380,12 +1276,18 @@ var CompletenessAnalyzer = class {
|
|
|
380
1276
|
}, "FunctionDeclaration"),
|
|
381
1277
|
// Empty method bodies
|
|
382
1278
|
ClassMethod: /* @__PURE__ */ __name((path) => {
|
|
383
|
-
if (path.node.abstract)
|
|
384
|
-
|
|
1279
|
+
if (path.node.abstract) {
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (path.node.kind === "get" || path.node.kind === "set") {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
385
1285
|
const body = path.node.body;
|
|
386
1286
|
if (body && body.body.length === 0) {
|
|
387
1287
|
const methodName = path.node.key.type === "Identifier" ? path.node.key.name : "anonymous";
|
|
388
|
-
if (methodName === "constructor")
|
|
1288
|
+
if (methodName === "constructor") {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
389
1291
|
issues.push({
|
|
390
1292
|
id: `completeness/empty-method/${file}/${path.node.loc?.start.line}`,
|
|
391
1293
|
severity: "medium",
|
|
@@ -682,21 +1584,18 @@ var ChangeImpactAnalyzer = class {
|
|
|
682
1584
|
}
|
|
683
1585
|
}
|
|
684
1586
|
/**
|
|
685
|
-
* Extract import statements from file content
|
|
1587
|
+
* Extract import statements from file content using AST analysis.
|
|
1588
|
+
*
|
|
1589
|
+
* UPGRADED (11b): Replaces regex-based import extraction with proper AST walking
|
|
1590
|
+
* via packages/core/src/analysis/ast/import-extractor.ts (oxc-parser based).
|
|
1591
|
+
* This eliminates false positives from imports in strings/comments and correctly
|
|
1592
|
+
* handles dynamic imports, re-exports, and type-only imports.
|
|
686
1593
|
*/
|
|
687
1594
|
extractImports(content, fromFile) {
|
|
1595
|
+
const rawSources = extractImportSources(content, fromFile);
|
|
688
1596
|
const imports = [];
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
let match;
|
|
692
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
693
|
-
const importPath = this.resolveImportPath(match[1], fromFile);
|
|
694
|
-
if (importPath) {
|
|
695
|
-
imports.push(importPath);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
while ((match = requireRegex.exec(content)) !== null) {
|
|
699
|
-
const importPath = this.resolveImportPath(match[1], fromFile);
|
|
1597
|
+
for (const source of rawSources) {
|
|
1598
|
+
const importPath = this.resolveImportPath(source, fromFile);
|
|
700
1599
|
if (importPath) {
|
|
701
1600
|
imports.push(importPath);
|
|
702
1601
|
}
|
|
@@ -931,7 +1830,9 @@ var SecurityAnalyzer = class {
|
|
|
931
1830
|
let nodesVisited = 0;
|
|
932
1831
|
const parseErrors = [];
|
|
933
1832
|
for (const [file, content] of context.contents) {
|
|
934
|
-
if (!this.shouldAnalyzeFile(file))
|
|
1833
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
935
1836
|
filesAnalyzed++;
|
|
936
1837
|
try {
|
|
937
1838
|
const ast = parse(content, {
|
|
@@ -1189,22 +2090,34 @@ var SecurityAnalyzer = class {
|
|
|
1189
2090
|
* Check if expression is a static string (safe)
|
|
1190
2091
|
*/
|
|
1191
2092
|
isStaticString(node) {
|
|
1192
|
-
if (node.type === "StringLiteral")
|
|
1193
|
-
|
|
2093
|
+
if (node.type === "StringLiteral") {
|
|
2094
|
+
return true;
|
|
2095
|
+
}
|
|
2096
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
2097
|
+
return true;
|
|
2098
|
+
}
|
|
1194
2099
|
return false;
|
|
1195
2100
|
}
|
|
1196
2101
|
/**
|
|
1197
2102
|
* Check if expression is a static path (safe)
|
|
1198
2103
|
*/
|
|
1199
2104
|
isStaticPath(node) {
|
|
1200
|
-
if (node.type === "StringLiteral")
|
|
1201
|
-
|
|
2105
|
+
if (node.type === "StringLiteral") {
|
|
2106
|
+
return true;
|
|
2107
|
+
}
|
|
2108
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
2109
|
+
return true;
|
|
2110
|
+
}
|
|
1202
2111
|
if (node.type === "CallExpression") {
|
|
1203
2112
|
const callee = node.callee;
|
|
1204
2113
|
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && callee.property.name === "join") {
|
|
1205
2114
|
return node.arguments.every((arg) => {
|
|
1206
|
-
if (arg.type === "StringLiteral")
|
|
1207
|
-
|
|
2115
|
+
if (arg.type === "StringLiteral") {
|
|
2116
|
+
return true;
|
|
2117
|
+
}
|
|
2118
|
+
if (arg.type === "Identifier" && (arg.name === "__dirname" || arg.name === "__filename")) {
|
|
2119
|
+
return true;
|
|
2120
|
+
}
|
|
1208
2121
|
return false;
|
|
1209
2122
|
});
|
|
1210
2123
|
}
|
|
@@ -1248,6 +2161,188 @@ var SecurityAnalyzer = class {
|
|
|
1248
2161
|
}
|
|
1249
2162
|
};
|
|
1250
2163
|
|
|
2164
|
+
// ../../packages/core/dist/analysis/pipeline.js
|
|
2165
|
+
var ANALYZER_COVERAGE_MAP = {
|
|
2166
|
+
syntax: "astParsed",
|
|
2167
|
+
security: "securityChecked",
|
|
2168
|
+
completeness: "completenessChecked",
|
|
2169
|
+
"change-impact": "architectureChecked",
|
|
2170
|
+
"import-graph": "importGraphChecked",
|
|
2171
|
+
complexity: "complexityChecked"
|
|
2172
|
+
};
|
|
2173
|
+
var CONFIDENCE_WEIGHTS = {
|
|
2174
|
+
syntax: 0.2,
|
|
2175
|
+
security: 0.25,
|
|
2176
|
+
completeness: 0.15,
|
|
2177
|
+
"change-impact": 0.1,
|
|
2178
|
+
"import-graph": 0.15,
|
|
2179
|
+
complexity: 0.15
|
|
2180
|
+
};
|
|
2181
|
+
async function runAnalysisPipeline(context, config) {
|
|
2182
|
+
const start = Date.now();
|
|
2183
|
+
const parallel = config?.parallel ?? true;
|
|
2184
|
+
const timeout = config?.timeout ?? 3e4;
|
|
2185
|
+
const allAnalyzers = createAnalyzers(context.workspaceRoot);
|
|
2186
|
+
const selectedAnalyzers = filterAnalyzers(allAnalyzers, context, config?.analyzers);
|
|
2187
|
+
const results = parallel ? await runParallel(selectedAnalyzers, context, timeout) : await runSequential(selectedAnalyzers, context, timeout);
|
|
2188
|
+
const coverage = buildCoverageInfo(results, context);
|
|
2189
|
+
const confidence = calculateConfidence(results, coverage);
|
|
2190
|
+
const allIssues = results.flatMap((r) => r.issues);
|
|
2191
|
+
const issuesBySeverity = groupBySeverity(allIssues);
|
|
2192
|
+
return {
|
|
2193
|
+
results,
|
|
2194
|
+
totalIssues: allIssues.length,
|
|
2195
|
+
issuesBySeverity,
|
|
2196
|
+
coverage,
|
|
2197
|
+
confidence,
|
|
2198
|
+
duration: Date.now() - start
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
__name(runAnalysisPipeline, "runAnalysisPipeline");
|
|
2202
|
+
function createAnalyzers(workspaceRoot) {
|
|
2203
|
+
return [
|
|
2204
|
+
new SyntaxAnalyzer(),
|
|
2205
|
+
new SecurityAnalyzer(),
|
|
2206
|
+
new CompletenessAnalyzer(),
|
|
2207
|
+
new ComplexityAnalyzer(),
|
|
2208
|
+
new ImportGraphAnalyzer(),
|
|
2209
|
+
new ChangeImpactAnalyzer(workspaceRoot)
|
|
2210
|
+
];
|
|
2211
|
+
}
|
|
2212
|
+
__name(createAnalyzers, "createAnalyzers");
|
|
2213
|
+
function filterAnalyzers(analyzers, context, selectedIds) {
|
|
2214
|
+
let filtered = analyzers;
|
|
2215
|
+
if (selectedIds && selectedIds.length > 0) {
|
|
2216
|
+
const idSet = new Set(selectedIds);
|
|
2217
|
+
filtered = filtered.filter((a) => idSet.has(a.id));
|
|
2218
|
+
}
|
|
2219
|
+
return filtered.filter((a) => a.shouldRun(context));
|
|
2220
|
+
}
|
|
2221
|
+
__name(filterAnalyzers, "filterAnalyzers");
|
|
2222
|
+
async function runParallel(analyzers, context, timeout) {
|
|
2223
|
+
const promises = analyzers.map((analyzer) => runWithTimeout(analyzer, context, timeout));
|
|
2224
|
+
return Promise.all(promises);
|
|
2225
|
+
}
|
|
2226
|
+
__name(runParallel, "runParallel");
|
|
2227
|
+
async function runSequential(analyzers, context, timeout) {
|
|
2228
|
+
const results = [];
|
|
2229
|
+
for (const analyzer of analyzers) {
|
|
2230
|
+
const result = await runWithTimeout(analyzer, context, timeout);
|
|
2231
|
+
results.push(result);
|
|
2232
|
+
}
|
|
2233
|
+
return results;
|
|
2234
|
+
}
|
|
2235
|
+
__name(runSequential, "runSequential");
|
|
2236
|
+
async function runWithTimeout(analyzer, context, timeout) {
|
|
2237
|
+
const start = Date.now();
|
|
2238
|
+
try {
|
|
2239
|
+
const result = await Promise.race([
|
|
2240
|
+
analyzer.analyze(context),
|
|
2241
|
+
new Promise((_, reject) => {
|
|
2242
|
+
setTimeout(() => reject(new Error(`Analyzer '${analyzer.id}' timed out after ${timeout}ms`)), timeout);
|
|
2243
|
+
})
|
|
2244
|
+
]);
|
|
2245
|
+
return result;
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
return {
|
|
2248
|
+
analyzer: analyzer.id,
|
|
2249
|
+
success: false,
|
|
2250
|
+
issues: [
|
|
2251
|
+
{
|
|
2252
|
+
id: `pipeline/${analyzer.id}/error`,
|
|
2253
|
+
severity: "high",
|
|
2254
|
+
type: "ANALYZER_ERROR",
|
|
2255
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2256
|
+
}
|
|
2257
|
+
],
|
|
2258
|
+
coverage: 0,
|
|
2259
|
+
duration: Date.now() - start
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
__name(runWithTimeout, "runWithTimeout");
|
|
2264
|
+
function buildCoverageInfo(results, context) {
|
|
2265
|
+
const coverage = {
|
|
2266
|
+
astParsed: false,
|
|
2267
|
+
securityChecked: false,
|
|
2268
|
+
completenessChecked: false,
|
|
2269
|
+
architectureChecked: false,
|
|
2270
|
+
importGraphChecked: false,
|
|
2271
|
+
complexityChecked: false,
|
|
2272
|
+
filesCoverage: 0
|
|
2273
|
+
};
|
|
2274
|
+
for (const result of results) {
|
|
2275
|
+
const field = ANALYZER_COVERAGE_MAP[result.analyzer];
|
|
2276
|
+
if (field && field !== "filesCoverage" && result.success) {
|
|
2277
|
+
coverage[field] = true;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
const totalFiles = context.files.length;
|
|
2281
|
+
if (totalFiles > 0) {
|
|
2282
|
+
const successfulResults = results.filter((r) => r.success);
|
|
2283
|
+
const avgCoverage = successfulResults.length > 0 ? successfulResults.reduce((sum, r) => sum + r.coverage, 0) / successfulResults.length : 0;
|
|
2284
|
+
coverage.filesCoverage = avgCoverage;
|
|
2285
|
+
}
|
|
2286
|
+
return coverage;
|
|
2287
|
+
}
|
|
2288
|
+
__name(buildCoverageInfo, "buildCoverageInfo");
|
|
2289
|
+
function calculateConfidence(results, coverage) {
|
|
2290
|
+
const breakdown = {};
|
|
2291
|
+
let weightedSum = 0;
|
|
2292
|
+
let totalWeight = 0;
|
|
2293
|
+
let maxPossible = 0;
|
|
2294
|
+
for (const [id, weight] of Object.entries(CONFIDENCE_WEIGHTS)) {
|
|
2295
|
+
const result = results.find((r) => r.analyzer === id);
|
|
2296
|
+
totalWeight += weight;
|
|
2297
|
+
if (result) {
|
|
2298
|
+
const analyzerConfidence = result.success ? result.coverage : 0;
|
|
2299
|
+
breakdown[id] = analyzerConfidence;
|
|
2300
|
+
weightedSum += weight * analyzerConfidence;
|
|
2301
|
+
maxPossible += weight;
|
|
2302
|
+
} else {
|
|
2303
|
+
breakdown[id] = 0;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
const confidence = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
2307
|
+
const maxPossibleConfidence = totalWeight > 0 ? maxPossible / totalWeight : 0;
|
|
2308
|
+
const ranCount = results.filter((r) => r.success).length;
|
|
2309
|
+
const totalAnalyzers = Object.keys(CONFIDENCE_WEIGHTS).length;
|
|
2310
|
+
const explanationParts = [
|
|
2311
|
+
`${ranCount}/${totalAnalyzers} analyzers ran successfully`,
|
|
2312
|
+
`Files coverage: ${(coverage.filesCoverage * 100).toFixed(0)}%`
|
|
2313
|
+
];
|
|
2314
|
+
const failedAnalyzers = results.filter((r) => !r.success);
|
|
2315
|
+
if (failedAnalyzers.length > 0) {
|
|
2316
|
+
explanationParts.push(`Failed: ${failedAnalyzers.map((r) => r.analyzer).join(", ")}`);
|
|
2317
|
+
}
|
|
2318
|
+
return {
|
|
2319
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
2320
|
+
breakdown,
|
|
2321
|
+
explanation: explanationParts.join(". "),
|
|
2322
|
+
maxPossibleConfidence: Math.round(maxPossibleConfidence * 100) / 100
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
__name(calculateConfidence, "calculateConfidence");
|
|
2326
|
+
function groupBySeverity(issues) {
|
|
2327
|
+
const grouped = {
|
|
2328
|
+
critical: [],
|
|
2329
|
+
high: [],
|
|
2330
|
+
medium: [],
|
|
2331
|
+
low: [],
|
|
2332
|
+
info: []
|
|
2333
|
+
};
|
|
2334
|
+
for (const issue of issues) {
|
|
2335
|
+
const severity = issue.severity || "info";
|
|
2336
|
+
if (severity in grouped) {
|
|
2337
|
+
grouped[severity].push(issue);
|
|
2338
|
+
} else {
|
|
2339
|
+
grouped.info.push(issue);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return grouped;
|
|
2343
|
+
}
|
|
2344
|
+
__name(groupBySeverity, "groupBySeverity");
|
|
2345
|
+
|
|
1251
2346
|
// ../../packages/core/dist/analysis/static/OrphanDetector.js
|
|
1252
2347
|
var DEFAULT_OPTIONS = {
|
|
1253
2348
|
fileExtensions: [
|
|
@@ -1350,7 +2445,7 @@ async function runStaticAnalysis(files, _workspaceRoot, options = {}) {
|
|
|
1350
2445
|
};
|
|
1351
2446
|
if (!options.skipTestDetection) {
|
|
1352
2447
|
try {
|
|
1353
|
-
const { analyzeSkippedTests: analyzeSkippedTests2 } = await import('./SkippedTestDetector-
|
|
2448
|
+
const { analyzeSkippedTests: analyzeSkippedTests2 } = await import('./SkippedTestDetector-AXTMWWHC.js');
|
|
1354
2449
|
const testResults = analyzeSkippedTests2(files);
|
|
1355
2450
|
for (const testResult of testResults) {
|
|
1356
2451
|
if (!testResult.parsed && testResult.error) {
|
|
@@ -1376,6 +2471,4 @@ async function runStaticAnalysis(files, _workspaceRoot, options = {}) {
|
|
|
1376
2471
|
}
|
|
1377
2472
|
__name(runStaticAnalysis, "runStaticAnalysis");
|
|
1378
2473
|
|
|
1379
|
-
export { ChangeImpactAnalyzer, CompletenessAnalyzer, SecurityAnalyzer, SyntaxAnalyzer, checkFilesForOrphanStatus, createChangeImpactAnalyzer, detectOrphans, filterOrphansToFiles, runStaticAnalysis };
|
|
1380
|
-
//# sourceMappingURL=chunk-G7QXHNGB.js.map
|
|
1381
|
-
//# sourceMappingURL=chunk-G7QXHNGB.js.map
|
|
2474
|
+
export { ChangeImpactAnalyzer, CompletenessAnalyzer, ComplexityAnalyzer, ImportGraphAnalyzer, SecurityAnalyzer, SyntaxAnalyzer, checkFilesForOrphanStatus, countASTNodes, createChangeImpactAnalyzer, detectOrphans, extractImportSources, extractImports, extractImportsBatch, filterOrphansToFiles, isSupportedFile, offsetToLine, parseSource, runAnalysisPipeline, runStaticAnalysis, walkAST };
|