@snapback/cli 1.6.0 → 3.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/README.md +120 -21
- 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-TDIHGKKL.js +1446 -0
- package/dist/auto-provision-organization-CXHL46P3.js +161 -0
- package/dist/{chunk-FVIYXFCL.js → chunk-4YTE4JEW.js} +2 -3
- package/dist/chunk-5EOPYJ4Y.js +12 -0
- package/dist/{chunk-ARVV3F4K.js → chunk-5SQA44V7.js} +1085 -18
- package/dist/{chunk-RB7H4UQJ.js → chunk-7ADPL4Q3.js} +10 -3
- package/dist/chunk-CBGOC6RV.js +293 -0
- package/dist/chunk-CPZWXRP2.js +4432 -0
- package/dist/{chunk-7JX6Y4TL.js → chunk-DPWFZNMY.js} +21 -34
- package/dist/{chunk-R7CUQ7CU.js → chunk-E6V6QKS7.js} +317 -33
- package/dist/chunk-FMWCFAY7.js +111 -0
- package/dist/chunk-GQ73B37K.js +314 -0
- package/dist/chunk-LIBBDBW5.js +6136 -0
- package/dist/chunk-O7HMAZ7L.js +3497 -0
- package/dist/chunk-PL4HF4M2.js +593 -0
- package/dist/chunk-Q4VC7GND.js +2300 -0
- package/dist/chunk-WS36HDEU.js +3735 -0
- package/dist/chunk-ZBQDE6WJ.js +108 -0
- package/dist/client-62E3L6DW.js +8 -0
- package/dist/dist-5LR7APG5.js +5 -0
- package/dist/dist-NFU5UJEW.js +9 -0
- package/dist/dist-OO5LJHL6.js +12 -0
- package/dist/index.js +61644 -37198
- package/dist/local-service-adapter-AB3UYRUK.js +6 -0
- package/dist/pioneer-oauth-hook-V2JKEXM7.js +12 -0
- package/dist/{secure-credentials-IWQB6KU4.js → secure-credentials-UEPG7GWW.js} +2 -3
- package/dist/snapback-dir-MG7DTRMF.js +6 -0
- package/package.json +12 -11
- package/scripts/postinstall.mjs +2 -3
- package/dist/SkippedTestDetector-5WJZKZQ3.js +0 -5
- package/dist/SkippedTestDetector-5WJZKZQ3.js.map +0 -1
- package/dist/analysis-YI4UNUCM.js +0 -6
- package/dist/analysis-YI4UNUCM.js.map +0 -1
- package/dist/chunk-7JX6Y4TL.js.map +0 -1
- package/dist/chunk-ARVV3F4K.js.map +0 -1
- package/dist/chunk-EU2IZPOK.js +0 -13002
- package/dist/chunk-EU2IZPOK.js.map +0 -1
- package/dist/chunk-FVIYXFCL.js.map +0 -1
- package/dist/chunk-R7CUQ7CU.js.map +0 -1
- package/dist/chunk-RB7H4UQJ.js.map +0 -1
- package/dist/chunk-SOABQWAU.js +0 -385
- package/dist/chunk-SOABQWAU.js.map +0 -1
- package/dist/dist-O6EBXLN6.js +0 -5
- package/dist/dist-O6EBXLN6.js.map +0 -1
- package/dist/dist-PJVBBZTF.js +0 -5
- package/dist/dist-PJVBBZTF.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/learning-pruner-QC4CTJDX.js +0 -5
- package/dist/learning-pruner-QC4CTJDX.js.map +0 -1
- package/dist/secure-credentials-IWQB6KU4.js.map +0 -1
- package/dist/snapback-dir-V6MWXIW4.js +0 -5
- package/dist/snapback-dir-V6MWXIW4.js.map +0 -1
|
@@ -0,0 +1,2475 @@
|
|
|
1
|
+
#!/usr/bin/env node --no-warnings=ExperimentalWarning
|
|
2
|
+
export { analyzeSkippedTests, detectSkippedTests, getSkippedTestSummary } from './chunk-ZBQDE6WJ.js';
|
|
3
|
+
import { __name } from './chunk-7ADPL4Q3.js';
|
|
4
|
+
import { parseSync } from 'oxc-parser';
|
|
5
|
+
import { dirname, resolve, relative, basename } from 'path';
|
|
6
|
+
import * as eslintParser from '@typescript-eslint/parser';
|
|
7
|
+
import { parse } from '@babel/parser';
|
|
8
|
+
import traverse from '@babel/traverse';
|
|
9
|
+
|
|
10
|
+
process.env.SNAPBACK_CLI='true';
|
|
11
|
+
var TS_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
12
|
+
".ts",
|
|
13
|
+
".tsx",
|
|
14
|
+
".mts",
|
|
15
|
+
".cts"
|
|
16
|
+
]);
|
|
17
|
+
var JSX_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
18
|
+
".tsx",
|
|
19
|
+
".jsx"
|
|
20
|
+
]);
|
|
21
|
+
var ALL_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
22
|
+
".ts",
|
|
23
|
+
".tsx",
|
|
24
|
+
".js",
|
|
25
|
+
".jsx",
|
|
26
|
+
".mts",
|
|
27
|
+
".cts",
|
|
28
|
+
".mjs",
|
|
29
|
+
".cjs"
|
|
30
|
+
]);
|
|
31
|
+
function isSupportedFile(filePath) {
|
|
32
|
+
const ext = getExtension(filePath);
|
|
33
|
+
return ALL_EXTENSIONS.has(ext);
|
|
34
|
+
}
|
|
35
|
+
__name(isSupportedFile, "isSupportedFile");
|
|
36
|
+
function parseSource(content, filePath) {
|
|
37
|
+
const ext = getExtension(filePath);
|
|
38
|
+
try {
|
|
39
|
+
const lang = JSX_EXTENSIONS.has(ext) ? TS_EXTENSIONS.has(ext) ? "tsx" : "jsx" : TS_EXTENSIONS.has(ext) ? "ts" : "js";
|
|
40
|
+
const result = parseSync(filePath, content, {
|
|
41
|
+
sourceType: "module",
|
|
42
|
+
lang
|
|
43
|
+
});
|
|
44
|
+
const errors = Array.isArray(result.errors) ? result.errors.map((e) => normalizeError(e)) : [];
|
|
45
|
+
return {
|
|
46
|
+
program: result.program,
|
|
47
|
+
errors,
|
|
48
|
+
success: errors.length === 0
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
program: {
|
|
53
|
+
type: "Program",
|
|
54
|
+
body: [],
|
|
55
|
+
sourceType: "module"
|
|
56
|
+
},
|
|
57
|
+
errors: [
|
|
58
|
+
{
|
|
59
|
+
message: error instanceof Error ? error.message : String(error),
|
|
60
|
+
severity: "error"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
success: false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
__name(parseSource, "parseSource");
|
|
68
|
+
function walkAST(node, visitor, parent) {
|
|
69
|
+
if (!node || typeof node !== "object") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const n = node;
|
|
73
|
+
if (typeof n.type === "string") {
|
|
74
|
+
visitor(n, parent);
|
|
75
|
+
}
|
|
76
|
+
for (const key of Object.keys(n)) {
|
|
77
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc") {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const value = n[key];
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
for (const item of value) {
|
|
83
|
+
if (item && typeof item === "object" && typeof item.type === "string") {
|
|
84
|
+
walkAST(item, visitor, n);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else if (value && typeof value === "object" && typeof value.type === "string") {
|
|
88
|
+
walkAST(value, visitor, n);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
__name(walkAST, "walkAST");
|
|
93
|
+
function countASTNodes(program) {
|
|
94
|
+
let count = 0;
|
|
95
|
+
walkAST(program, () => {
|
|
96
|
+
count++;
|
|
97
|
+
});
|
|
98
|
+
return count;
|
|
99
|
+
}
|
|
100
|
+
__name(countASTNodes, "countASTNodes");
|
|
101
|
+
function offsetToLine(source, offset) {
|
|
102
|
+
if (offset < 0 || offset > source.length) {
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
let line = 1;
|
|
106
|
+
for (let i = 0; i < offset; i++) {
|
|
107
|
+
if (source[i] === "\n") {
|
|
108
|
+
line++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return line;
|
|
112
|
+
}
|
|
113
|
+
__name(offsetToLine, "offsetToLine");
|
|
114
|
+
function getExtension(filePath) {
|
|
115
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
116
|
+
return lastDot >= 0 ? filePath.substring(lastDot).toLowerCase() : "";
|
|
117
|
+
}
|
|
118
|
+
__name(getExtension, "getExtension");
|
|
119
|
+
function normalizeError(e) {
|
|
120
|
+
if (typeof e === "object" && e !== null) {
|
|
121
|
+
const err = e;
|
|
122
|
+
return {
|
|
123
|
+
message: String(err.message ?? err),
|
|
124
|
+
severity: String(err.severity ?? "error"),
|
|
125
|
+
labels: Array.isArray(err.labels) ? err.labels : void 0
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
message: String(e),
|
|
130
|
+
severity: "error"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
__name(normalizeError, "normalizeError");
|
|
134
|
+
|
|
135
|
+
// ../../node_modules/@snapback/core/dist/analysis/ast/ComplexityAnalyzer.js
|
|
136
|
+
var THRESHOLDS = {
|
|
137
|
+
/** Cyclomatic complexity per function */
|
|
138
|
+
maxCyclomaticPerFunction: 15,
|
|
139
|
+
/** Maximum nesting depth per function */
|
|
140
|
+
maxNestingDepth: 5,
|
|
141
|
+
/** Maximum parameters per function */
|
|
142
|
+
maxParameters: 5,
|
|
143
|
+
/** Maximum functions per file */
|
|
144
|
+
maxFunctionsPerFile: 30,
|
|
145
|
+
/** File-level aggregate cyclomatic complexity */
|
|
146
|
+
maxCyclomaticPerFile: 50
|
|
147
|
+
};
|
|
148
|
+
var BRANCH_NODES = /* @__PURE__ */ new Set([
|
|
149
|
+
"IfStatement",
|
|
150
|
+
"ConditionalExpression",
|
|
151
|
+
"SwitchCase",
|
|
152
|
+
"ForStatement",
|
|
153
|
+
"ForInStatement",
|
|
154
|
+
"ForOfStatement",
|
|
155
|
+
"WhileStatement",
|
|
156
|
+
"DoWhileStatement",
|
|
157
|
+
"CatchClause"
|
|
158
|
+
]);
|
|
159
|
+
var LOGICAL_OPERATORS = /* @__PURE__ */ new Set([
|
|
160
|
+
"&&",
|
|
161
|
+
"||",
|
|
162
|
+
"??"
|
|
163
|
+
]);
|
|
164
|
+
var FUNCTION_NODES = /* @__PURE__ */ new Set([
|
|
165
|
+
"FunctionDeclaration",
|
|
166
|
+
"FunctionExpression",
|
|
167
|
+
"ArrowFunctionExpression",
|
|
168
|
+
"MethodDefinition"
|
|
169
|
+
]);
|
|
170
|
+
var ComplexityAnalyzer = class {
|
|
171
|
+
static {
|
|
172
|
+
__name(this, "ComplexityAnalyzer");
|
|
173
|
+
}
|
|
174
|
+
id = "complexity";
|
|
175
|
+
name = "Complexity Analysis";
|
|
176
|
+
filePatterns = [
|
|
177
|
+
"*.ts",
|
|
178
|
+
"*.tsx",
|
|
179
|
+
"*.js",
|
|
180
|
+
"*.jsx"
|
|
181
|
+
];
|
|
182
|
+
async analyze(context) {
|
|
183
|
+
const startTime = performance.now();
|
|
184
|
+
const issues = [];
|
|
185
|
+
let filesAnalyzed = 0;
|
|
186
|
+
let totalNodesVisited = 0;
|
|
187
|
+
const parseErrors = [];
|
|
188
|
+
for (const [file, content] of context.contents) {
|
|
189
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
filesAnalyzed++;
|
|
193
|
+
const fileComplexity = this.analyzeFile(content, file);
|
|
194
|
+
if (!fileComplexity) {
|
|
195
|
+
parseErrors.push(`${file}: Failed to parse`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
totalNodesVisited += fileComplexity.functions.length;
|
|
199
|
+
for (const fn of fileComplexity.functions) {
|
|
200
|
+
if (fn.cyclomatic > THRESHOLDS.maxCyclomaticPerFunction) {
|
|
201
|
+
issues.push({
|
|
202
|
+
id: `complexity/cyclomatic/${file}/${fn.line}`,
|
|
203
|
+
severity: fn.cyclomatic > THRESHOLDS.maxCyclomaticPerFunction * 2 ? "high" : "medium",
|
|
204
|
+
type: "HIGH_CYCLOMATIC_COMPLEXITY",
|
|
205
|
+
message: `Function "${fn.name}" has cyclomatic complexity ${fn.cyclomatic} (max: ${THRESHOLDS.maxCyclomaticPerFunction})`,
|
|
206
|
+
file,
|
|
207
|
+
line: fn.line,
|
|
208
|
+
fix: "Extract helper functions or simplify branching logic"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (fn.maxNesting > THRESHOLDS.maxNestingDepth) {
|
|
212
|
+
issues.push({
|
|
213
|
+
id: `complexity/nesting/${file}/${fn.line}`,
|
|
214
|
+
severity: "medium",
|
|
215
|
+
type: "DEEP_NESTING",
|
|
216
|
+
message: `Function "${fn.name}" has nesting depth ${fn.maxNesting} (max: ${THRESHOLDS.maxNestingDepth})`,
|
|
217
|
+
file,
|
|
218
|
+
line: fn.line,
|
|
219
|
+
fix: "Use early returns or extract nested logic into helper functions"
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (fn.parameters > THRESHOLDS.maxParameters) {
|
|
223
|
+
issues.push({
|
|
224
|
+
id: `complexity/parameters/${file}/${fn.line}`,
|
|
225
|
+
severity: "low",
|
|
226
|
+
type: "TOO_MANY_PARAMETERS",
|
|
227
|
+
message: `Function "${fn.name}" has ${fn.parameters} parameters (max: ${THRESHOLDS.maxParameters})`,
|
|
228
|
+
file,
|
|
229
|
+
line: fn.line,
|
|
230
|
+
fix: "Use an options object pattern to reduce parameter count"
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (fileComplexity.functionCount > THRESHOLDS.maxFunctionsPerFile) {
|
|
235
|
+
issues.push({
|
|
236
|
+
id: `complexity/function-count/${file}`,
|
|
237
|
+
severity: "low",
|
|
238
|
+
type: "TOO_MANY_FUNCTIONS",
|
|
239
|
+
message: `${file} has ${fileComplexity.functionCount} functions (max: ${THRESHOLDS.maxFunctionsPerFile})`,
|
|
240
|
+
file,
|
|
241
|
+
fix: "Split into multiple focused modules"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (fileComplexity.totalCyclomatic > THRESHOLDS.maxCyclomaticPerFile) {
|
|
245
|
+
issues.push({
|
|
246
|
+
id: `complexity/file-complexity/${file}`,
|
|
247
|
+
severity: "medium",
|
|
248
|
+
type: "HIGH_FILE_COMPLEXITY",
|
|
249
|
+
message: `${file} has total cyclomatic complexity ${fileComplexity.totalCyclomatic} (max: ${THRESHOLDS.maxCyclomaticPerFile})`,
|
|
250
|
+
file,
|
|
251
|
+
fix: "Consider splitting this file into smaller modules"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
analyzer: this.id,
|
|
257
|
+
success: true,
|
|
258
|
+
issues,
|
|
259
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
260
|
+
duration: performance.now() - startTime,
|
|
261
|
+
metadata: {
|
|
262
|
+
filesAnalyzed,
|
|
263
|
+
nodesVisited: totalNodesVisited,
|
|
264
|
+
patternsChecked: [
|
|
265
|
+
"HIGH_CYCLOMATIC_COMPLEXITY",
|
|
266
|
+
"DEEP_NESTING",
|
|
267
|
+
"TOO_MANY_PARAMETERS",
|
|
268
|
+
"TOO_MANY_FUNCTIONS",
|
|
269
|
+
"HIGH_FILE_COMPLEXITY"
|
|
270
|
+
],
|
|
271
|
+
parseErrors
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
shouldRun(context) {
|
|
276
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Analyze a single file and return complexity metrics.
|
|
280
|
+
* Useful for external callers that just want metrics, not issues.
|
|
281
|
+
*/
|
|
282
|
+
analyzeFile(content, filePath) {
|
|
283
|
+
if (!isSupportedFile(filePath)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const { program, success } = parseSource(content, filePath);
|
|
287
|
+
if (!success && program.body.length === 0) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const functions = [];
|
|
291
|
+
let fileMaxNesting = 0;
|
|
292
|
+
walkAST(program, (node) => {
|
|
293
|
+
if (!FUNCTION_NODES.has(node.type)) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const fn = this.analyzeFunctionNode(node);
|
|
297
|
+
functions.push(fn);
|
|
298
|
+
if (fn.maxNesting > fileMaxNesting) {
|
|
299
|
+
fileMaxNesting = fn.maxNesting;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
const totalCyclomatic = functions.reduce((sum, fn) => sum + fn.cyclomatic, 0);
|
|
303
|
+
return {
|
|
304
|
+
filePath,
|
|
305
|
+
functions,
|
|
306
|
+
totalCyclomatic,
|
|
307
|
+
maxNesting: fileMaxNesting,
|
|
308
|
+
functionCount: functions.length,
|
|
309
|
+
averageCyclomatic: functions.length > 0 ? totalCyclomatic / functions.length : 0
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// -----------------------------------------------------------------------
|
|
313
|
+
// Per-function analysis
|
|
314
|
+
// -----------------------------------------------------------------------
|
|
315
|
+
analyzeFunctionNode(node) {
|
|
316
|
+
const name = this.getFunctionName(node);
|
|
317
|
+
const line = node.start ?? 0;
|
|
318
|
+
const params = this.getParameterCount(node);
|
|
319
|
+
let cyclomatic = 1;
|
|
320
|
+
let maxNesting = 0;
|
|
321
|
+
const body = this.getFunctionBody(node);
|
|
322
|
+
if (body) {
|
|
323
|
+
this.walkForComplexity(body, (n, depth) => {
|
|
324
|
+
if (BRANCH_NODES.has(n.type)) {
|
|
325
|
+
cyclomatic++;
|
|
326
|
+
}
|
|
327
|
+
if (n.type === "LogicalExpression") {
|
|
328
|
+
const operator = n.operator;
|
|
329
|
+
if (LOGICAL_OPERATORS.has(operator)) {
|
|
330
|
+
cyclomatic++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (depth > maxNesting) {
|
|
334
|
+
maxNesting = depth;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
name,
|
|
340
|
+
line,
|
|
341
|
+
cyclomatic,
|
|
342
|
+
maxNesting,
|
|
343
|
+
parameters: params
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Walk AST nodes counting nesting depth for complexity metrics
|
|
348
|
+
*/
|
|
349
|
+
walkForComplexity(node, callback, depth = 0) {
|
|
350
|
+
if (!node || typeof node !== "object") {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const n = node;
|
|
354
|
+
if (typeof n.type !== "string") {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const nestingNodes = /* @__PURE__ */ new Set([
|
|
358
|
+
"IfStatement",
|
|
359
|
+
"ForStatement",
|
|
360
|
+
"ForInStatement",
|
|
361
|
+
"ForOfStatement",
|
|
362
|
+
"WhileStatement",
|
|
363
|
+
"DoWhileStatement",
|
|
364
|
+
"SwitchStatement",
|
|
365
|
+
"TryStatement"
|
|
366
|
+
]);
|
|
367
|
+
const newDepth = nestingNodes.has(n.type) ? depth + 1 : depth;
|
|
368
|
+
callback(n, newDepth);
|
|
369
|
+
for (const key of Object.keys(n)) {
|
|
370
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc") {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const value = n[key];
|
|
374
|
+
if (Array.isArray(value)) {
|
|
375
|
+
for (const item of value) {
|
|
376
|
+
this.walkForComplexity(item, callback, newDepth);
|
|
377
|
+
}
|
|
378
|
+
} else if (value && typeof value === "object" && typeof value.type === "string") {
|
|
379
|
+
this.walkForComplexity(value, callback, newDepth);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// -----------------------------------------------------------------------
|
|
384
|
+
// Helpers
|
|
385
|
+
// -----------------------------------------------------------------------
|
|
386
|
+
getFunctionName(node) {
|
|
387
|
+
if (node.id && typeof node.id === "object") {
|
|
388
|
+
const id = node.id;
|
|
389
|
+
if (typeof id.name === "string") {
|
|
390
|
+
return id.name;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (node.key && typeof node.key === "object") {
|
|
394
|
+
const key = node.key;
|
|
395
|
+
if (typeof key.name === "string") {
|
|
396
|
+
return key.name;
|
|
397
|
+
}
|
|
398
|
+
if (typeof key.value === "string") {
|
|
399
|
+
return key.value;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return "<anonymous>";
|
|
403
|
+
}
|
|
404
|
+
getParameterCount(node) {
|
|
405
|
+
if (node.type === "MethodDefinition") {
|
|
406
|
+
const value = node.value;
|
|
407
|
+
if (value && Array.isArray(value.params)) {
|
|
408
|
+
return value.params.length;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (Array.isArray(node.params)) {
|
|
412
|
+
return node.params.length;
|
|
413
|
+
}
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
getFunctionBody(node) {
|
|
417
|
+
if (node.type === "MethodDefinition") {
|
|
418
|
+
const value = node.value;
|
|
419
|
+
if (value?.body) {
|
|
420
|
+
return value.body;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (node.body) {
|
|
424
|
+
return node.body;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
shouldAnalyzeFile(file) {
|
|
429
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
430
|
+
return [
|
|
431
|
+
"ts",
|
|
432
|
+
"tsx",
|
|
433
|
+
"js",
|
|
434
|
+
"jsx",
|
|
435
|
+
"mts",
|
|
436
|
+
"cts"
|
|
437
|
+
].includes(ext ?? "");
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// ../../node_modules/@snapback/core/dist/analysis/ast/import-extractor.js
|
|
442
|
+
function extractImports(content, filePath) {
|
|
443
|
+
if (!isSupportedFile(filePath)) {
|
|
444
|
+
return {
|
|
445
|
+
filePath,
|
|
446
|
+
imports: [],
|
|
447
|
+
parseSuccess: false,
|
|
448
|
+
parseErrors: [
|
|
449
|
+
`Unsupported file type: ${filePath}`
|
|
450
|
+
]
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const { program, errors, success } = parseSource(content, filePath);
|
|
454
|
+
const imports = [];
|
|
455
|
+
walkAST(program, (node) => {
|
|
456
|
+
switch (node.type) {
|
|
457
|
+
// import ... from 'source'
|
|
458
|
+
case "ImportDeclaration":
|
|
459
|
+
handleImportDeclaration(node, imports);
|
|
460
|
+
break;
|
|
461
|
+
// export { ... } from 'source' OR export * from 'source'
|
|
462
|
+
case "ExportNamedDeclaration":
|
|
463
|
+
case "ExportAllDeclaration":
|
|
464
|
+
handleExportDeclaration(node, imports);
|
|
465
|
+
break;
|
|
466
|
+
// import('source') — ESTree ImportExpression
|
|
467
|
+
case "ImportExpression":
|
|
468
|
+
handleImportExpression(node, imports);
|
|
469
|
+
break;
|
|
470
|
+
// require('source') OR legacy import('source') as CallExpression
|
|
471
|
+
case "CallExpression":
|
|
472
|
+
handleCallExpression(node, imports);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
filePath,
|
|
478
|
+
imports,
|
|
479
|
+
parseSuccess: success,
|
|
480
|
+
parseErrors: errors.map((e) => e.message)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
__name(extractImports, "extractImports");
|
|
484
|
+
function extractImportSources(content, filePath) {
|
|
485
|
+
const result = extractImports(content, filePath);
|
|
486
|
+
const sources = /* @__PURE__ */ new Set();
|
|
487
|
+
for (const imp of result.imports) {
|
|
488
|
+
sources.add(imp.source);
|
|
489
|
+
}
|
|
490
|
+
return [
|
|
491
|
+
...sources
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
__name(extractImportSources, "extractImportSources");
|
|
495
|
+
function extractImportsBatch(files) {
|
|
496
|
+
const results = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const [filePath, content] of files) {
|
|
498
|
+
results.set(filePath, extractImports(content, filePath));
|
|
499
|
+
}
|
|
500
|
+
return results;
|
|
501
|
+
}
|
|
502
|
+
__name(extractImportsBatch, "extractImportsBatch");
|
|
503
|
+
function handleImportDeclaration(node, imports) {
|
|
504
|
+
const source = getStringValue(node.source);
|
|
505
|
+
if (!source) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const specifiers = [];
|
|
509
|
+
if (Array.isArray(node.specifiers)) {
|
|
510
|
+
for (const spec of node.specifiers) {
|
|
511
|
+
const s = spec;
|
|
512
|
+
if (s.type === "ImportSpecifier") {
|
|
513
|
+
const imported = s.imported;
|
|
514
|
+
specifiers.push(getIdentifierName(imported) ?? "unknown");
|
|
515
|
+
} else if (s.type === "ImportDefaultSpecifier") {
|
|
516
|
+
specifiers.push("default");
|
|
517
|
+
} else if (s.type === "ImportNamespaceSpecifier") {
|
|
518
|
+
specifiers.push("*");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
imports.push({
|
|
523
|
+
source,
|
|
524
|
+
kind: "static",
|
|
525
|
+
typeOnly: node.importKind === "type" || Boolean(node.isTypeOnly),
|
|
526
|
+
line: node.start != null ? node.start : void 0,
|
|
527
|
+
specifiers
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
__name(handleImportDeclaration, "handleImportDeclaration");
|
|
531
|
+
function handleExportDeclaration(node, imports) {
|
|
532
|
+
const source = getStringValue(node.source);
|
|
533
|
+
if (!source) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const specifiers = [];
|
|
537
|
+
if (node.type === "ExportAllDeclaration") {
|
|
538
|
+
specifiers.push("*");
|
|
539
|
+
} else if (Array.isArray(node.specifiers)) {
|
|
540
|
+
for (const spec of node.specifiers) {
|
|
541
|
+
const s = spec;
|
|
542
|
+
const local = s.local;
|
|
543
|
+
specifiers.push(getIdentifierName(local) ?? "unknown");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
imports.push({
|
|
547
|
+
source,
|
|
548
|
+
kind: "re-export",
|
|
549
|
+
typeOnly: node.exportKind === "type" || Boolean(node.isTypeOnly),
|
|
550
|
+
line: node.start != null ? node.start : void 0,
|
|
551
|
+
specifiers
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
__name(handleExportDeclaration, "handleExportDeclaration");
|
|
555
|
+
function handleImportExpression(node, imports) {
|
|
556
|
+
const source = getStringValue(node.source);
|
|
557
|
+
if (source) {
|
|
558
|
+
imports.push({
|
|
559
|
+
source,
|
|
560
|
+
kind: "dynamic",
|
|
561
|
+
typeOnly: false,
|
|
562
|
+
line: node.start != null ? node.start : void 0,
|
|
563
|
+
specifiers: []
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
__name(handleImportExpression, "handleImportExpression");
|
|
568
|
+
function handleCallExpression(node, imports) {
|
|
569
|
+
const callee = node.callee;
|
|
570
|
+
if (!callee) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (node.type === "CallExpression" && callee.type === "Import") {
|
|
574
|
+
const args = node.arguments;
|
|
575
|
+
if (args && args.length > 0) {
|
|
576
|
+
const source = getStringValue(args[0]);
|
|
577
|
+
if (source) {
|
|
578
|
+
imports.push({
|
|
579
|
+
source,
|
|
580
|
+
kind: "dynamic",
|
|
581
|
+
typeOnly: false,
|
|
582
|
+
line: node.start != null ? node.start : void 0,
|
|
583
|
+
specifiers: []
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (callee.type === "Identifier" && callee.name === "require") {
|
|
590
|
+
const args = node.arguments;
|
|
591
|
+
if (args && args.length > 0) {
|
|
592
|
+
const source = getStringValue(args[0]);
|
|
593
|
+
if (source) {
|
|
594
|
+
imports.push({
|
|
595
|
+
source,
|
|
596
|
+
kind: "require",
|
|
597
|
+
typeOnly: false,
|
|
598
|
+
line: node.start != null ? node.start : void 0,
|
|
599
|
+
specifiers: []
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
__name(handleCallExpression, "handleCallExpression");
|
|
606
|
+
function getStringValue(node) {
|
|
607
|
+
if (!node || typeof node !== "object") {
|
|
608
|
+
return void 0;
|
|
609
|
+
}
|
|
610
|
+
const n = node;
|
|
611
|
+
if (n.type === "StringLiteral" || n.type === "Literal") {
|
|
612
|
+
return typeof n.value === "string" ? n.value : void 0;
|
|
613
|
+
}
|
|
614
|
+
return void 0;
|
|
615
|
+
}
|
|
616
|
+
__name(getStringValue, "getStringValue");
|
|
617
|
+
function getIdentifierName(node) {
|
|
618
|
+
if (!node || typeof node !== "object") {
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
const n = node;
|
|
622
|
+
if (n.type === "Identifier" || n.type === "IdentifierName" || n.type === "IdentifierReference") {
|
|
623
|
+
return typeof n.name === "string" ? n.name : void 0;
|
|
624
|
+
}
|
|
625
|
+
return void 0;
|
|
626
|
+
}
|
|
627
|
+
__name(getIdentifierName, "getIdentifierName");
|
|
628
|
+
|
|
629
|
+
// ../../node_modules/@snapback/core/dist/analysis/ast/ImportGraphAnalyzer.js
|
|
630
|
+
var THRESHOLDS2 = {
|
|
631
|
+
/** Max files that can import a single file before it's flagged as high fan-in */
|
|
632
|
+
highFanIn: 15,
|
|
633
|
+
/** Max imports a single file can have before it's flagged as high fan-out */
|
|
634
|
+
highFanOut: 20,
|
|
635
|
+
/** Minimum cycle length to report (avoids noise from self-imports) */
|
|
636
|
+
minCycleLength: 2
|
|
637
|
+
};
|
|
638
|
+
var ImportGraphAnalyzer = class {
|
|
639
|
+
static {
|
|
640
|
+
__name(this, "ImportGraphAnalyzer");
|
|
641
|
+
}
|
|
642
|
+
id = "import-graph";
|
|
643
|
+
name = "Import Graph Analysis";
|
|
644
|
+
filePatterns = [
|
|
645
|
+
"*.ts",
|
|
646
|
+
"*.tsx",
|
|
647
|
+
"*.js",
|
|
648
|
+
"*.jsx"
|
|
649
|
+
];
|
|
650
|
+
async analyze(context) {
|
|
651
|
+
const startTime = performance.now();
|
|
652
|
+
const issues = [];
|
|
653
|
+
let filesAnalyzed = 0;
|
|
654
|
+
const parseErrors = [];
|
|
655
|
+
const extractions = /* @__PURE__ */ new Map();
|
|
656
|
+
for (const [file, content] of context.contents) {
|
|
657
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
filesAnalyzed++;
|
|
661
|
+
const result = extractImports(content, file);
|
|
662
|
+
extractions.set(file, result);
|
|
663
|
+
if (!result.parseSuccess) {
|
|
664
|
+
parseErrors.push(...result.parseErrors);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const graph = this.buildGraph(extractions, context.workspaceRoot);
|
|
668
|
+
const cycles = this.detectCycles(graph.edges);
|
|
669
|
+
graph.cycles = cycles;
|
|
670
|
+
for (const cycle of cycles) {
|
|
671
|
+
issues.push({
|
|
672
|
+
id: `import-graph/circular/${cycle.join("->")}`,
|
|
673
|
+
severity: "high",
|
|
674
|
+
type: "CIRCULAR_DEPENDENCY",
|
|
675
|
+
message: `Circular dependency: ${cycle.join(" \u2192 ")}`,
|
|
676
|
+
file: cycle[0],
|
|
677
|
+
fix: "Break the cycle by extracting shared code or using dependency injection"
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
for (const [file, node] of graph.nodes) {
|
|
681
|
+
if (node.importedBy.length > THRESHOLDS2.highFanIn) {
|
|
682
|
+
issues.push({
|
|
683
|
+
id: `import-graph/high-fan-in/${file}`,
|
|
684
|
+
severity: "medium",
|
|
685
|
+
type: "HIGH_FAN_IN",
|
|
686
|
+
message: `${file} is imported by ${node.importedBy.length} files \u2014 changes here have high blast radius`,
|
|
687
|
+
file,
|
|
688
|
+
fix: "Consider splitting into smaller, more focused modules"
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const [file, node] of graph.nodes) {
|
|
693
|
+
const runtimeImports = node.imports.filter((imp) => !node.typeOnlyImports.includes(imp));
|
|
694
|
+
if (runtimeImports.length > THRESHOLDS2.highFanOut) {
|
|
695
|
+
issues.push({
|
|
696
|
+
id: `import-graph/high-fan-out/${file}`,
|
|
697
|
+
severity: "low",
|
|
698
|
+
type: "HIGH_FAN_OUT",
|
|
699
|
+
message: `${file} imports ${runtimeImports.length} modules \u2014 high coupling`,
|
|
700
|
+
file,
|
|
701
|
+
fix: "Consider using a facade or consolidating related imports"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
for (const [file, node] of graph.nodes) {
|
|
706
|
+
if (node.importedBy.length === 0 && !this.isEntryPoint(file)) {
|
|
707
|
+
issues.push({
|
|
708
|
+
id: `import-graph/orphan/${file}`,
|
|
709
|
+
severity: "info",
|
|
710
|
+
type: "ORPHAN_FILE",
|
|
711
|
+
message: `${file} is not imported by any other analyzed file`,
|
|
712
|
+
file,
|
|
713
|
+
fix: "Verify this file is needed \u2014 it may be dead code"
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
analyzer: this.id,
|
|
719
|
+
success: true,
|
|
720
|
+
issues,
|
|
721
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
722
|
+
duration: performance.now() - startTime,
|
|
723
|
+
metadata: {
|
|
724
|
+
filesAnalyzed,
|
|
725
|
+
nodesVisited: graph.nodes.size,
|
|
726
|
+
patternsChecked: [
|
|
727
|
+
"CIRCULAR_DEPENDENCY",
|
|
728
|
+
"HIGH_FAN_IN",
|
|
729
|
+
"HIGH_FAN_OUT",
|
|
730
|
+
"ORPHAN_FILE"
|
|
731
|
+
],
|
|
732
|
+
parseErrors
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
shouldRun(context) {
|
|
737
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Build the import graph and return it for external consumption.
|
|
741
|
+
* Useful for other tools (momentum scoring, risk propagation, etc.).
|
|
742
|
+
*/
|
|
743
|
+
buildGraphFromContext(context) {
|
|
744
|
+
const extractions = /* @__PURE__ */ new Map();
|
|
745
|
+
for (const [file, content] of context.contents) {
|
|
746
|
+
if (this.shouldAnalyzeFile(file)) {
|
|
747
|
+
extractions.set(file, extractImports(content, file));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const graph = this.buildGraph(extractions, context.workspaceRoot);
|
|
751
|
+
graph.cycles = this.detectCycles(graph.edges);
|
|
752
|
+
return graph;
|
|
753
|
+
}
|
|
754
|
+
// -----------------------------------------------------------------------
|
|
755
|
+
// Graph construction
|
|
756
|
+
// -----------------------------------------------------------------------
|
|
757
|
+
buildGraph(extractions, workspaceRoot) {
|
|
758
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
759
|
+
const edges = /* @__PURE__ */ new Map();
|
|
760
|
+
const reverseEdges = /* @__PURE__ */ new Map();
|
|
761
|
+
for (const filePath of extractions.keys()) {
|
|
762
|
+
const normalized = this.normalizePath(filePath);
|
|
763
|
+
nodes.set(normalized, {
|
|
764
|
+
filePath: normalized,
|
|
765
|
+
imports: [],
|
|
766
|
+
importedBy: [],
|
|
767
|
+
typeOnlyImports: []
|
|
768
|
+
});
|
|
769
|
+
edges.set(normalized, /* @__PURE__ */ new Set());
|
|
770
|
+
}
|
|
771
|
+
for (const [filePath, extraction] of extractions) {
|
|
772
|
+
const normalized = this.normalizePath(filePath);
|
|
773
|
+
for (const imp of extraction.imports) {
|
|
774
|
+
const resolved = this.resolveImport(imp.source, filePath, workspaceRoot);
|
|
775
|
+
if (!resolved) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const resolvedNorm = this.normalizePath(resolved);
|
|
779
|
+
edges.get(normalized)?.add(resolvedNorm);
|
|
780
|
+
const node = nodes.get(normalized);
|
|
781
|
+
if (node && !node.imports.includes(resolvedNorm)) {
|
|
782
|
+
node.imports.push(resolvedNorm);
|
|
783
|
+
if (imp.typeOnly) {
|
|
784
|
+
node.typeOnlyImports.push(resolvedNorm);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (!reverseEdges.has(resolvedNorm)) {
|
|
788
|
+
reverseEdges.set(resolvedNorm, /* @__PURE__ */ new Set());
|
|
789
|
+
}
|
|
790
|
+
reverseEdges.get(resolvedNorm)?.add(normalized);
|
|
791
|
+
if (!nodes.has(resolvedNorm)) {
|
|
792
|
+
nodes.set(resolvedNorm, {
|
|
793
|
+
filePath: resolvedNorm,
|
|
794
|
+
imports: [],
|
|
795
|
+
importedBy: [],
|
|
796
|
+
typeOnlyImports: []
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
for (const [file, importers] of reverseEdges) {
|
|
802
|
+
const node = nodes.get(file);
|
|
803
|
+
if (node) {
|
|
804
|
+
node.importedBy = [
|
|
805
|
+
...importers
|
|
806
|
+
];
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
nodes,
|
|
811
|
+
edges,
|
|
812
|
+
reverseEdges,
|
|
813
|
+
cycles: []
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
// -----------------------------------------------------------------------
|
|
817
|
+
// Cycle detection (Tarjan's SCC adapted for cycles)
|
|
818
|
+
// -----------------------------------------------------------------------
|
|
819
|
+
detectCycles(edges) {
|
|
820
|
+
const cycles = [];
|
|
821
|
+
const visited = /* @__PURE__ */ new Set();
|
|
822
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
823
|
+
const stack = [];
|
|
824
|
+
const dfs = /* @__PURE__ */ __name((node) => {
|
|
825
|
+
if (inStack.has(node)) {
|
|
826
|
+
const cycleStart = stack.indexOf(node);
|
|
827
|
+
if (cycleStart >= 0) {
|
|
828
|
+
const cycle = stack.slice(cycleStart);
|
|
829
|
+
if (cycle.length >= THRESHOLDS2.minCycleLength) {
|
|
830
|
+
cycles.push([
|
|
831
|
+
...cycle,
|
|
832
|
+
node
|
|
833
|
+
]);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (visited.has(node)) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
visited.add(node);
|
|
842
|
+
inStack.add(node);
|
|
843
|
+
stack.push(node);
|
|
844
|
+
const neighbors = edges.get(node) ?? /* @__PURE__ */ new Set();
|
|
845
|
+
for (const neighbor of neighbors) {
|
|
846
|
+
dfs(neighbor);
|
|
847
|
+
}
|
|
848
|
+
stack.pop();
|
|
849
|
+
inStack.delete(node);
|
|
850
|
+
}, "dfs");
|
|
851
|
+
for (const node of edges.keys()) {
|
|
852
|
+
dfs(node);
|
|
853
|
+
}
|
|
854
|
+
return cycles;
|
|
855
|
+
}
|
|
856
|
+
// -----------------------------------------------------------------------
|
|
857
|
+
// Import resolution
|
|
858
|
+
// -----------------------------------------------------------------------
|
|
859
|
+
resolveImport(importSource, fromFile, _workspaceRoot) {
|
|
860
|
+
if (!importSource.startsWith(".") && !importSource.startsWith("/")) {
|
|
861
|
+
if (importSource.startsWith("@")) {
|
|
862
|
+
const parts = importSource.split("/");
|
|
863
|
+
if (parts.length >= 2) {
|
|
864
|
+
const pkg = parts[1];
|
|
865
|
+
return `packages/${pkg}/src/index.ts`;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
const fromDir = dirname(fromFile);
|
|
871
|
+
let resolved = resolve(fromDir, importSource);
|
|
872
|
+
if (!resolved.match(/\.(ts|tsx|js|jsx|mts|cts|mjs|cjs)$/)) {
|
|
873
|
+
resolved += ".ts";
|
|
874
|
+
}
|
|
875
|
+
resolved = resolved.replace(/\.js$/, ".ts").replace(/\.jsx$/, ".tsx");
|
|
876
|
+
return resolved;
|
|
877
|
+
}
|
|
878
|
+
// -----------------------------------------------------------------------
|
|
879
|
+
// Helpers
|
|
880
|
+
// -----------------------------------------------------------------------
|
|
881
|
+
shouldAnalyzeFile(file) {
|
|
882
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
883
|
+
return [
|
|
884
|
+
"ts",
|
|
885
|
+
"tsx",
|
|
886
|
+
"js",
|
|
887
|
+
"jsx",
|
|
888
|
+
"mts",
|
|
889
|
+
"cts"
|
|
890
|
+
].includes(ext ?? "");
|
|
891
|
+
}
|
|
892
|
+
isEntryPoint(file) {
|
|
893
|
+
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.");
|
|
894
|
+
}
|
|
895
|
+
normalizePath(filePath) {
|
|
896
|
+
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var SyntaxAnalyzer = class {
|
|
900
|
+
static {
|
|
901
|
+
__name(this, "SyntaxAnalyzer");
|
|
902
|
+
}
|
|
903
|
+
id = "syntax";
|
|
904
|
+
name = "Syntax Analysis";
|
|
905
|
+
filePatterns = [
|
|
906
|
+
"*.ts",
|
|
907
|
+
"*.tsx",
|
|
908
|
+
"*.js",
|
|
909
|
+
"*.jsx"
|
|
910
|
+
];
|
|
911
|
+
async analyze(context) {
|
|
912
|
+
const startTime = performance.now();
|
|
913
|
+
const issues = [];
|
|
914
|
+
let filesAnalyzed = 0;
|
|
915
|
+
let nodesVisited = 0;
|
|
916
|
+
const parseErrors = [];
|
|
917
|
+
for (const [file, content] of context.contents) {
|
|
918
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
filesAnalyzed++;
|
|
922
|
+
try {
|
|
923
|
+
const ast = eslintParser.parse(content, {
|
|
924
|
+
sourceType: "module",
|
|
925
|
+
ecmaFeatures: {
|
|
926
|
+
jsx: file.endsWith(".tsx") || file.endsWith(".jsx")
|
|
927
|
+
},
|
|
928
|
+
ecmaVersion: "latest",
|
|
929
|
+
// Error recovery mode to get partial AST even with errors
|
|
930
|
+
errorOnUnknownASTType: false
|
|
931
|
+
});
|
|
932
|
+
nodesVisited += this.countNodes(ast);
|
|
933
|
+
this.checkSyntaxPatterns(content, file, issues);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
const parseError = this.extractParseError(error);
|
|
936
|
+
parseErrors.push(`${file}: ${parseError.message}`);
|
|
937
|
+
issues.push({
|
|
938
|
+
id: `syntax/parse-error/${file}/${parseError.line}`,
|
|
939
|
+
severity: "critical",
|
|
940
|
+
type: "SYNTAX_ERROR",
|
|
941
|
+
message: parseError.message,
|
|
942
|
+
file,
|
|
943
|
+
line: parseError.line,
|
|
944
|
+
column: parseError.column,
|
|
945
|
+
fix: "Fix the syntax error to allow parsing"
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
analyzer: this.id,
|
|
951
|
+
success: true,
|
|
952
|
+
issues,
|
|
953
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
954
|
+
duration: performance.now() - startTime,
|
|
955
|
+
metadata: {
|
|
956
|
+
filesAnalyzed,
|
|
957
|
+
nodesVisited,
|
|
958
|
+
parseErrors
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
shouldRun(context) {
|
|
963
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
964
|
+
}
|
|
965
|
+
shouldAnalyzeFile(file) {
|
|
966
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
967
|
+
return [
|
|
968
|
+
"ts",
|
|
969
|
+
"tsx",
|
|
970
|
+
"js",
|
|
971
|
+
"jsx"
|
|
972
|
+
].includes(ext || "");
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Extract parse error information from parser exception
|
|
976
|
+
*/
|
|
977
|
+
extractParseError(error) {
|
|
978
|
+
if (error instanceof Error) {
|
|
979
|
+
const match = error.message.match(/\((\d+):(\d+)\)/);
|
|
980
|
+
if (match) {
|
|
981
|
+
return {
|
|
982
|
+
message: error.message,
|
|
983
|
+
line: Number.parseInt(match[1], 10),
|
|
984
|
+
column: Number.parseInt(match[2], 10)
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
message: error.message,
|
|
989
|
+
line: 1,
|
|
990
|
+
column: 1
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
message: String(error),
|
|
995
|
+
line: 1,
|
|
996
|
+
column: 1
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Count AST nodes for coverage metrics
|
|
1001
|
+
*/
|
|
1002
|
+
countNodes(node) {
|
|
1003
|
+
if (!node || typeof node !== "object") {
|
|
1004
|
+
return 0;
|
|
1005
|
+
}
|
|
1006
|
+
let count = 1;
|
|
1007
|
+
for (const key of Object.keys(node)) {
|
|
1008
|
+
const value = node[key];
|
|
1009
|
+
if (Array.isArray(value)) {
|
|
1010
|
+
for (const item of value) {
|
|
1011
|
+
count += this.countNodes(item);
|
|
1012
|
+
}
|
|
1013
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
1014
|
+
count += this.countNodes(value);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return count;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Check for additional syntax patterns that may indicate issues
|
|
1021
|
+
*/
|
|
1022
|
+
checkSyntaxPatterns(content, file, issues) {
|
|
1023
|
+
const lines = content.split("\n");
|
|
1024
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1025
|
+
const line = lines[i];
|
|
1026
|
+
const lineNum = i + 1;
|
|
1027
|
+
if (line.includes(";;")) {
|
|
1028
|
+
issues.push({
|
|
1029
|
+
id: `syntax/double-semicolon/${file}/${lineNum}`,
|
|
1030
|
+
severity: "low",
|
|
1031
|
+
type: "SYNTAX_WARNING",
|
|
1032
|
+
message: "Double semicolon detected",
|
|
1033
|
+
file,
|
|
1034
|
+
line: lineNum,
|
|
1035
|
+
column: line.indexOf(";;") + 1,
|
|
1036
|
+
fix: "Remove extra semicolon",
|
|
1037
|
+
snippet: line.trim()
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
if (/console\.assert\([^,]+,\s*\)/.test(line)) {
|
|
1041
|
+
issues.push({
|
|
1042
|
+
id: `syntax/empty-assert/${file}/${lineNum}`,
|
|
1043
|
+
severity: "medium",
|
|
1044
|
+
type: "SYNTAX_WARNING",
|
|
1045
|
+
message: "console.assert with empty message",
|
|
1046
|
+
file,
|
|
1047
|
+
line: lineNum,
|
|
1048
|
+
fix: "Add assertion message for debugging",
|
|
1049
|
+
snippet: line.trim()
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
if (/if\s*\([^=]*=\s*[^=]/.test(line) && !/if\s*\([^=]*[=!]==/.test(line)) {
|
|
1053
|
+
const assignMatch = line.match(/if\s*\(\s*(\w+)\s*=\s*[^=]/);
|
|
1054
|
+
if (assignMatch) {
|
|
1055
|
+
issues.push({
|
|
1056
|
+
id: `syntax/assignment-in-condition/${file}/${lineNum}`,
|
|
1057
|
+
severity: "medium",
|
|
1058
|
+
type: "SYNTAX_WARNING",
|
|
1059
|
+
message: "Possible assignment in condition (did you mean ===?)",
|
|
1060
|
+
file,
|
|
1061
|
+
line: lineNum,
|
|
1062
|
+
fix: "Use === for comparison, or wrap in extra parentheses if intentional",
|
|
1063
|
+
snippet: line.trim()
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
var CompletenessAnalyzer = class {
|
|
1071
|
+
static {
|
|
1072
|
+
__name(this, "CompletenessAnalyzer");
|
|
1073
|
+
}
|
|
1074
|
+
id = "completeness";
|
|
1075
|
+
name = "Completeness Detection";
|
|
1076
|
+
filePatterns = [
|
|
1077
|
+
"*.ts",
|
|
1078
|
+
"*.tsx",
|
|
1079
|
+
"*.js",
|
|
1080
|
+
"*.jsx"
|
|
1081
|
+
];
|
|
1082
|
+
todoPatterns = [
|
|
1083
|
+
/\/\/\s*TODO\b/gi,
|
|
1084
|
+
/\/\/\s*FIXME\b/gi,
|
|
1085
|
+
/\/\/\s*XXX\b/gi,
|
|
1086
|
+
/\/\/\s*HACK\b/gi,
|
|
1087
|
+
/\/\*\s*TODO\b/gi,
|
|
1088
|
+
/\/\*\s*FIXME\b/gi
|
|
1089
|
+
];
|
|
1090
|
+
placeholderPatterns = [
|
|
1091
|
+
/throw\s+new\s+Error\s*\(\s*['"`].*not\s*implemented.*['"`]\s*\)/gi,
|
|
1092
|
+
/throw\s+new\s+Error\s*\(\s*['"`]TODO.*['"`]\s*\)/gi,
|
|
1093
|
+
/NotImplementedError/gi,
|
|
1094
|
+
/throw\s+new\s+Error\s*\(\s*['"`]STUB['"`]\s*\)/gi
|
|
1095
|
+
];
|
|
1096
|
+
parserOptions = {
|
|
1097
|
+
sourceType: "module",
|
|
1098
|
+
plugins: [
|
|
1099
|
+
"typescript",
|
|
1100
|
+
"jsx"
|
|
1101
|
+
],
|
|
1102
|
+
errorRecovery: true
|
|
1103
|
+
};
|
|
1104
|
+
async analyze(context) {
|
|
1105
|
+
const startTime = performance.now();
|
|
1106
|
+
const issues = [];
|
|
1107
|
+
let filesAnalyzed = 0;
|
|
1108
|
+
let nodesVisited = 0;
|
|
1109
|
+
const parseErrors = [];
|
|
1110
|
+
for (const [file, content] of context.contents) {
|
|
1111
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
filesAnalyzed++;
|
|
1115
|
+
this.checkTodoComments(content, file, issues);
|
|
1116
|
+
this.checkPlaceholderPatterns(content, file, issues);
|
|
1117
|
+
try {
|
|
1118
|
+
const ast = parse(content, {
|
|
1119
|
+
...this.parserOptions,
|
|
1120
|
+
plugins: this.getPluginsForFile(file)
|
|
1121
|
+
});
|
|
1122
|
+
const result = this.analyzeAST(ast, content, file);
|
|
1123
|
+
issues.push(...result.issues);
|
|
1124
|
+
nodesVisited += result.nodesVisited;
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
parseErrors.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
analyzer: this.id,
|
|
1131
|
+
success: true,
|
|
1132
|
+
issues,
|
|
1133
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
1134
|
+
duration: performance.now() - startTime,
|
|
1135
|
+
metadata: {
|
|
1136
|
+
filesAnalyzed,
|
|
1137
|
+
nodesVisited,
|
|
1138
|
+
patternsChecked: [
|
|
1139
|
+
"TODO",
|
|
1140
|
+
"FIXME",
|
|
1141
|
+
"EMPTY_CATCH",
|
|
1142
|
+
"EMPTY_FUNCTION",
|
|
1143
|
+
"NOT_IMPLEMENTED",
|
|
1144
|
+
"PLACEHOLDER"
|
|
1145
|
+
],
|
|
1146
|
+
parseErrors
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
shouldRun(context) {
|
|
1151
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
1152
|
+
}
|
|
1153
|
+
shouldAnalyzeFile(file) {
|
|
1154
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
1155
|
+
return [
|
|
1156
|
+
"ts",
|
|
1157
|
+
"tsx",
|
|
1158
|
+
"js",
|
|
1159
|
+
"jsx"
|
|
1160
|
+
].includes(ext || "");
|
|
1161
|
+
}
|
|
1162
|
+
getPluginsForFile(file) {
|
|
1163
|
+
const plugins = [
|
|
1164
|
+
"typescript"
|
|
1165
|
+
];
|
|
1166
|
+
if (file.endsWith(".tsx") || file.endsWith(".jsx")) {
|
|
1167
|
+
plugins.push("jsx");
|
|
1168
|
+
}
|
|
1169
|
+
return plugins;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Check for TODO/FIXME comments
|
|
1173
|
+
*/
|
|
1174
|
+
checkTodoComments(content, file, issues) {
|
|
1175
|
+
const lines = content.split("\n");
|
|
1176
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1177
|
+
const line = lines[i];
|
|
1178
|
+
const lineNum = i + 1;
|
|
1179
|
+
for (const pattern of this.todoPatterns) {
|
|
1180
|
+
pattern.lastIndex = 0;
|
|
1181
|
+
if (pattern.test(line)) {
|
|
1182
|
+
const todoContent = line.trim().slice(0, 100);
|
|
1183
|
+
issues.push({
|
|
1184
|
+
id: `completeness/todo/${file}/${lineNum}`,
|
|
1185
|
+
severity: "medium",
|
|
1186
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
1187
|
+
message: `TODO/FIXME: ${todoContent}`,
|
|
1188
|
+
file,
|
|
1189
|
+
line: lineNum,
|
|
1190
|
+
snippet: todoContent
|
|
1191
|
+
});
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Check for placeholder/stub patterns
|
|
1199
|
+
*/
|
|
1200
|
+
checkPlaceholderPatterns(content, file, issues) {
|
|
1201
|
+
const lines = content.split("\n");
|
|
1202
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1203
|
+
const line = lines[i];
|
|
1204
|
+
const lineNum = i + 1;
|
|
1205
|
+
for (const pattern of this.placeholderPatterns) {
|
|
1206
|
+
pattern.lastIndex = 0;
|
|
1207
|
+
if (pattern.test(line)) {
|
|
1208
|
+
issues.push({
|
|
1209
|
+
id: `completeness/placeholder/${file}/${lineNum}`,
|
|
1210
|
+
severity: "high",
|
|
1211
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
1212
|
+
message: 'Placeholder implementation: "not implemented" or similar',
|
|
1213
|
+
file,
|
|
1214
|
+
line: lineNum,
|
|
1215
|
+
fix: "Implement the functionality or remove the placeholder",
|
|
1216
|
+
snippet: line.trim().slice(0, 100)
|
|
1217
|
+
});
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* AST-based detection of empty/incomplete code
|
|
1225
|
+
*/
|
|
1226
|
+
analyzeAST(ast, _content, file) {
|
|
1227
|
+
const issues = [];
|
|
1228
|
+
let nodesVisited = 0;
|
|
1229
|
+
traverse(ast, {
|
|
1230
|
+
enter() {
|
|
1231
|
+
nodesVisited++;
|
|
1232
|
+
},
|
|
1233
|
+
// Empty catch blocks
|
|
1234
|
+
CatchClause: /* @__PURE__ */ __name((path) => {
|
|
1235
|
+
const body = path.node.body;
|
|
1236
|
+
if (body.body.length === 0) {
|
|
1237
|
+
issues.push({
|
|
1238
|
+
id: `completeness/empty-catch/${file}/${path.node.loc?.start.line}`,
|
|
1239
|
+
severity: "medium",
|
|
1240
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
1241
|
+
message: "Empty catch block - errors silently swallowed",
|
|
1242
|
+
file,
|
|
1243
|
+
line: path.node.loc?.start.line,
|
|
1244
|
+
fix: "Add error handling, rethrow, or log the error"
|
|
1245
|
+
});
|
|
1246
|
+
} else if (body.body.length === 1) {
|
|
1247
|
+
const stmt = body.body[0];
|
|
1248
|
+
if (stmt.type === "EmptyStatement") {
|
|
1249
|
+
issues.push({
|
|
1250
|
+
id: `completeness/empty-catch/${file}/${path.node.loc?.start.line}`,
|
|
1251
|
+
severity: "medium",
|
|
1252
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
1253
|
+
message: "Catch block contains only empty statement",
|
|
1254
|
+
file,
|
|
1255
|
+
line: path.node.loc?.start.line,
|
|
1256
|
+
fix: "Add proper error handling"
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}, "CatchClause"),
|
|
1261
|
+
// Empty function bodies (excluding type declarations and interface methods)
|
|
1262
|
+
FunctionDeclaration: /* @__PURE__ */ __name((path) => {
|
|
1263
|
+
if (path.node.body.body.length === 0) {
|
|
1264
|
+
const funcName = path.node.id?.name || "anonymous";
|
|
1265
|
+
{
|
|
1266
|
+
issues.push({
|
|
1267
|
+
id: `completeness/empty-fn/${file}/${path.node.loc?.start.line}`,
|
|
1268
|
+
severity: "medium",
|
|
1269
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
1270
|
+
message: `Empty function body: ${funcName}()`,
|
|
1271
|
+
file,
|
|
1272
|
+
line: path.node.loc?.start.line,
|
|
1273
|
+
fix: "Implement the function or mark as abstract/stub if intentional"
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}, "FunctionDeclaration"),
|
|
1278
|
+
// Empty method bodies
|
|
1279
|
+
ClassMethod: /* @__PURE__ */ __name((path) => {
|
|
1280
|
+
if (path.node.abstract) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (path.node.kind === "get" || path.node.kind === "set") {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const body = path.node.body;
|
|
1287
|
+
if (body && body.body.length === 0) {
|
|
1288
|
+
const methodName = path.node.key.type === "Identifier" ? path.node.key.name : "anonymous";
|
|
1289
|
+
if (methodName === "constructor") {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
issues.push({
|
|
1293
|
+
id: `completeness/empty-method/${file}/${path.node.loc?.start.line}`,
|
|
1294
|
+
severity: "medium",
|
|
1295
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
1296
|
+
message: `Empty method body: ${methodName}()`,
|
|
1297
|
+
file,
|
|
1298
|
+
line: path.node.loc?.start.line,
|
|
1299
|
+
fix: "Implement the method or mark as abstract if intentional"
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}, "ClassMethod"),
|
|
1303
|
+
// Arrow functions that just throw or are empty (might be intentional)
|
|
1304
|
+
ArrowFunctionExpression: /* @__PURE__ */ __name((path) => {
|
|
1305
|
+
const body = path.node.body;
|
|
1306
|
+
if (body.type === "BlockStatement" && body.body.length === 0) {
|
|
1307
|
+
const parent = path.parent;
|
|
1308
|
+
if (parent.type === "VariableDeclarator") {
|
|
1309
|
+
const varName = parent.id.type === "Identifier" ? parent.id.name : "anonymous";
|
|
1310
|
+
issues.push({
|
|
1311
|
+
id: `completeness/empty-arrow/${file}/${path.node.loc?.start.line}`,
|
|
1312
|
+
severity: "low",
|
|
1313
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
1314
|
+
message: `Empty arrow function: ${varName}`,
|
|
1315
|
+
file,
|
|
1316
|
+
line: path.node.loc?.start.line,
|
|
1317
|
+
fix: "Implement the function or use () => {} if intentionally empty"
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}, "ArrowFunctionExpression"),
|
|
1322
|
+
// Check for console.log that might be debug code
|
|
1323
|
+
CallExpression: /* @__PURE__ */ __name((path) => {
|
|
1324
|
+
const callee = path.node.callee;
|
|
1325
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "console" && callee.property.type === "Identifier" && callee.property.name === "log") {
|
|
1326
|
+
const firstArg = path.node.arguments[0];
|
|
1327
|
+
if (firstArg && firstArg.type === "StringLiteral") {
|
|
1328
|
+
const msg = firstArg.value.toLowerCase();
|
|
1329
|
+
if (msg.includes("debug") || msg.includes("test") || msg.includes("todo") || msg.includes("remove")) {
|
|
1330
|
+
issues.push({
|
|
1331
|
+
id: `completeness/debug-log/${file}/${path.node.loc?.start.line}`,
|
|
1332
|
+
severity: "low",
|
|
1333
|
+
type: "DEBUG_CODE",
|
|
1334
|
+
message: `Debug console.log left in code: "${firstArg.value.slice(0, 50)}"`,
|
|
1335
|
+
file,
|
|
1336
|
+
line: path.node.loc?.start.line,
|
|
1337
|
+
fix: "Remove debug logging before commit"
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}, "CallExpression")
|
|
1343
|
+
});
|
|
1344
|
+
return {
|
|
1345
|
+
issues,
|
|
1346
|
+
nodesVisited
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
var EXPORT_PATTERNS = [
|
|
1351
|
+
/export\s+(const|function|class|interface|type|enum)\s+(\w+)/g,
|
|
1352
|
+
/export\s+default\s+(function|class)?\s*(\w+)?/g,
|
|
1353
|
+
/export\s+\{([^}]+)\}/g
|
|
1354
|
+
];
|
|
1355
|
+
var PERFORMANCE_PATTERNS = [
|
|
1356
|
+
{
|
|
1357
|
+
pattern: /\.forEach\s*\(/g,
|
|
1358
|
+
type: "computation",
|
|
1359
|
+
risk: "low"
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
pattern: /for\s*\(\s*let\s+\w+\s*=\s*0/g,
|
|
1363
|
+
type: "computation",
|
|
1364
|
+
risk: "low"
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
pattern: /while\s*\(/g,
|
|
1368
|
+
type: "computation",
|
|
1369
|
+
risk: "medium"
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
pattern: /async\s+function|await\s+/g,
|
|
1373
|
+
type: "io",
|
|
1374
|
+
risk: "medium"
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
pattern: /new\s+(Map|Set|Array)\s*\(/g,
|
|
1378
|
+
type: "memory",
|
|
1379
|
+
risk: "low"
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
pattern: /JSON\.(parse|stringify)/g,
|
|
1383
|
+
type: "computation",
|
|
1384
|
+
risk: "medium"
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
pattern: /readFileSync|writeFileSync/g,
|
|
1388
|
+
type: "io",
|
|
1389
|
+
risk: "high"
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
pattern: /spawn|exec\s*\(/g,
|
|
1393
|
+
type: "io",
|
|
1394
|
+
risk: "high"
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
pattern: /import\s*\(/g,
|
|
1398
|
+
type: "bundle",
|
|
1399
|
+
risk: "low"
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
pattern: /require\s*\(/g,
|
|
1403
|
+
type: "bundle",
|
|
1404
|
+
risk: "medium"
|
|
1405
|
+
}
|
|
1406
|
+
];
|
|
1407
|
+
var TEST_FILE_PATTERNS = [
|
|
1408
|
+
/\.test\.[tj]sx?$/,
|
|
1409
|
+
/\.spec\.[tj]sx?$/,
|
|
1410
|
+
/__tests__\//,
|
|
1411
|
+
/test\//,
|
|
1412
|
+
/tests\//
|
|
1413
|
+
];
|
|
1414
|
+
var ChangeImpactAnalyzer = class {
|
|
1415
|
+
static {
|
|
1416
|
+
__name(this, "ChangeImpactAnalyzer");
|
|
1417
|
+
}
|
|
1418
|
+
id = "change-impact";
|
|
1419
|
+
name = "Change Impact Analyzer";
|
|
1420
|
+
filePatterns = [
|
|
1421
|
+
"**/*.ts",
|
|
1422
|
+
"**/*.tsx",
|
|
1423
|
+
"**/*.js",
|
|
1424
|
+
"**/*.jsx"
|
|
1425
|
+
];
|
|
1426
|
+
workspaceRoot;
|
|
1427
|
+
dependencyGraph = /* @__PURE__ */ new Map();
|
|
1428
|
+
reverseDependencyGraph = /* @__PURE__ */ new Map();
|
|
1429
|
+
constructor(workspaceRoot) {
|
|
1430
|
+
this.workspaceRoot = workspaceRoot;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Check if this analyzer should run
|
|
1434
|
+
*/
|
|
1435
|
+
shouldRun(context) {
|
|
1436
|
+
return context.files.some((f) => this.filePatterns.some((p) => new RegExp(p.replace(/\*/g, ".*")).test(f)));
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Run impact analysis
|
|
1440
|
+
*/
|
|
1441
|
+
async analyze(context) {
|
|
1442
|
+
const start = Date.now();
|
|
1443
|
+
const issues = [];
|
|
1444
|
+
try {
|
|
1445
|
+
await this.buildDependencyGraph(context);
|
|
1446
|
+
for (const file of context.files) {
|
|
1447
|
+
const content = context.contents.get(file);
|
|
1448
|
+
if (!content) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const breakingChanges = this.detectBreakingChanges(content, file);
|
|
1452
|
+
for (const bc of breakingChanges) {
|
|
1453
|
+
issues.push({
|
|
1454
|
+
id: `impact/breaking/${bc.type}/${file}/${bc.symbol}`,
|
|
1455
|
+
severity: bc.severity,
|
|
1456
|
+
type: `BREAKING_${bc.type.toUpperCase()}`,
|
|
1457
|
+
message: bc.description,
|
|
1458
|
+
file,
|
|
1459
|
+
fix: bc.migration
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
const perfImpacts = this.detectPerformanceImpacts(content, file);
|
|
1463
|
+
for (const pi of perfImpacts) {
|
|
1464
|
+
if (pi.risk === "high" || pi.risk === "critical") {
|
|
1465
|
+
issues.push({
|
|
1466
|
+
id: `impact/perf/${pi.type}/${file}/${pi.component}`,
|
|
1467
|
+
severity: pi.risk === "critical" ? "critical" : "high",
|
|
1468
|
+
type: `PERF_${pi.type.toUpperCase()}`,
|
|
1469
|
+
message: pi.description,
|
|
1470
|
+
file,
|
|
1471
|
+
fix: pi.recommendation
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
const affectedTests = this.findAffectedTests(file);
|
|
1476
|
+
if (affectedTests.length > 5) {
|
|
1477
|
+
issues.push({
|
|
1478
|
+
id: `impact/tests/${file}`,
|
|
1479
|
+
severity: "medium",
|
|
1480
|
+
type: "HIGH_TEST_IMPACT",
|
|
1481
|
+
message: `Change affects ${affectedTests.length} test files - consider running full test suite`,
|
|
1482
|
+
file
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
analyzer: this.id,
|
|
1488
|
+
success: true,
|
|
1489
|
+
issues,
|
|
1490
|
+
coverage: 1,
|
|
1491
|
+
duration: Date.now() - start,
|
|
1492
|
+
metadata: {
|
|
1493
|
+
filesAnalyzed: context.files.length
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
return {
|
|
1498
|
+
analyzer: this.id,
|
|
1499
|
+
success: false,
|
|
1500
|
+
issues: [
|
|
1501
|
+
{
|
|
1502
|
+
id: "impact/error",
|
|
1503
|
+
severity: "high",
|
|
1504
|
+
type: "ANALYSIS_ERROR",
|
|
1505
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1506
|
+
}
|
|
1507
|
+
],
|
|
1508
|
+
coverage: 0,
|
|
1509
|
+
duration: Date.now() - start
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Get full impact analysis (more detailed than standard analyze)
|
|
1515
|
+
*/
|
|
1516
|
+
async getFullImpact(files, contents) {
|
|
1517
|
+
const start = Date.now();
|
|
1518
|
+
const context = {
|
|
1519
|
+
workspaceRoot: this.workspaceRoot,
|
|
1520
|
+
files,
|
|
1521
|
+
contents
|
|
1522
|
+
};
|
|
1523
|
+
await this.buildDependencyGraph(context);
|
|
1524
|
+
const affectedTests = [];
|
|
1525
|
+
const breakingChanges = [];
|
|
1526
|
+
const performanceImpacts = [];
|
|
1527
|
+
const dependentFiles = [];
|
|
1528
|
+
const recommendations = [];
|
|
1529
|
+
for (const file of files) {
|
|
1530
|
+
const content = contents.get(file) || "";
|
|
1531
|
+
const tests = this.findAffectedTests(file);
|
|
1532
|
+
affectedTests.push(...tests);
|
|
1533
|
+
const breaks = this.detectBreakingChanges(content, file);
|
|
1534
|
+
breakingChanges.push(...breaks);
|
|
1535
|
+
const perfs = this.detectPerformanceImpacts(content, file);
|
|
1536
|
+
performanceImpacts.push(...perfs);
|
|
1537
|
+
const deps = this.findDependentFiles(file);
|
|
1538
|
+
dependentFiles.push(...deps);
|
|
1539
|
+
}
|
|
1540
|
+
const impactScore = this.calculateImpactScore(affectedTests, breakingChanges, performanceImpacts, dependentFiles);
|
|
1541
|
+
if (breakingChanges.length > 0) {
|
|
1542
|
+
recommendations.push(`\u26A0\uFE0F ${breakingChanges.length} breaking change(s) detected - update dependent code`);
|
|
1543
|
+
}
|
|
1544
|
+
if (affectedTests.length > 10) {
|
|
1545
|
+
recommendations.push(`\u{1F9EA} Run full test suite - ${affectedTests.length} tests potentially affected`);
|
|
1546
|
+
}
|
|
1547
|
+
if (performanceImpacts.some((p) => p.risk === "high" || p.risk === "critical")) {
|
|
1548
|
+
recommendations.push("\u26A1 Performance-sensitive code modified - run benchmarks");
|
|
1549
|
+
}
|
|
1550
|
+
if (dependentFiles.length > 20) {
|
|
1551
|
+
recommendations.push("\u{1F517} High ripple effect - consider incremental rollout");
|
|
1552
|
+
}
|
|
1553
|
+
return {
|
|
1554
|
+
filesAnalyzed: files.length,
|
|
1555
|
+
affectedTests: this.dedupeItems(affectedTests),
|
|
1556
|
+
breakingChanges,
|
|
1557
|
+
performanceImpacts,
|
|
1558
|
+
dependentFiles: this.dedupeItems(dependentFiles),
|
|
1559
|
+
impactScore,
|
|
1560
|
+
recommendations,
|
|
1561
|
+
duration: Date.now() - start
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
// =========================================================================
|
|
1565
|
+
// Private Methods
|
|
1566
|
+
// =========================================================================
|
|
1567
|
+
/**
|
|
1568
|
+
* Build dependency graph from file contents
|
|
1569
|
+
*/
|
|
1570
|
+
async buildDependencyGraph(context) {
|
|
1571
|
+
this.dependencyGraph.clear();
|
|
1572
|
+
this.reverseDependencyGraph.clear();
|
|
1573
|
+
for (const file of context.files) {
|
|
1574
|
+
const content = context.contents.get(file);
|
|
1575
|
+
if (!content) {
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
const imports = this.extractImports(content, file);
|
|
1579
|
+
this.dependencyGraph.set(file, imports);
|
|
1580
|
+
for (const imp of imports) {
|
|
1581
|
+
const existing = this.reverseDependencyGraph.get(imp) || [];
|
|
1582
|
+
existing.push(file);
|
|
1583
|
+
this.reverseDependencyGraph.set(imp, existing);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Extract import statements from file content using AST analysis.
|
|
1589
|
+
*
|
|
1590
|
+
* UPGRADED (11b): Replaces regex-based import extraction with proper AST walking
|
|
1591
|
+
* via packages/core/src/analysis/ast/import-extractor.ts (oxc-parser based).
|
|
1592
|
+
* This eliminates false positives from imports in strings/comments and correctly
|
|
1593
|
+
* handles dynamic imports, re-exports, and type-only imports.
|
|
1594
|
+
*/
|
|
1595
|
+
extractImports(content, fromFile) {
|
|
1596
|
+
const rawSources = extractImportSources(content, fromFile);
|
|
1597
|
+
const imports = [];
|
|
1598
|
+
for (const source of rawSources) {
|
|
1599
|
+
const importPath = this.resolveImportPath(source, fromFile);
|
|
1600
|
+
if (importPath) {
|
|
1601
|
+
imports.push(importPath);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return imports;
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Resolve import path to absolute file path
|
|
1608
|
+
*/
|
|
1609
|
+
resolveImportPath(importPath, fromFile) {
|
|
1610
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
const dir = dirname(fromFile);
|
|
1614
|
+
const extensions = [
|
|
1615
|
+
".ts",
|
|
1616
|
+
".tsx",
|
|
1617
|
+
".js",
|
|
1618
|
+
".jsx",
|
|
1619
|
+
"/index.ts",
|
|
1620
|
+
"/index.tsx",
|
|
1621
|
+
"/index.js"
|
|
1622
|
+
];
|
|
1623
|
+
for (const ext of extensions) {
|
|
1624
|
+
const resolved = `${dir}/${importPath}${ext}`.replace(/\/\.\//g, "/");
|
|
1625
|
+
return resolved;
|
|
1626
|
+
}
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Find test files that might be affected by a change
|
|
1631
|
+
*/
|
|
1632
|
+
findAffectedTests(file) {
|
|
1633
|
+
const tests = [];
|
|
1634
|
+
const relPath = relative(this.workspaceRoot, file);
|
|
1635
|
+
const fileName = basename(file).replace(/\.[tj]sx?$/, "");
|
|
1636
|
+
const directTestPatterns = [
|
|
1637
|
+
`${fileName}.test.ts`,
|
|
1638
|
+
`${fileName}.test.tsx`,
|
|
1639
|
+
`${fileName}.spec.ts`,
|
|
1640
|
+
`${fileName}.spec.tsx`,
|
|
1641
|
+
`__tests__/${fileName}.test.ts`,
|
|
1642
|
+
`__tests__/${fileName}.test.tsx`
|
|
1643
|
+
];
|
|
1644
|
+
for (const pattern of directTestPatterns) {
|
|
1645
|
+
tests.push({
|
|
1646
|
+
path: pattern,
|
|
1647
|
+
reason: "Direct test file for changed source",
|
|
1648
|
+
level: "high"
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
const importers = this.reverseDependencyGraph.get(file) || [];
|
|
1652
|
+
for (const importer of importers) {
|
|
1653
|
+
if (this.isTestFile(importer)) {
|
|
1654
|
+
tests.push({
|
|
1655
|
+
path: relative(this.workspaceRoot, importer),
|
|
1656
|
+
reason: "Test file imports changed module",
|
|
1657
|
+
level: "medium"
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (relPath.includes("/core/") || relPath.includes("/services/")) {
|
|
1662
|
+
tests.push({
|
|
1663
|
+
path: "**/*.integration.test.ts",
|
|
1664
|
+
reason: "Core module change may affect integration tests",
|
|
1665
|
+
level: "low"
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
return tests;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Check if a file is a test file
|
|
1672
|
+
*/
|
|
1673
|
+
isTestFile(file) {
|
|
1674
|
+
return TEST_FILE_PATTERNS.some((p) => p.test(file));
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Detect breaking changes in content
|
|
1678
|
+
*/
|
|
1679
|
+
detectBreakingChanges(content, file) {
|
|
1680
|
+
const breaks = [];
|
|
1681
|
+
for (const pattern of EXPORT_PATTERNS) {
|
|
1682
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1683
|
+
let match2;
|
|
1684
|
+
while ((match2 = regex.exec(content)) !== null) {
|
|
1685
|
+
const symbolName = match2[2] || match2[1];
|
|
1686
|
+
if (symbolName) {
|
|
1687
|
+
breaks.push({
|
|
1688
|
+
type: "export",
|
|
1689
|
+
symbol: symbolName,
|
|
1690
|
+
file,
|
|
1691
|
+
description: `Exported symbol '${symbolName}' may have changed`,
|
|
1692
|
+
severity: "medium",
|
|
1693
|
+
migration: `Verify consumers of '${symbolName}' are updated`
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1699
|
+
let match;
|
|
1700
|
+
while ((match = interfaceRegex.exec(content)) !== null) {
|
|
1701
|
+
const interfaceName = match[1];
|
|
1702
|
+
const body = match[2];
|
|
1703
|
+
if (body.includes("?:") || body.includes(": ")) {
|
|
1704
|
+
breaks.push({
|
|
1705
|
+
type: "type",
|
|
1706
|
+
symbol: interfaceName,
|
|
1707
|
+
file,
|
|
1708
|
+
description: `Interface '${interfaceName}' definition changed`,
|
|
1709
|
+
severity: "medium"
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return breaks;
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Detect performance-sensitive code changes
|
|
1717
|
+
*/
|
|
1718
|
+
detectPerformanceImpacts(content, file) {
|
|
1719
|
+
const impacts = [];
|
|
1720
|
+
for (const { pattern, type, risk } of PERFORMANCE_PATTERNS) {
|
|
1721
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1722
|
+
let match;
|
|
1723
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1724
|
+
impacts.push({
|
|
1725
|
+
type,
|
|
1726
|
+
description: `${type} operation detected: ${match[0]}`,
|
|
1727
|
+
risk,
|
|
1728
|
+
component: basename(file),
|
|
1729
|
+
recommendation: this.getPerformanceRecommendation(type)
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return impacts;
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Get recommendation for performance issue type
|
|
1737
|
+
*/
|
|
1738
|
+
getPerformanceRecommendation(type) {
|
|
1739
|
+
switch (type) {
|
|
1740
|
+
case "hotpath":
|
|
1741
|
+
return "Consider memoization or caching for hot paths";
|
|
1742
|
+
case "memory":
|
|
1743
|
+
return "Monitor memory usage, consider object pooling";
|
|
1744
|
+
case "io":
|
|
1745
|
+
return "Use async operations, consider batching";
|
|
1746
|
+
case "computation":
|
|
1747
|
+
return "Profile for bottlenecks, consider Web Workers";
|
|
1748
|
+
case "bundle":
|
|
1749
|
+
return "Use dynamic imports for code splitting";
|
|
1750
|
+
default:
|
|
1751
|
+
return "Profile before optimizing";
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Find files that depend on changed file
|
|
1756
|
+
*/
|
|
1757
|
+
findDependentFiles(file) {
|
|
1758
|
+
const dependents = [];
|
|
1759
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1760
|
+
const traverse3 = /* @__PURE__ */ __name((current, depth) => {
|
|
1761
|
+
if (visited.has(current) || depth > 3) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
visited.add(current);
|
|
1765
|
+
const importers = this.reverseDependencyGraph.get(current) || [];
|
|
1766
|
+
for (const importer of importers) {
|
|
1767
|
+
dependents.push({
|
|
1768
|
+
path: relative(this.workspaceRoot, importer),
|
|
1769
|
+
reason: depth === 0 ? "Directly imports changed file" : `Transitive dependency (depth ${depth})`,
|
|
1770
|
+
level: depth === 0 ? "high" : depth === 1 ? "medium" : "low"
|
|
1771
|
+
});
|
|
1772
|
+
traverse3(importer, depth + 1);
|
|
1773
|
+
}
|
|
1774
|
+
}, "traverse");
|
|
1775
|
+
traverse3(file, 0);
|
|
1776
|
+
return dependents;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Calculate overall impact score
|
|
1780
|
+
*/
|
|
1781
|
+
calculateImpactScore(tests, breaks, perfs, deps) {
|
|
1782
|
+
let score = 0;
|
|
1783
|
+
score += Math.min(tests.length * 0.05, 0.25);
|
|
1784
|
+
score += Math.min(breaks.length * 0.15, 0.35);
|
|
1785
|
+
score += Math.min(perfs.filter((p) => p.risk === "high").length * 0.1, 0.2);
|
|
1786
|
+
score += Math.min(deps.length * 0.02, 0.2);
|
|
1787
|
+
return Math.min(score, 1);
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Deduplicate impact items
|
|
1791
|
+
*/
|
|
1792
|
+
dedupeItems(items) {
|
|
1793
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1794
|
+
return items.filter((item) => {
|
|
1795
|
+
if (seen.has(item.path)) {
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
seen.add(item.path);
|
|
1799
|
+
return true;
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
function createChangeImpactAnalyzer(workspaceRoot) {
|
|
1804
|
+
return new ChangeImpactAnalyzer(workspaceRoot);
|
|
1805
|
+
}
|
|
1806
|
+
__name(createChangeImpactAnalyzer, "createChangeImpactAnalyzer");
|
|
1807
|
+
var SecurityAnalyzer = class {
|
|
1808
|
+
static {
|
|
1809
|
+
__name(this, "SecurityAnalyzer");
|
|
1810
|
+
}
|
|
1811
|
+
id = "security";
|
|
1812
|
+
name = "Security Analysis";
|
|
1813
|
+
filePatterns = [
|
|
1814
|
+
"*.ts",
|
|
1815
|
+
"*.tsx",
|
|
1816
|
+
"*.js",
|
|
1817
|
+
"*.jsx"
|
|
1818
|
+
];
|
|
1819
|
+
parserOptions = {
|
|
1820
|
+
sourceType: "module",
|
|
1821
|
+
plugins: [
|
|
1822
|
+
"typescript",
|
|
1823
|
+
"jsx"
|
|
1824
|
+
],
|
|
1825
|
+
errorRecovery: true
|
|
1826
|
+
};
|
|
1827
|
+
async analyze(context) {
|
|
1828
|
+
const startTime = performance.now();
|
|
1829
|
+
const issues = [];
|
|
1830
|
+
let filesAnalyzed = 0;
|
|
1831
|
+
let nodesVisited = 0;
|
|
1832
|
+
const parseErrors = [];
|
|
1833
|
+
for (const [file, content] of context.contents) {
|
|
1834
|
+
if (!this.shouldAnalyzeFile(file)) {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
filesAnalyzed++;
|
|
1838
|
+
try {
|
|
1839
|
+
const ast = parse(content, {
|
|
1840
|
+
...this.parserOptions,
|
|
1841
|
+
plugins: this.getPluginsForFile(file)
|
|
1842
|
+
});
|
|
1843
|
+
const fileIssues = this.analyzeAST(ast, content, file);
|
|
1844
|
+
issues.push(...fileIssues.issues);
|
|
1845
|
+
nodesVisited += fileIssues.nodesVisited;
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
parseErrors.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1848
|
+
issues.push({
|
|
1849
|
+
id: `security/parse-error/${file}`,
|
|
1850
|
+
severity: "info",
|
|
1851
|
+
type: "PARSE_ERROR",
|
|
1852
|
+
message: `Could not parse for security analysis: ${error instanceof Error ? error.message : String(error)}`,
|
|
1853
|
+
file
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return {
|
|
1858
|
+
analyzer: this.id,
|
|
1859
|
+
success: true,
|
|
1860
|
+
issues,
|
|
1861
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
1862
|
+
duration: performance.now() - startTime,
|
|
1863
|
+
metadata: {
|
|
1864
|
+
filesAnalyzed,
|
|
1865
|
+
nodesVisited,
|
|
1866
|
+
patternsChecked: [
|
|
1867
|
+
"UNSAFE_EVAL",
|
|
1868
|
+
"PATH_TRAVERSAL",
|
|
1869
|
+
"MISSING_SIGNAL_HANDLER",
|
|
1870
|
+
"COMMAND_INJECTION",
|
|
1871
|
+
"SQL_INJECTION",
|
|
1872
|
+
"XSS_RISK",
|
|
1873
|
+
"HARDCODED_SECRET",
|
|
1874
|
+
"UNSAFE_REGEX"
|
|
1875
|
+
],
|
|
1876
|
+
parseErrors
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
shouldRun(context) {
|
|
1881
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
1882
|
+
}
|
|
1883
|
+
shouldAnalyzeFile(file) {
|
|
1884
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
1885
|
+
return [
|
|
1886
|
+
"ts",
|
|
1887
|
+
"tsx",
|
|
1888
|
+
"js",
|
|
1889
|
+
"jsx"
|
|
1890
|
+
].includes(ext || "");
|
|
1891
|
+
}
|
|
1892
|
+
getPluginsForFile(file) {
|
|
1893
|
+
const plugins = [
|
|
1894
|
+
"typescript"
|
|
1895
|
+
];
|
|
1896
|
+
if (file.endsWith(".tsx") || file.endsWith(".jsx")) {
|
|
1897
|
+
plugins.push("jsx");
|
|
1898
|
+
}
|
|
1899
|
+
return plugins;
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Analyze AST for security issues
|
|
1903
|
+
*/
|
|
1904
|
+
analyzeAST(ast, content, file) {
|
|
1905
|
+
const issues = [];
|
|
1906
|
+
let nodesVisited = 0;
|
|
1907
|
+
const fileContext = {
|
|
1908
|
+
isDaemon: false,
|
|
1909
|
+
hasSignalHandler: false};
|
|
1910
|
+
fileContext.isDaemon = content.includes(".listen(") || file.includes("daemon") || file.includes("server") || file.includes("worker");
|
|
1911
|
+
traverse(ast, {
|
|
1912
|
+
enter() {
|
|
1913
|
+
nodesVisited++;
|
|
1914
|
+
},
|
|
1915
|
+
// Detect eval()
|
|
1916
|
+
CallExpression: /* @__PURE__ */ __name((path) => {
|
|
1917
|
+
const callee = path.node.callee;
|
|
1918
|
+
if (callee.type === "Identifier" && callee.name === "eval") {
|
|
1919
|
+
issues.push({
|
|
1920
|
+
id: `security/eval/${file}/${path.node.loc?.start.line}`,
|
|
1921
|
+
severity: "critical",
|
|
1922
|
+
type: "UNSAFE_EVAL",
|
|
1923
|
+
message: "eval() allows arbitrary code execution",
|
|
1924
|
+
file,
|
|
1925
|
+
line: path.node.loc?.start.line,
|
|
1926
|
+
column: path.node.loc?.start.column,
|
|
1927
|
+
fix: "Use JSON.parse() for data or refactor logic to avoid eval"
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
if (callee.type === "Identifier" && callee.name === "Function") {
|
|
1931
|
+
issues.push({
|
|
1932
|
+
id: `security/function-constructor/${file}/${path.node.loc?.start.line}`,
|
|
1933
|
+
severity: "critical",
|
|
1934
|
+
type: "UNSAFE_EVAL",
|
|
1935
|
+
message: "new Function() is equivalent to eval() and allows arbitrary code execution",
|
|
1936
|
+
file,
|
|
1937
|
+
line: path.node.loc?.start.line,
|
|
1938
|
+
column: path.node.loc?.start.column,
|
|
1939
|
+
fix: "Refactor to avoid dynamic code generation"
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
if (callee.type === "Identifier" && (callee.name === "setTimeout" || callee.name === "setInterval")) {
|
|
1943
|
+
const firstArg = path.node.arguments[0];
|
|
1944
|
+
if (firstArg && firstArg.type === "StringLiteral") {
|
|
1945
|
+
issues.push({
|
|
1946
|
+
id: `security/string-timer/${file}/${path.node.loc?.start.line}`,
|
|
1947
|
+
severity: "high",
|
|
1948
|
+
type: "UNSAFE_EVAL",
|
|
1949
|
+
message: `${callee.name} with string argument executes code like eval()`,
|
|
1950
|
+
file,
|
|
1951
|
+
line: path.node.loc?.start.line,
|
|
1952
|
+
fix: "Pass a function instead of a string"
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
if (callee.type === "Identifier" && (callee.name === "exec" || callee.name === "execSync")) {
|
|
1957
|
+
const firstArg = path.node.arguments[0];
|
|
1958
|
+
if (firstArg && !this.isStaticString(firstArg)) {
|
|
1959
|
+
issues.push({
|
|
1960
|
+
id: `security/command-injection/${file}/${path.node.loc?.start.line}`,
|
|
1961
|
+
severity: "high",
|
|
1962
|
+
type: "COMMAND_INJECTION",
|
|
1963
|
+
message: "exec with dynamic command - potential command injection",
|
|
1964
|
+
file,
|
|
1965
|
+
line: path.node.loc?.start.line,
|
|
1966
|
+
fix: "Validate/sanitize input or use execFile with explicit arguments"
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "process" && callee.property.type === "Identifier" && callee.property.name === "on") {
|
|
1971
|
+
const firstArg = path.node.arguments[0];
|
|
1972
|
+
if (firstArg && firstArg.type === "StringLiteral") {
|
|
1973
|
+
if (firstArg.value === "SIGTERM" || firstArg.value === "SIGINT") {
|
|
1974
|
+
fileContext.hasSignalHandler = true;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}, "CallExpression"),
|
|
1979
|
+
// Detect fs operations with dynamic paths
|
|
1980
|
+
MemberExpression: /* @__PURE__ */ __name((path) => {
|
|
1981
|
+
const node = path.node;
|
|
1982
|
+
if (node.object.type === "Identifier" && (node.object.name === "fs" || node.object.name === "fsp")) {
|
|
1983
|
+
const parent = path.parentPath;
|
|
1984
|
+
if (parent.isCallExpression()) {
|
|
1985
|
+
const methodName = node.property.type === "Identifier" ? node.property.name : node.property.value;
|
|
1986
|
+
const pathMethods = [
|
|
1987
|
+
"readFile",
|
|
1988
|
+
"readFileSync",
|
|
1989
|
+
"writeFile",
|
|
1990
|
+
"writeFileSync",
|
|
1991
|
+
"readdir",
|
|
1992
|
+
"readdirSync",
|
|
1993
|
+
"stat",
|
|
1994
|
+
"statSync",
|
|
1995
|
+
"unlink",
|
|
1996
|
+
"unlinkSync",
|
|
1997
|
+
"mkdir",
|
|
1998
|
+
"mkdirSync",
|
|
1999
|
+
"rmdir",
|
|
2000
|
+
"rmdirSync",
|
|
2001
|
+
"access",
|
|
2002
|
+
"accessSync"
|
|
2003
|
+
];
|
|
2004
|
+
if (pathMethods.includes(methodName)) {
|
|
2005
|
+
const firstArg = parent.node.arguments[0];
|
|
2006
|
+
if (firstArg && !this.isStaticPath(firstArg)) {
|
|
2007
|
+
issues.push({
|
|
2008
|
+
id: `security/path-traversal/${file}/${path.node.loc?.start.line}`,
|
|
2009
|
+
severity: "high",
|
|
2010
|
+
type: "PATH_TRAVERSAL",
|
|
2011
|
+
message: `fs.${methodName} with dynamic path - potential path traversal`,
|
|
2012
|
+
file,
|
|
2013
|
+
line: path.node.loc?.start.line,
|
|
2014
|
+
fix: "Validate paths against workspace root before use"
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}, "MemberExpression"),
|
|
2021
|
+
// Check for dangerous regex patterns
|
|
2022
|
+
NewExpression: /* @__PURE__ */ __name((path) => {
|
|
2023
|
+
if (path.node.callee.type === "Identifier" && path.node.callee.name === "RegExp") {
|
|
2024
|
+
const firstArg = path.node.arguments[0];
|
|
2025
|
+
if (firstArg && !this.isStaticString(firstArg)) {
|
|
2026
|
+
issues.push({
|
|
2027
|
+
id: `security/unsafe-regex/${file}/${path.node.loc?.start.line}`,
|
|
2028
|
+
severity: "medium",
|
|
2029
|
+
type: "UNSAFE_REGEX",
|
|
2030
|
+
message: "Dynamic RegExp - potential ReDoS or injection vulnerability",
|
|
2031
|
+
file,
|
|
2032
|
+
line: path.node.loc?.start.line,
|
|
2033
|
+
fix: "Use static regex patterns or validate input"
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}, "NewExpression"),
|
|
2038
|
+
// Check for innerHTML/dangerouslySetInnerHTML (XSS)
|
|
2039
|
+
JSXAttribute: /* @__PURE__ */ __name((path) => {
|
|
2040
|
+
const name = path.node.name;
|
|
2041
|
+
if (name.type === "JSXIdentifier" && name.name === "dangerouslySetInnerHTML") {
|
|
2042
|
+
issues.push({
|
|
2043
|
+
id: `security/xss-risk/${file}/${path.node.loc?.start.line}`,
|
|
2044
|
+
severity: "high",
|
|
2045
|
+
type: "XSS_RISK",
|
|
2046
|
+
message: "dangerouslySetInnerHTML can lead to XSS if content is not sanitized",
|
|
2047
|
+
file,
|
|
2048
|
+
line: path.node.loc?.start.line,
|
|
2049
|
+
fix: "Sanitize HTML content before rendering or avoid using dangerouslySetInnerHTML"
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
}, "JSXAttribute"),
|
|
2053
|
+
// Check for hardcoded secrets in variable declarations
|
|
2054
|
+
VariableDeclarator: /* @__PURE__ */ __name((path) => {
|
|
2055
|
+
const id = path.node.id;
|
|
2056
|
+
const init = path.node.init;
|
|
2057
|
+
if (id.type === "Identifier" && init) {
|
|
2058
|
+
this.checkForHardcodedSecret(id.name, init, file, path.node.loc?.start.line, issues);
|
|
2059
|
+
}
|
|
2060
|
+
}, "VariableDeclarator"),
|
|
2061
|
+
// Check for hardcoded secrets in class properties
|
|
2062
|
+
ClassProperty: /* @__PURE__ */ __name((path) => {
|
|
2063
|
+
const key = path.node.key;
|
|
2064
|
+
const value = path.node.value;
|
|
2065
|
+
if (key.type === "Identifier" && value) {
|
|
2066
|
+
this.checkForHardcodedSecret(key.name, value, file, path.node.loc?.start.line, issues);
|
|
2067
|
+
}
|
|
2068
|
+
}, "ClassProperty"),
|
|
2069
|
+
// After traversal is complete, check daemon-specific patterns
|
|
2070
|
+
Program: {
|
|
2071
|
+
exit: /* @__PURE__ */ __name(() => {
|
|
2072
|
+
if (fileContext.isDaemon && !fileContext.hasSignalHandler) {
|
|
2073
|
+
issues.push({
|
|
2074
|
+
id: `security/signal-handler/${file}`,
|
|
2075
|
+
severity: "high",
|
|
2076
|
+
type: "MISSING_SIGNAL_HANDLER",
|
|
2077
|
+
message: "Daemon/server missing signal handlers (SIGTERM/SIGINT)",
|
|
2078
|
+
file,
|
|
2079
|
+
fix: "Add process.on('SIGTERM', gracefulShutdown) for clean shutdown"
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
}, "exit")
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
return {
|
|
2086
|
+
issues,
|
|
2087
|
+
nodesVisited
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Check if expression is a static string (safe)
|
|
2092
|
+
*/
|
|
2093
|
+
isStaticString(node) {
|
|
2094
|
+
if (node.type === "StringLiteral") {
|
|
2095
|
+
return true;
|
|
2096
|
+
}
|
|
2097
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Check if expression is a static path (safe)
|
|
2104
|
+
*/
|
|
2105
|
+
isStaticPath(node) {
|
|
2106
|
+
if (node.type === "StringLiteral") {
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
if (node.type === "CallExpression") {
|
|
2113
|
+
const callee = node.callee;
|
|
2114
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && callee.property.name === "join") {
|
|
2115
|
+
return node.arguments.every((arg) => {
|
|
2116
|
+
if (arg.type === "StringLiteral") {
|
|
2117
|
+
return true;
|
|
2118
|
+
}
|
|
2119
|
+
if (arg.type === "Identifier" && (arg.name === "__dirname" || arg.name === "__filename")) {
|
|
2120
|
+
return true;
|
|
2121
|
+
}
|
|
2122
|
+
return false;
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
return false;
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Check if a value looks like a hardcoded secret
|
|
2130
|
+
*/
|
|
2131
|
+
checkForHardcodedSecret(name, value, file, line, issues) {
|
|
2132
|
+
if (!value) {
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
const varName = name.toLowerCase();
|
|
2136
|
+
const secretIndicators = [
|
|
2137
|
+
"apikey",
|
|
2138
|
+
"api_key",
|
|
2139
|
+
"secret",
|
|
2140
|
+
"password",
|
|
2141
|
+
"token",
|
|
2142
|
+
"credential",
|
|
2143
|
+
"auth",
|
|
2144
|
+
"key"
|
|
2145
|
+
];
|
|
2146
|
+
if (secretIndicators.some((s) => varName.includes(s))) {
|
|
2147
|
+
if (value.type === "StringLiteral" && value.value.length > 8) {
|
|
2148
|
+
const valueStr = value.value.toLowerCase();
|
|
2149
|
+
if (!valueStr.includes("placeholder") && !valueStr.includes("example") && !valueStr.includes("xxx") && !valueStr.includes("todo") && !valueStr.includes("your_") && !valueStr.includes("env.")) {
|
|
2150
|
+
issues.push({
|
|
2151
|
+
id: `security/hardcoded-secret/${file}/${line}`,
|
|
2152
|
+
severity: "critical",
|
|
2153
|
+
type: "HARDCODED_SECRET",
|
|
2154
|
+
message: `Possible hardcoded secret in "${name}"`,
|
|
2155
|
+
file,
|
|
2156
|
+
line,
|
|
2157
|
+
fix: "Use environment variables for secrets"
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
|
|
2165
|
+
// ../../node_modules/@snapback/core/dist/analysis/pipeline.js
|
|
2166
|
+
var ANALYZER_COVERAGE_MAP = {
|
|
2167
|
+
syntax: "astParsed",
|
|
2168
|
+
security: "securityChecked",
|
|
2169
|
+
completeness: "completenessChecked",
|
|
2170
|
+
"change-impact": "architectureChecked",
|
|
2171
|
+
"import-graph": "importGraphChecked",
|
|
2172
|
+
complexity: "complexityChecked"
|
|
2173
|
+
};
|
|
2174
|
+
var CONFIDENCE_WEIGHTS = {
|
|
2175
|
+
syntax: 0.2,
|
|
2176
|
+
security: 0.25,
|
|
2177
|
+
completeness: 0.15,
|
|
2178
|
+
"change-impact": 0.1,
|
|
2179
|
+
"import-graph": 0.15,
|
|
2180
|
+
complexity: 0.15
|
|
2181
|
+
};
|
|
2182
|
+
async function runAnalysisPipeline(context, config) {
|
|
2183
|
+
const start = Date.now();
|
|
2184
|
+
const parallel = config?.parallel ?? true;
|
|
2185
|
+
const timeout = config?.timeout ?? 3e4;
|
|
2186
|
+
const allAnalyzers = createAnalyzers(context.workspaceRoot);
|
|
2187
|
+
const selectedAnalyzers = filterAnalyzers(allAnalyzers, context, config?.analyzers);
|
|
2188
|
+
const results = parallel ? await runParallel(selectedAnalyzers, context, timeout) : await runSequential(selectedAnalyzers, context, timeout);
|
|
2189
|
+
const coverage = buildCoverageInfo(results, context);
|
|
2190
|
+
const confidence = calculateConfidence(results, coverage);
|
|
2191
|
+
const allIssues = results.flatMap((r) => r.issues);
|
|
2192
|
+
const issuesBySeverity = groupBySeverity(allIssues);
|
|
2193
|
+
return {
|
|
2194
|
+
results,
|
|
2195
|
+
totalIssues: allIssues.length,
|
|
2196
|
+
issuesBySeverity,
|
|
2197
|
+
coverage,
|
|
2198
|
+
confidence,
|
|
2199
|
+
duration: Date.now() - start
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
__name(runAnalysisPipeline, "runAnalysisPipeline");
|
|
2203
|
+
function createAnalyzers(workspaceRoot) {
|
|
2204
|
+
return [
|
|
2205
|
+
new SyntaxAnalyzer(),
|
|
2206
|
+
new SecurityAnalyzer(),
|
|
2207
|
+
new CompletenessAnalyzer(),
|
|
2208
|
+
new ComplexityAnalyzer(),
|
|
2209
|
+
new ImportGraphAnalyzer(),
|
|
2210
|
+
new ChangeImpactAnalyzer(workspaceRoot)
|
|
2211
|
+
];
|
|
2212
|
+
}
|
|
2213
|
+
__name(createAnalyzers, "createAnalyzers");
|
|
2214
|
+
function filterAnalyzers(analyzers, context, selectedIds) {
|
|
2215
|
+
let filtered = analyzers;
|
|
2216
|
+
if (selectedIds && selectedIds.length > 0) {
|
|
2217
|
+
const idSet = new Set(selectedIds);
|
|
2218
|
+
filtered = filtered.filter((a) => idSet.has(a.id));
|
|
2219
|
+
}
|
|
2220
|
+
return filtered.filter((a) => a.shouldRun(context));
|
|
2221
|
+
}
|
|
2222
|
+
__name(filterAnalyzers, "filterAnalyzers");
|
|
2223
|
+
async function runParallel(analyzers, context, timeout) {
|
|
2224
|
+
const promises = analyzers.map((analyzer) => runWithTimeout(analyzer, context, timeout));
|
|
2225
|
+
return Promise.all(promises);
|
|
2226
|
+
}
|
|
2227
|
+
__name(runParallel, "runParallel");
|
|
2228
|
+
async function runSequential(analyzers, context, timeout) {
|
|
2229
|
+
const results = [];
|
|
2230
|
+
for (const analyzer of analyzers) {
|
|
2231
|
+
const result = await runWithTimeout(analyzer, context, timeout);
|
|
2232
|
+
results.push(result);
|
|
2233
|
+
}
|
|
2234
|
+
return results;
|
|
2235
|
+
}
|
|
2236
|
+
__name(runSequential, "runSequential");
|
|
2237
|
+
async function runWithTimeout(analyzer, context, timeout) {
|
|
2238
|
+
const start = Date.now();
|
|
2239
|
+
try {
|
|
2240
|
+
const result = await Promise.race([
|
|
2241
|
+
analyzer.analyze(context),
|
|
2242
|
+
new Promise((_, reject) => {
|
|
2243
|
+
setTimeout(() => reject(new Error(`Analyzer '${analyzer.id}' timed out after ${timeout}ms`)), timeout);
|
|
2244
|
+
})
|
|
2245
|
+
]);
|
|
2246
|
+
return result;
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
return {
|
|
2249
|
+
analyzer: analyzer.id,
|
|
2250
|
+
success: false,
|
|
2251
|
+
issues: [
|
|
2252
|
+
{
|
|
2253
|
+
id: `pipeline/${analyzer.id}/error`,
|
|
2254
|
+
severity: "high",
|
|
2255
|
+
type: "ANALYZER_ERROR",
|
|
2256
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2257
|
+
}
|
|
2258
|
+
],
|
|
2259
|
+
coverage: 0,
|
|
2260
|
+
duration: Date.now() - start
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
__name(runWithTimeout, "runWithTimeout");
|
|
2265
|
+
function buildCoverageInfo(results, context) {
|
|
2266
|
+
const coverage = {
|
|
2267
|
+
astParsed: false,
|
|
2268
|
+
securityChecked: false,
|
|
2269
|
+
completenessChecked: false,
|
|
2270
|
+
architectureChecked: false,
|
|
2271
|
+
importGraphChecked: false,
|
|
2272
|
+
complexityChecked: false,
|
|
2273
|
+
filesCoverage: 0
|
|
2274
|
+
};
|
|
2275
|
+
for (const result of results) {
|
|
2276
|
+
const field = ANALYZER_COVERAGE_MAP[result.analyzer];
|
|
2277
|
+
if (field && field !== "filesCoverage" && result.success) {
|
|
2278
|
+
coverage[field] = true;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
const totalFiles = context.files.length;
|
|
2282
|
+
if (totalFiles > 0) {
|
|
2283
|
+
const successfulResults = results.filter((r) => r.success);
|
|
2284
|
+
const avgCoverage = successfulResults.length > 0 ? successfulResults.reduce((sum, r) => sum + r.coverage, 0) / successfulResults.length : 0;
|
|
2285
|
+
coverage.filesCoverage = avgCoverage;
|
|
2286
|
+
}
|
|
2287
|
+
return coverage;
|
|
2288
|
+
}
|
|
2289
|
+
__name(buildCoverageInfo, "buildCoverageInfo");
|
|
2290
|
+
function calculateConfidence(results, coverage) {
|
|
2291
|
+
const breakdown = {};
|
|
2292
|
+
let weightedSum = 0;
|
|
2293
|
+
let totalWeight = 0;
|
|
2294
|
+
let maxPossible = 0;
|
|
2295
|
+
for (const [id, weight] of Object.entries(CONFIDENCE_WEIGHTS)) {
|
|
2296
|
+
const result = results.find((r) => r.analyzer === id);
|
|
2297
|
+
totalWeight += weight;
|
|
2298
|
+
if (result) {
|
|
2299
|
+
const analyzerConfidence = result.success ? result.coverage : 0;
|
|
2300
|
+
breakdown[id] = analyzerConfidence;
|
|
2301
|
+
weightedSum += weight * analyzerConfidence;
|
|
2302
|
+
maxPossible += weight;
|
|
2303
|
+
} else {
|
|
2304
|
+
breakdown[id] = 0;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
const confidence = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
2308
|
+
const maxPossibleConfidence = totalWeight > 0 ? maxPossible / totalWeight : 0;
|
|
2309
|
+
const ranCount = results.filter((r) => r.success).length;
|
|
2310
|
+
const totalAnalyzers = Object.keys(CONFIDENCE_WEIGHTS).length;
|
|
2311
|
+
const explanationParts = [
|
|
2312
|
+
`${ranCount}/${totalAnalyzers} analyzers ran successfully`,
|
|
2313
|
+
`Files coverage: ${(coverage.filesCoverage * 100).toFixed(0)}%`
|
|
2314
|
+
];
|
|
2315
|
+
const failedAnalyzers = results.filter((r) => !r.success);
|
|
2316
|
+
if (failedAnalyzers.length > 0) {
|
|
2317
|
+
explanationParts.push(`Failed: ${failedAnalyzers.map((r) => r.analyzer).join(", ")}`);
|
|
2318
|
+
}
|
|
2319
|
+
return {
|
|
2320
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
2321
|
+
breakdown,
|
|
2322
|
+
explanation: explanationParts.join(". "),
|
|
2323
|
+
maxPossibleConfidence: Math.round(maxPossibleConfidence * 100) / 100
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
__name(calculateConfidence, "calculateConfidence");
|
|
2327
|
+
function groupBySeverity(issues) {
|
|
2328
|
+
const grouped = {
|
|
2329
|
+
critical: [],
|
|
2330
|
+
high: [],
|
|
2331
|
+
medium: [],
|
|
2332
|
+
low: [],
|
|
2333
|
+
info: []
|
|
2334
|
+
};
|
|
2335
|
+
for (const issue of issues) {
|
|
2336
|
+
const severity = issue.severity || "info";
|
|
2337
|
+
if (severity in grouped) {
|
|
2338
|
+
grouped[severity].push(issue);
|
|
2339
|
+
} else {
|
|
2340
|
+
grouped.info.push(issue);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
return grouped;
|
|
2344
|
+
}
|
|
2345
|
+
__name(groupBySeverity, "groupBySeverity");
|
|
2346
|
+
|
|
2347
|
+
// ../../node_modules/@snapback/core/dist/analysis/static/OrphanDetector.js
|
|
2348
|
+
var DEFAULT_OPTIONS = {
|
|
2349
|
+
fileExtensions: [
|
|
2350
|
+
"ts",
|
|
2351
|
+
"tsx",
|
|
2352
|
+
"js",
|
|
2353
|
+
"jsx"
|
|
2354
|
+
],
|
|
2355
|
+
excludePatterns: [
|
|
2356
|
+
"node_modules",
|
|
2357
|
+
"dist",
|
|
2358
|
+
".next",
|
|
2359
|
+
"coverage",
|
|
2360
|
+
"**/*.test.*",
|
|
2361
|
+
"**/*.spec.*",
|
|
2362
|
+
"**/__tests__/**",
|
|
2363
|
+
"**/__mocks__/**"
|
|
2364
|
+
]
|
|
2365
|
+
};
|
|
2366
|
+
async function detectOrphans(entryPoint, options = {}) {
|
|
2367
|
+
const startTime = Date.now();
|
|
2368
|
+
const mergedOptions = {
|
|
2369
|
+
...DEFAULT_OPTIONS,
|
|
2370
|
+
...options
|
|
2371
|
+
};
|
|
2372
|
+
try {
|
|
2373
|
+
const madgeModule = await import('madge');
|
|
2374
|
+
const madge = madgeModule.default || madgeModule;
|
|
2375
|
+
const result = await madge(entryPoint, {
|
|
2376
|
+
fileExtensions: mergedOptions.fileExtensions,
|
|
2377
|
+
excludeRegExp: mergedOptions.excludePatterns.map((p) => {
|
|
2378
|
+
const regexPattern = p.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\./g, "\\.");
|
|
2379
|
+
return new RegExp(regexPattern);
|
|
2380
|
+
}),
|
|
2381
|
+
tsConfig: mergedOptions.tsConfigPath,
|
|
2382
|
+
detectiveOptions: {
|
|
2383
|
+
ts: {
|
|
2384
|
+
skipTypeImports: true
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
const orphans = result.orphans();
|
|
2389
|
+
const allFiles = Object.keys(result.obj());
|
|
2390
|
+
return {
|
|
2391
|
+
orphans,
|
|
2392
|
+
totalFiles: allFiles.length,
|
|
2393
|
+
success: true,
|
|
2394
|
+
duration: Date.now() - startTime
|
|
2395
|
+
};
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
return {
|
|
2398
|
+
orphans: [],
|
|
2399
|
+
totalFiles: 0,
|
|
2400
|
+
success: false,
|
|
2401
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2402
|
+
duration: Date.now() - startTime
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
__name(detectOrphans, "detectOrphans");
|
|
2407
|
+
function filterOrphansToFiles(orphanResult, targetFiles) {
|
|
2408
|
+
if (!orphanResult.success) {
|
|
2409
|
+
return [];
|
|
2410
|
+
}
|
|
2411
|
+
const targetSet = new Set(targetFiles.map((f) => f.replace(/\\/g, "/")));
|
|
2412
|
+
return orphanResult.orphans.filter((orphan) => {
|
|
2413
|
+
const normalizedOrphan = orphan.replace(/\\/g, "/");
|
|
2414
|
+
return targetSet.has(normalizedOrphan) || targetFiles.some((t) => normalizedOrphan.endsWith(t));
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
__name(filterOrphansToFiles, "filterOrphansToFiles");
|
|
2418
|
+
async function checkFilesForOrphanStatus(files, workspaceRoot) {
|
|
2419
|
+
const result = await detectOrphans(workspaceRoot, {
|
|
2420
|
+
baseDir: workspaceRoot
|
|
2421
|
+
});
|
|
2422
|
+
if (!result.success) {
|
|
2423
|
+
return {
|
|
2424
|
+
orphans: [],
|
|
2425
|
+
success: false,
|
|
2426
|
+
error: result.error
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
const orphans = filterOrphansToFiles(result, files);
|
|
2430
|
+
return {
|
|
2431
|
+
orphans,
|
|
2432
|
+
success: true
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
__name(checkFilesForOrphanStatus, "checkFilesForOrphanStatus");
|
|
2436
|
+
|
|
2437
|
+
// ../../node_modules/@snapback/core/dist/analysis/static/index.js
|
|
2438
|
+
async function runStaticAnalysis(files, _workspaceRoot, options = {}) {
|
|
2439
|
+
const startTime = Date.now();
|
|
2440
|
+
const result = {
|
|
2441
|
+
skippedTests: [],
|
|
2442
|
+
orphanedFiles: [],
|
|
2443
|
+
duration: 0,
|
|
2444
|
+
success: true,
|
|
2445
|
+
errors: []
|
|
2446
|
+
};
|
|
2447
|
+
if (!options.skipTestDetection) {
|
|
2448
|
+
try {
|
|
2449
|
+
const { analyzeSkippedTests: analyzeSkippedTests2 } = await import('./SkippedTestDetector-QLSQV7K7.js');
|
|
2450
|
+
const testResults = analyzeSkippedTests2(files);
|
|
2451
|
+
for (const testResult of testResults) {
|
|
2452
|
+
if (!testResult.parsed && testResult.error) {
|
|
2453
|
+
result.errors.push(`Parse error in ${testResult.file}: ${testResult.error}`);
|
|
2454
|
+
}
|
|
2455
|
+
for (const skipped of testResult.skipped) {
|
|
2456
|
+
result.skippedTests.push({
|
|
2457
|
+
file: skipped.file,
|
|
2458
|
+
type: skipped.type,
|
|
2459
|
+
name: skipped.name,
|
|
2460
|
+
line: skipped.line
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
result.errors.push(`Skipped test detection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (!options.skipOrphanDetection) ;
|
|
2469
|
+
result.duration = Date.now() - startTime;
|
|
2470
|
+
result.success = result.errors.length === 0;
|
|
2471
|
+
return result;
|
|
2472
|
+
}
|
|
2473
|
+
__name(runStaticAnalysis, "runStaticAnalysis");
|
|
2474
|
+
|
|
2475
|
+
export { ChangeImpactAnalyzer, CompletenessAnalyzer, ComplexityAnalyzer, ImportGraphAnalyzer, SecurityAnalyzer, SyntaxAnalyzer, checkFilesForOrphanStatus, countASTNodes, createChangeImpactAnalyzer, detectOrphans, extractImportSources, extractImports, extractImportsBatch, filterOrphansToFiles, isSupportedFile, offsetToLine, parseSource, runAnalysisPipeline, runStaticAnalysis, walkAST };
|