@snapback/cli 1.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/LICENSE +201 -0
- package/README.md +565 -0
- package/dist/SkippedTestDetector-JY4EF5BN.js +4 -0
- package/dist/SkippedTestDetector-JY4EF5BN.js.map +1 -0
- package/dist/analysis-B4NVULM4.js +5 -0
- package/dist/analysis-B4NVULM4.js.map +1 -0
- package/dist/chunk-BCIXMIPW.js +26 -0
- package/dist/chunk-BCIXMIPW.js.map +1 -0
- package/dist/chunk-BJS6XH2V.js +108 -0
- package/dist/chunk-BJS6XH2V.js.map +1 -0
- package/dist/chunk-KSPLKCVF.js +299 -0
- package/dist/chunk-KSPLKCVF.js.map +1 -0
- package/dist/chunk-MTQ6ESQR.js +16222 -0
- package/dist/chunk-MTQ6ESQR.js.map +1 -0
- package/dist/chunk-RU7BOXR3.js +906 -0
- package/dist/chunk-RU7BOXR3.js.map +1 -0
- package/dist/chunk-VSJ33PLA.js +1380 -0
- package/dist/chunk-VSJ33PLA.js.map +1 -0
- package/dist/chunk-WALLF2AH.js +12744 -0
- package/dist/chunk-WALLF2AH.js.map +1 -0
- package/dist/chunk-WCQVDF3K.js +12 -0
- package/dist/chunk-WCQVDF3K.js.map +1 -0
- package/dist/dist-7GPVXUEA.js +4 -0
- package/dist/dist-7GPVXUEA.js.map +1 -0
- package/dist/dist-DVM64QIS.js +7 -0
- package/dist/dist-DVM64QIS.js.map +1 -0
- package/dist/dist-FBRR6YHP.js +4 -0
- package/dist/dist-FBRR6YHP.js.map +1 -0
- package/dist/index.js +30717 -0
- package/dist/index.js.map +1 -0
- package/dist/secure-credentials-YKZHAZNB.js +257 -0
- package/dist/secure-credentials-YKZHAZNB.js.map +1 -0
- package/dist/snapback-dir-4QRR2IPV.js +5 -0
- package/dist/snapback-dir-4QRR2IPV.js.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
import { __name } from './chunk-WCQVDF3K.js';
|
|
2
|
+
import * as eslintParser from '@typescript-eslint/parser';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import traverse from '@babel/traverse';
|
|
5
|
+
import { dirname, relative, basename } from 'path';
|
|
6
|
+
|
|
7
|
+
var SyntaxAnalyzer = class {
|
|
8
|
+
static {
|
|
9
|
+
__name(this, "SyntaxAnalyzer");
|
|
10
|
+
}
|
|
11
|
+
id = "syntax";
|
|
12
|
+
name = "Syntax Analysis";
|
|
13
|
+
filePatterns = [
|
|
14
|
+
"*.ts",
|
|
15
|
+
"*.tsx",
|
|
16
|
+
"*.js",
|
|
17
|
+
"*.jsx"
|
|
18
|
+
];
|
|
19
|
+
async analyze(context) {
|
|
20
|
+
const startTime = performance.now();
|
|
21
|
+
const issues = [];
|
|
22
|
+
let filesAnalyzed = 0;
|
|
23
|
+
let nodesVisited = 0;
|
|
24
|
+
const parseErrors = [];
|
|
25
|
+
for (const [file, content] of context.contents) {
|
|
26
|
+
if (!this.shouldAnalyzeFile(file)) continue;
|
|
27
|
+
filesAnalyzed++;
|
|
28
|
+
try {
|
|
29
|
+
const ast = eslintParser.parse(content, {
|
|
30
|
+
sourceType: "module",
|
|
31
|
+
ecmaFeatures: {
|
|
32
|
+
jsx: file.endsWith(".tsx") || file.endsWith(".jsx")
|
|
33
|
+
},
|
|
34
|
+
ecmaVersion: "latest",
|
|
35
|
+
// Error recovery mode to get partial AST even with errors
|
|
36
|
+
errorOnUnknownASTType: false
|
|
37
|
+
});
|
|
38
|
+
nodesVisited += this.countNodes(ast);
|
|
39
|
+
this.checkSyntaxPatterns(content, file, issues);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const parseError = this.extractParseError(error);
|
|
42
|
+
parseErrors.push(`${file}: ${parseError.message}`);
|
|
43
|
+
issues.push({
|
|
44
|
+
id: `syntax/parse-error/${file}/${parseError.line}`,
|
|
45
|
+
severity: "critical",
|
|
46
|
+
type: "SYNTAX_ERROR",
|
|
47
|
+
message: parseError.message,
|
|
48
|
+
file,
|
|
49
|
+
line: parseError.line,
|
|
50
|
+
column: parseError.column,
|
|
51
|
+
fix: "Fix the syntax error to allow parsing"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
analyzer: this.id,
|
|
57
|
+
success: true,
|
|
58
|
+
issues,
|
|
59
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
60
|
+
duration: performance.now() - startTime,
|
|
61
|
+
metadata: {
|
|
62
|
+
filesAnalyzed,
|
|
63
|
+
nodesVisited,
|
|
64
|
+
parseErrors
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
shouldRun(context) {
|
|
69
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
70
|
+
}
|
|
71
|
+
shouldAnalyzeFile(file) {
|
|
72
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
73
|
+
return [
|
|
74
|
+
"ts",
|
|
75
|
+
"tsx",
|
|
76
|
+
"js",
|
|
77
|
+
"jsx"
|
|
78
|
+
].includes(ext || "");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract parse error information from parser exception
|
|
82
|
+
*/
|
|
83
|
+
extractParseError(error) {
|
|
84
|
+
if (error instanceof Error) {
|
|
85
|
+
const match = error.message.match(/\((\d+):(\d+)\)/);
|
|
86
|
+
if (match) {
|
|
87
|
+
return {
|
|
88
|
+
message: error.message,
|
|
89
|
+
line: Number.parseInt(match[1], 10),
|
|
90
|
+
column: Number.parseInt(match[2], 10)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
message: error.message,
|
|
95
|
+
line: 1,
|
|
96
|
+
column: 1
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
message: String(error),
|
|
101
|
+
line: 1,
|
|
102
|
+
column: 1
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Count AST nodes for coverage metrics
|
|
107
|
+
*/
|
|
108
|
+
countNodes(node) {
|
|
109
|
+
if (!node || typeof node !== "object") return 0;
|
|
110
|
+
let count = 1;
|
|
111
|
+
for (const key of Object.keys(node)) {
|
|
112
|
+
const value = node[key];
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
for (const item of value) {
|
|
115
|
+
count += this.countNodes(item);
|
|
116
|
+
}
|
|
117
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
118
|
+
count += this.countNodes(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return count;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Check for additional syntax patterns that may indicate issues
|
|
125
|
+
*/
|
|
126
|
+
checkSyntaxPatterns(content, file, issues) {
|
|
127
|
+
const lines = content.split("\n");
|
|
128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
129
|
+
const line = lines[i];
|
|
130
|
+
const lineNum = i + 1;
|
|
131
|
+
if (line.includes(";;")) {
|
|
132
|
+
issues.push({
|
|
133
|
+
id: `syntax/double-semicolon/${file}/${lineNum}`,
|
|
134
|
+
severity: "low",
|
|
135
|
+
type: "SYNTAX_WARNING",
|
|
136
|
+
message: "Double semicolon detected",
|
|
137
|
+
file,
|
|
138
|
+
line: lineNum,
|
|
139
|
+
column: line.indexOf(";;") + 1,
|
|
140
|
+
fix: "Remove extra semicolon",
|
|
141
|
+
snippet: line.trim()
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (/console\.assert\([^,]+,\s*\)/.test(line)) {
|
|
145
|
+
issues.push({
|
|
146
|
+
id: `syntax/empty-assert/${file}/${lineNum}`,
|
|
147
|
+
severity: "medium",
|
|
148
|
+
type: "SYNTAX_WARNING",
|
|
149
|
+
message: "console.assert with empty message",
|
|
150
|
+
file,
|
|
151
|
+
line: lineNum,
|
|
152
|
+
fix: "Add assertion message for debugging",
|
|
153
|
+
snippet: line.trim()
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (/if\s*\([^=]*=\s*[^=]/.test(line) && !/if\s*\([^=]*[=!]==/.test(line)) {
|
|
157
|
+
const assignMatch = line.match(/if\s*\(\s*(\w+)\s*=\s*[^=]/);
|
|
158
|
+
if (assignMatch) {
|
|
159
|
+
issues.push({
|
|
160
|
+
id: `syntax/assignment-in-condition/${file}/${lineNum}`,
|
|
161
|
+
severity: "medium",
|
|
162
|
+
type: "SYNTAX_WARNING",
|
|
163
|
+
message: "Possible assignment in condition (did you mean ===?)",
|
|
164
|
+
file,
|
|
165
|
+
line: lineNum,
|
|
166
|
+
fix: "Use === for comparison, or wrap in extra parentheses if intentional",
|
|
167
|
+
snippet: line.trim()
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var CompletenessAnalyzer = class {
|
|
175
|
+
static {
|
|
176
|
+
__name(this, "CompletenessAnalyzer");
|
|
177
|
+
}
|
|
178
|
+
id = "completeness";
|
|
179
|
+
name = "Completeness Detection";
|
|
180
|
+
filePatterns = [
|
|
181
|
+
"*.ts",
|
|
182
|
+
"*.tsx",
|
|
183
|
+
"*.js",
|
|
184
|
+
"*.jsx"
|
|
185
|
+
];
|
|
186
|
+
todoPatterns = [
|
|
187
|
+
/\/\/\s*TODO\b/gi,
|
|
188
|
+
/\/\/\s*FIXME\b/gi,
|
|
189
|
+
/\/\/\s*XXX\b/gi,
|
|
190
|
+
/\/\/\s*HACK\b/gi,
|
|
191
|
+
/\/\*\s*TODO\b/gi,
|
|
192
|
+
/\/\*\s*FIXME\b/gi
|
|
193
|
+
];
|
|
194
|
+
placeholderPatterns = [
|
|
195
|
+
/throw\s+new\s+Error\s*\(\s*['"`].*not\s*implemented.*['"`]\s*\)/gi,
|
|
196
|
+
/throw\s+new\s+Error\s*\(\s*['"`]TODO.*['"`]\s*\)/gi,
|
|
197
|
+
/NotImplementedError/gi,
|
|
198
|
+
/throw\s+new\s+Error\s*\(\s*['"`]STUB['"`]\s*\)/gi
|
|
199
|
+
];
|
|
200
|
+
parserOptions = {
|
|
201
|
+
sourceType: "module",
|
|
202
|
+
plugins: [
|
|
203
|
+
"typescript",
|
|
204
|
+
"jsx"
|
|
205
|
+
],
|
|
206
|
+
errorRecovery: true
|
|
207
|
+
};
|
|
208
|
+
async analyze(context) {
|
|
209
|
+
const startTime = performance.now();
|
|
210
|
+
const issues = [];
|
|
211
|
+
let filesAnalyzed = 0;
|
|
212
|
+
let nodesVisited = 0;
|
|
213
|
+
const parseErrors = [];
|
|
214
|
+
for (const [file, content] of context.contents) {
|
|
215
|
+
if (!this.shouldAnalyzeFile(file)) continue;
|
|
216
|
+
filesAnalyzed++;
|
|
217
|
+
this.checkTodoComments(content, file, issues);
|
|
218
|
+
this.checkPlaceholderPatterns(content, file, issues);
|
|
219
|
+
try {
|
|
220
|
+
const ast = parse(content, {
|
|
221
|
+
...this.parserOptions,
|
|
222
|
+
plugins: this.getPluginsForFile(file)
|
|
223
|
+
});
|
|
224
|
+
const result = this.analyzeAST(ast, content, file);
|
|
225
|
+
issues.push(...result.issues);
|
|
226
|
+
nodesVisited += result.nodesVisited;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
parseErrors.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
analyzer: this.id,
|
|
233
|
+
success: true,
|
|
234
|
+
issues,
|
|
235
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
236
|
+
duration: performance.now() - startTime,
|
|
237
|
+
metadata: {
|
|
238
|
+
filesAnalyzed,
|
|
239
|
+
nodesVisited,
|
|
240
|
+
patternsChecked: [
|
|
241
|
+
"TODO",
|
|
242
|
+
"FIXME",
|
|
243
|
+
"EMPTY_CATCH",
|
|
244
|
+
"EMPTY_FUNCTION",
|
|
245
|
+
"NOT_IMPLEMENTED",
|
|
246
|
+
"PLACEHOLDER"
|
|
247
|
+
],
|
|
248
|
+
parseErrors
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
shouldRun(context) {
|
|
253
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
254
|
+
}
|
|
255
|
+
shouldAnalyzeFile(file) {
|
|
256
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
257
|
+
return [
|
|
258
|
+
"ts",
|
|
259
|
+
"tsx",
|
|
260
|
+
"js",
|
|
261
|
+
"jsx"
|
|
262
|
+
].includes(ext || "");
|
|
263
|
+
}
|
|
264
|
+
getPluginsForFile(file) {
|
|
265
|
+
const plugins = [
|
|
266
|
+
"typescript"
|
|
267
|
+
];
|
|
268
|
+
if (file.endsWith(".tsx") || file.endsWith(".jsx")) {
|
|
269
|
+
plugins.push("jsx");
|
|
270
|
+
}
|
|
271
|
+
return plugins;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Check for TODO/FIXME comments
|
|
275
|
+
*/
|
|
276
|
+
checkTodoComments(content, file, issues) {
|
|
277
|
+
const lines = content.split("\n");
|
|
278
|
+
for (let i = 0; i < lines.length; i++) {
|
|
279
|
+
const line = lines[i];
|
|
280
|
+
const lineNum = i + 1;
|
|
281
|
+
for (const pattern of this.todoPatterns) {
|
|
282
|
+
pattern.lastIndex = 0;
|
|
283
|
+
if (pattern.test(line)) {
|
|
284
|
+
const todoContent = line.trim().slice(0, 100);
|
|
285
|
+
issues.push({
|
|
286
|
+
id: `completeness/todo/${file}/${lineNum}`,
|
|
287
|
+
severity: "medium",
|
|
288
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
289
|
+
message: `TODO/FIXME: ${todoContent}`,
|
|
290
|
+
file,
|
|
291
|
+
line: lineNum,
|
|
292
|
+
snippet: todoContent
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check for placeholder/stub patterns
|
|
301
|
+
*/
|
|
302
|
+
checkPlaceholderPatterns(content, file, issues) {
|
|
303
|
+
const lines = content.split("\n");
|
|
304
|
+
for (let i = 0; i < lines.length; i++) {
|
|
305
|
+
const line = lines[i];
|
|
306
|
+
const lineNum = i + 1;
|
|
307
|
+
for (const pattern of this.placeholderPatterns) {
|
|
308
|
+
pattern.lastIndex = 0;
|
|
309
|
+
if (pattern.test(line)) {
|
|
310
|
+
issues.push({
|
|
311
|
+
id: `completeness/placeholder/${file}/${lineNum}`,
|
|
312
|
+
severity: "high",
|
|
313
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
314
|
+
message: 'Placeholder implementation: "not implemented" or similar',
|
|
315
|
+
file,
|
|
316
|
+
line: lineNum,
|
|
317
|
+
fix: "Implement the functionality or remove the placeholder",
|
|
318
|
+
snippet: line.trim().slice(0, 100)
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* AST-based detection of empty/incomplete code
|
|
327
|
+
*/
|
|
328
|
+
analyzeAST(ast, _content, file) {
|
|
329
|
+
const issues = [];
|
|
330
|
+
let nodesVisited = 0;
|
|
331
|
+
traverse(ast, {
|
|
332
|
+
enter() {
|
|
333
|
+
nodesVisited++;
|
|
334
|
+
},
|
|
335
|
+
// Empty catch blocks
|
|
336
|
+
CatchClause: /* @__PURE__ */ __name((path) => {
|
|
337
|
+
const body = path.node.body;
|
|
338
|
+
if (body.body.length === 0) {
|
|
339
|
+
issues.push({
|
|
340
|
+
id: `completeness/empty-catch/${file}/${path.node.loc?.start.line}`,
|
|
341
|
+
severity: "medium",
|
|
342
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
343
|
+
message: "Empty catch block - errors silently swallowed",
|
|
344
|
+
file,
|
|
345
|
+
line: path.node.loc?.start.line,
|
|
346
|
+
fix: "Add error handling, rethrow, or log the error"
|
|
347
|
+
});
|
|
348
|
+
} else if (body.body.length === 1) {
|
|
349
|
+
const stmt = body.body[0];
|
|
350
|
+
if (stmt.type === "EmptyStatement") {
|
|
351
|
+
issues.push({
|
|
352
|
+
id: `completeness/empty-catch/${file}/${path.node.loc?.start.line}`,
|
|
353
|
+
severity: "medium",
|
|
354
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
355
|
+
message: "Catch block contains only empty statement",
|
|
356
|
+
file,
|
|
357
|
+
line: path.node.loc?.start.line,
|
|
358
|
+
fix: "Add proper error handling"
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}, "CatchClause"),
|
|
363
|
+
// Empty function bodies (excluding type declarations and interface methods)
|
|
364
|
+
FunctionDeclaration: /* @__PURE__ */ __name((path) => {
|
|
365
|
+
if (path.node.body.body.length === 0) {
|
|
366
|
+
const funcName = path.node.id?.name || "anonymous";
|
|
367
|
+
{
|
|
368
|
+
issues.push({
|
|
369
|
+
id: `completeness/empty-fn/${file}/${path.node.loc?.start.line}`,
|
|
370
|
+
severity: "medium",
|
|
371
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
372
|
+
message: `Empty function body: ${funcName}()`,
|
|
373
|
+
file,
|
|
374
|
+
line: path.node.loc?.start.line,
|
|
375
|
+
fix: "Implement the function or mark as abstract/stub if intentional"
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}, "FunctionDeclaration"),
|
|
380
|
+
// Empty method bodies
|
|
381
|
+
ClassMethod: /* @__PURE__ */ __name((path) => {
|
|
382
|
+
if (path.node.abstract) return;
|
|
383
|
+
if (path.node.kind === "get" || path.node.kind === "set") return;
|
|
384
|
+
const body = path.node.body;
|
|
385
|
+
if (body && body.body.length === 0) {
|
|
386
|
+
const methodName = path.node.key.type === "Identifier" ? path.node.key.name : "anonymous";
|
|
387
|
+
if (methodName === "constructor") return;
|
|
388
|
+
issues.push({
|
|
389
|
+
id: `completeness/empty-method/${file}/${path.node.loc?.start.line}`,
|
|
390
|
+
severity: "medium",
|
|
391
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
392
|
+
message: `Empty method body: ${methodName}()`,
|
|
393
|
+
file,
|
|
394
|
+
line: path.node.loc?.start.line,
|
|
395
|
+
fix: "Implement the method or mark as abstract if intentional"
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}, "ClassMethod"),
|
|
399
|
+
// Arrow functions that just throw or are empty (might be intentional)
|
|
400
|
+
ArrowFunctionExpression: /* @__PURE__ */ __name((path) => {
|
|
401
|
+
const body = path.node.body;
|
|
402
|
+
if (body.type === "BlockStatement" && body.body.length === 0) {
|
|
403
|
+
const parent = path.parent;
|
|
404
|
+
if (parent.type === "VariableDeclarator") {
|
|
405
|
+
const varName = parent.id.type === "Identifier" ? parent.id.name : "anonymous";
|
|
406
|
+
issues.push({
|
|
407
|
+
id: `completeness/empty-arrow/${file}/${path.node.loc?.start.line}`,
|
|
408
|
+
severity: "low",
|
|
409
|
+
type: "INCOMPLETE_IMPLEMENTATION",
|
|
410
|
+
message: `Empty arrow function: ${varName}`,
|
|
411
|
+
file,
|
|
412
|
+
line: path.node.loc?.start.line,
|
|
413
|
+
fix: "Implement the function or use () => {} if intentionally empty"
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}, "ArrowFunctionExpression"),
|
|
418
|
+
// Check for console.log that might be debug code
|
|
419
|
+
CallExpression: /* @__PURE__ */ __name((path) => {
|
|
420
|
+
const callee = path.node.callee;
|
|
421
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "console" && callee.property.type === "Identifier" && callee.property.name === "log") {
|
|
422
|
+
const firstArg = path.node.arguments[0];
|
|
423
|
+
if (firstArg && firstArg.type === "StringLiteral") {
|
|
424
|
+
const msg = firstArg.value.toLowerCase();
|
|
425
|
+
if (msg.includes("debug") || msg.includes("test") || msg.includes("todo") || msg.includes("remove")) {
|
|
426
|
+
issues.push({
|
|
427
|
+
id: `completeness/debug-log/${file}/${path.node.loc?.start.line}`,
|
|
428
|
+
severity: "low",
|
|
429
|
+
type: "DEBUG_CODE",
|
|
430
|
+
message: `Debug console.log left in code: "${firstArg.value.slice(0, 50)}"`,
|
|
431
|
+
file,
|
|
432
|
+
line: path.node.loc?.start.line,
|
|
433
|
+
fix: "Remove debug logging before commit"
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}, "CallExpression")
|
|
439
|
+
});
|
|
440
|
+
return {
|
|
441
|
+
issues,
|
|
442
|
+
nodesVisited
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var EXPORT_PATTERNS = [
|
|
447
|
+
/export\s+(const|function|class|interface|type|enum)\s+(\w+)/g,
|
|
448
|
+
/export\s+default\s+(function|class)?\s*(\w+)?/g,
|
|
449
|
+
/export\s+\{([^}]+)\}/g
|
|
450
|
+
];
|
|
451
|
+
var PERFORMANCE_PATTERNS = [
|
|
452
|
+
{
|
|
453
|
+
pattern: /\.forEach\s*\(/g,
|
|
454
|
+
type: "computation",
|
|
455
|
+
risk: "low"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
pattern: /for\s*\(\s*let\s+\w+\s*=\s*0/g,
|
|
459
|
+
type: "computation",
|
|
460
|
+
risk: "low"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
pattern: /while\s*\(/g,
|
|
464
|
+
type: "computation",
|
|
465
|
+
risk: "medium"
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
pattern: /async\s+function|await\s+/g,
|
|
469
|
+
type: "io",
|
|
470
|
+
risk: "medium"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
pattern: /new\s+(Map|Set|Array)\s*\(/g,
|
|
474
|
+
type: "memory",
|
|
475
|
+
risk: "low"
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
pattern: /JSON\.(parse|stringify)/g,
|
|
479
|
+
type: "computation",
|
|
480
|
+
risk: "medium"
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
pattern: /readFileSync|writeFileSync/g,
|
|
484
|
+
type: "io",
|
|
485
|
+
risk: "high"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
pattern: /spawn|exec\s*\(/g,
|
|
489
|
+
type: "io",
|
|
490
|
+
risk: "high"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
pattern: /import\s*\(/g,
|
|
494
|
+
type: "bundle",
|
|
495
|
+
risk: "low"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
pattern: /require\s*\(/g,
|
|
499
|
+
type: "bundle",
|
|
500
|
+
risk: "medium"
|
|
501
|
+
}
|
|
502
|
+
];
|
|
503
|
+
var TEST_FILE_PATTERNS = [
|
|
504
|
+
/\.test\.[tj]sx?$/,
|
|
505
|
+
/\.spec\.[tj]sx?$/,
|
|
506
|
+
/__tests__\//,
|
|
507
|
+
/test\//,
|
|
508
|
+
/tests\//
|
|
509
|
+
];
|
|
510
|
+
var ChangeImpactAnalyzer = class {
|
|
511
|
+
static {
|
|
512
|
+
__name(this, "ChangeImpactAnalyzer");
|
|
513
|
+
}
|
|
514
|
+
id = "change-impact";
|
|
515
|
+
name = "Change Impact Analyzer";
|
|
516
|
+
filePatterns = [
|
|
517
|
+
"**/*.ts",
|
|
518
|
+
"**/*.tsx",
|
|
519
|
+
"**/*.js",
|
|
520
|
+
"**/*.jsx"
|
|
521
|
+
];
|
|
522
|
+
workspaceRoot;
|
|
523
|
+
dependencyGraph = /* @__PURE__ */ new Map();
|
|
524
|
+
reverseDependencyGraph = /* @__PURE__ */ new Map();
|
|
525
|
+
constructor(workspaceRoot) {
|
|
526
|
+
this.workspaceRoot = workspaceRoot;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Check if this analyzer should run
|
|
530
|
+
*/
|
|
531
|
+
shouldRun(context) {
|
|
532
|
+
return context.files.some((f) => this.filePatterns.some((p) => new RegExp(p.replace(/\*/g, ".*")).test(f)));
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Run impact analysis
|
|
536
|
+
*/
|
|
537
|
+
async analyze(context) {
|
|
538
|
+
const start = Date.now();
|
|
539
|
+
const issues = [];
|
|
540
|
+
try {
|
|
541
|
+
await this.buildDependencyGraph(context);
|
|
542
|
+
for (const file of context.files) {
|
|
543
|
+
const content = context.contents.get(file);
|
|
544
|
+
if (!content) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const breakingChanges = this.detectBreakingChanges(content, file);
|
|
548
|
+
for (const bc of breakingChanges) {
|
|
549
|
+
issues.push({
|
|
550
|
+
id: `impact/breaking/${bc.type}/${file}/${bc.symbol}`,
|
|
551
|
+
severity: bc.severity,
|
|
552
|
+
type: `BREAKING_${bc.type.toUpperCase()}`,
|
|
553
|
+
message: bc.description,
|
|
554
|
+
file,
|
|
555
|
+
fix: bc.migration
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
const perfImpacts = this.detectPerformanceImpacts(content, file);
|
|
559
|
+
for (const pi of perfImpacts) {
|
|
560
|
+
if (pi.risk === "high" || pi.risk === "critical") {
|
|
561
|
+
issues.push({
|
|
562
|
+
id: `impact/perf/${pi.type}/${file}/${pi.component}`,
|
|
563
|
+
severity: pi.risk === "critical" ? "critical" : "high",
|
|
564
|
+
type: `PERF_${pi.type.toUpperCase()}`,
|
|
565
|
+
message: pi.description,
|
|
566
|
+
file,
|
|
567
|
+
fix: pi.recommendation
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const affectedTests = this.findAffectedTests(file);
|
|
572
|
+
if (affectedTests.length > 5) {
|
|
573
|
+
issues.push({
|
|
574
|
+
id: `impact/tests/${file}`,
|
|
575
|
+
severity: "medium",
|
|
576
|
+
type: "HIGH_TEST_IMPACT",
|
|
577
|
+
message: `Change affects ${affectedTests.length} test files - consider running full test suite`,
|
|
578
|
+
file
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
analyzer: this.id,
|
|
584
|
+
success: true,
|
|
585
|
+
issues,
|
|
586
|
+
coverage: 1,
|
|
587
|
+
duration: Date.now() - start,
|
|
588
|
+
metadata: {
|
|
589
|
+
filesAnalyzed: context.files.length
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
} catch (error) {
|
|
593
|
+
return {
|
|
594
|
+
analyzer: this.id,
|
|
595
|
+
success: false,
|
|
596
|
+
issues: [
|
|
597
|
+
{
|
|
598
|
+
id: "impact/error",
|
|
599
|
+
severity: "high",
|
|
600
|
+
type: "ANALYSIS_ERROR",
|
|
601
|
+
message: error instanceof Error ? error.message : String(error)
|
|
602
|
+
}
|
|
603
|
+
],
|
|
604
|
+
coverage: 0,
|
|
605
|
+
duration: Date.now() - start
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Get full impact analysis (more detailed than standard analyze)
|
|
611
|
+
*/
|
|
612
|
+
async getFullImpact(files, contents) {
|
|
613
|
+
const start = Date.now();
|
|
614
|
+
const context = {
|
|
615
|
+
workspaceRoot: this.workspaceRoot,
|
|
616
|
+
files,
|
|
617
|
+
contents
|
|
618
|
+
};
|
|
619
|
+
await this.buildDependencyGraph(context);
|
|
620
|
+
const affectedTests = [];
|
|
621
|
+
const breakingChanges = [];
|
|
622
|
+
const performanceImpacts = [];
|
|
623
|
+
const dependentFiles = [];
|
|
624
|
+
const recommendations = [];
|
|
625
|
+
for (const file of files) {
|
|
626
|
+
const content = contents.get(file) || "";
|
|
627
|
+
const tests = this.findAffectedTests(file);
|
|
628
|
+
affectedTests.push(...tests);
|
|
629
|
+
const breaks = this.detectBreakingChanges(content, file);
|
|
630
|
+
breakingChanges.push(...breaks);
|
|
631
|
+
const perfs = this.detectPerformanceImpacts(content, file);
|
|
632
|
+
performanceImpacts.push(...perfs);
|
|
633
|
+
const deps = this.findDependentFiles(file);
|
|
634
|
+
dependentFiles.push(...deps);
|
|
635
|
+
}
|
|
636
|
+
const impactScore = this.calculateImpactScore(affectedTests, breakingChanges, performanceImpacts, dependentFiles);
|
|
637
|
+
if (breakingChanges.length > 0) {
|
|
638
|
+
recommendations.push(`\u26A0\uFE0F ${breakingChanges.length} breaking change(s) detected - update dependent code`);
|
|
639
|
+
}
|
|
640
|
+
if (affectedTests.length > 10) {
|
|
641
|
+
recommendations.push(`\u{1F9EA} Run full test suite - ${affectedTests.length} tests potentially affected`);
|
|
642
|
+
}
|
|
643
|
+
if (performanceImpacts.some((p) => p.risk === "high" || p.risk === "critical")) {
|
|
644
|
+
recommendations.push("\u26A1 Performance-sensitive code modified - run benchmarks");
|
|
645
|
+
}
|
|
646
|
+
if (dependentFiles.length > 20) {
|
|
647
|
+
recommendations.push("\u{1F517} High ripple effect - consider incremental rollout");
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
filesAnalyzed: files.length,
|
|
651
|
+
affectedTests: this.dedupeItems(affectedTests),
|
|
652
|
+
breakingChanges,
|
|
653
|
+
performanceImpacts,
|
|
654
|
+
dependentFiles: this.dedupeItems(dependentFiles),
|
|
655
|
+
impactScore,
|
|
656
|
+
recommendations,
|
|
657
|
+
duration: Date.now() - start
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
// =========================================================================
|
|
661
|
+
// Private Methods
|
|
662
|
+
// =========================================================================
|
|
663
|
+
/**
|
|
664
|
+
* Build dependency graph from file contents
|
|
665
|
+
*/
|
|
666
|
+
async buildDependencyGraph(context) {
|
|
667
|
+
this.dependencyGraph.clear();
|
|
668
|
+
this.reverseDependencyGraph.clear();
|
|
669
|
+
for (const file of context.files) {
|
|
670
|
+
const content = context.contents.get(file);
|
|
671
|
+
if (!content) {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const imports = this.extractImports(content, file);
|
|
675
|
+
this.dependencyGraph.set(file, imports);
|
|
676
|
+
for (const imp of imports) {
|
|
677
|
+
const existing = this.reverseDependencyGraph.get(imp) || [];
|
|
678
|
+
existing.push(file);
|
|
679
|
+
this.reverseDependencyGraph.set(imp, existing);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Extract import statements from file content
|
|
685
|
+
*/
|
|
686
|
+
extractImports(content, fromFile) {
|
|
687
|
+
const imports = [];
|
|
688
|
+
const importRegex = /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
689
|
+
const requireRegex = /require\s*\(['"]([^'"]+)['"]\)/g;
|
|
690
|
+
let match;
|
|
691
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
692
|
+
const importPath = this.resolveImportPath(match[1], fromFile);
|
|
693
|
+
if (importPath) {
|
|
694
|
+
imports.push(importPath);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
698
|
+
const importPath = this.resolveImportPath(match[1], fromFile);
|
|
699
|
+
if (importPath) {
|
|
700
|
+
imports.push(importPath);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return imports;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Resolve import path to absolute file path
|
|
707
|
+
*/
|
|
708
|
+
resolveImportPath(importPath, fromFile) {
|
|
709
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
const dir = dirname(fromFile);
|
|
713
|
+
const extensions = [
|
|
714
|
+
".ts",
|
|
715
|
+
".tsx",
|
|
716
|
+
".js",
|
|
717
|
+
".jsx",
|
|
718
|
+
"/index.ts",
|
|
719
|
+
"/index.tsx",
|
|
720
|
+
"/index.js"
|
|
721
|
+
];
|
|
722
|
+
for (const ext of extensions) {
|
|
723
|
+
const resolved = `${dir}/${importPath}${ext}`.replace(/\/\.\//g, "/");
|
|
724
|
+
return resolved;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Find test files that might be affected by a change
|
|
730
|
+
*/
|
|
731
|
+
findAffectedTests(file) {
|
|
732
|
+
const tests = [];
|
|
733
|
+
const relPath = relative(this.workspaceRoot, file);
|
|
734
|
+
const fileName = basename(file).replace(/\.[tj]sx?$/, "");
|
|
735
|
+
const directTestPatterns = [
|
|
736
|
+
`${fileName}.test.ts`,
|
|
737
|
+
`${fileName}.test.tsx`,
|
|
738
|
+
`${fileName}.spec.ts`,
|
|
739
|
+
`${fileName}.spec.tsx`,
|
|
740
|
+
`__tests__/${fileName}.test.ts`,
|
|
741
|
+
`__tests__/${fileName}.test.tsx`
|
|
742
|
+
];
|
|
743
|
+
for (const pattern of directTestPatterns) {
|
|
744
|
+
tests.push({
|
|
745
|
+
path: pattern,
|
|
746
|
+
reason: "Direct test file for changed source",
|
|
747
|
+
level: "high"
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
const importers = this.reverseDependencyGraph.get(file) || [];
|
|
751
|
+
for (const importer of importers) {
|
|
752
|
+
if (this.isTestFile(importer)) {
|
|
753
|
+
tests.push({
|
|
754
|
+
path: relative(this.workspaceRoot, importer),
|
|
755
|
+
reason: "Test file imports changed module",
|
|
756
|
+
level: "medium"
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (relPath.includes("/core/") || relPath.includes("/services/")) {
|
|
761
|
+
tests.push({
|
|
762
|
+
path: "**/*.integration.test.ts",
|
|
763
|
+
reason: "Core module change may affect integration tests",
|
|
764
|
+
level: "low"
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
return tests;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Check if a file is a test file
|
|
771
|
+
*/
|
|
772
|
+
isTestFile(file) {
|
|
773
|
+
return TEST_FILE_PATTERNS.some((p) => p.test(file));
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Detect breaking changes in content
|
|
777
|
+
*/
|
|
778
|
+
detectBreakingChanges(content, file) {
|
|
779
|
+
const breaks = [];
|
|
780
|
+
for (const pattern of EXPORT_PATTERNS) {
|
|
781
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
782
|
+
let match2;
|
|
783
|
+
while ((match2 = regex.exec(content)) !== null) {
|
|
784
|
+
const symbolName = match2[2] || match2[1];
|
|
785
|
+
if (symbolName) {
|
|
786
|
+
breaks.push({
|
|
787
|
+
type: "export",
|
|
788
|
+
symbol: symbolName,
|
|
789
|
+
file,
|
|
790
|
+
description: `Exported symbol '${symbolName}' may have changed`,
|
|
791
|
+
severity: "medium",
|
|
792
|
+
migration: `Verify consumers of '${symbolName}' are updated`
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)\s*\{([^}]+)\}/g;
|
|
798
|
+
let match;
|
|
799
|
+
while ((match = interfaceRegex.exec(content)) !== null) {
|
|
800
|
+
const interfaceName = match[1];
|
|
801
|
+
const body = match[2];
|
|
802
|
+
if (body.includes("?:") || body.includes(": ")) {
|
|
803
|
+
breaks.push({
|
|
804
|
+
type: "type",
|
|
805
|
+
symbol: interfaceName,
|
|
806
|
+
file,
|
|
807
|
+
description: `Interface '${interfaceName}' definition changed`,
|
|
808
|
+
severity: "medium"
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return breaks;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Detect performance-sensitive code changes
|
|
816
|
+
*/
|
|
817
|
+
detectPerformanceImpacts(content, file) {
|
|
818
|
+
const impacts = [];
|
|
819
|
+
for (const { pattern, type, risk } of PERFORMANCE_PATTERNS) {
|
|
820
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
821
|
+
let match;
|
|
822
|
+
while ((match = regex.exec(content)) !== null) {
|
|
823
|
+
impacts.push({
|
|
824
|
+
type,
|
|
825
|
+
description: `${type} operation detected: ${match[0]}`,
|
|
826
|
+
risk,
|
|
827
|
+
component: basename(file),
|
|
828
|
+
recommendation: this.getPerformanceRecommendation(type)
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return impacts;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Get recommendation for performance issue type
|
|
836
|
+
*/
|
|
837
|
+
getPerformanceRecommendation(type) {
|
|
838
|
+
switch (type) {
|
|
839
|
+
case "hotpath":
|
|
840
|
+
return "Consider memoization or caching for hot paths";
|
|
841
|
+
case "memory":
|
|
842
|
+
return "Monitor memory usage, consider object pooling";
|
|
843
|
+
case "io":
|
|
844
|
+
return "Use async operations, consider batching";
|
|
845
|
+
case "computation":
|
|
846
|
+
return "Profile for bottlenecks, consider Web Workers";
|
|
847
|
+
case "bundle":
|
|
848
|
+
return "Use dynamic imports for code splitting";
|
|
849
|
+
default:
|
|
850
|
+
return "Profile before optimizing";
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Find files that depend on changed file
|
|
855
|
+
*/
|
|
856
|
+
findDependentFiles(file) {
|
|
857
|
+
const dependents = [];
|
|
858
|
+
const visited = /* @__PURE__ */ new Set();
|
|
859
|
+
const traverse3 = /* @__PURE__ */ __name((current, depth) => {
|
|
860
|
+
if (visited.has(current) || depth > 3) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
visited.add(current);
|
|
864
|
+
const importers = this.reverseDependencyGraph.get(current) || [];
|
|
865
|
+
for (const importer of importers) {
|
|
866
|
+
dependents.push({
|
|
867
|
+
path: relative(this.workspaceRoot, importer),
|
|
868
|
+
reason: depth === 0 ? "Directly imports changed file" : `Transitive dependency (depth ${depth})`,
|
|
869
|
+
level: depth === 0 ? "high" : depth === 1 ? "medium" : "low"
|
|
870
|
+
});
|
|
871
|
+
traverse3(importer, depth + 1);
|
|
872
|
+
}
|
|
873
|
+
}, "traverse");
|
|
874
|
+
traverse3(file, 0);
|
|
875
|
+
return dependents;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Calculate overall impact score
|
|
879
|
+
*/
|
|
880
|
+
calculateImpactScore(tests, breaks, perfs, deps) {
|
|
881
|
+
let score = 0;
|
|
882
|
+
score += Math.min(tests.length * 0.05, 0.25);
|
|
883
|
+
score += Math.min(breaks.length * 0.15, 0.35);
|
|
884
|
+
score += Math.min(perfs.filter((p) => p.risk === "high").length * 0.1, 0.2);
|
|
885
|
+
score += Math.min(deps.length * 0.02, 0.2);
|
|
886
|
+
return Math.min(score, 1);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Deduplicate impact items
|
|
890
|
+
*/
|
|
891
|
+
dedupeItems(items) {
|
|
892
|
+
const seen = /* @__PURE__ */ new Set();
|
|
893
|
+
return items.filter((item) => {
|
|
894
|
+
if (seen.has(item.path)) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
seen.add(item.path);
|
|
898
|
+
return true;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
function createChangeImpactAnalyzer(workspaceRoot) {
|
|
903
|
+
return new ChangeImpactAnalyzer(workspaceRoot);
|
|
904
|
+
}
|
|
905
|
+
__name(createChangeImpactAnalyzer, "createChangeImpactAnalyzer");
|
|
906
|
+
var SecurityAnalyzer = class {
|
|
907
|
+
static {
|
|
908
|
+
__name(this, "SecurityAnalyzer");
|
|
909
|
+
}
|
|
910
|
+
id = "security";
|
|
911
|
+
name = "Security Analysis";
|
|
912
|
+
filePatterns = [
|
|
913
|
+
"*.ts",
|
|
914
|
+
"*.tsx",
|
|
915
|
+
"*.js",
|
|
916
|
+
"*.jsx"
|
|
917
|
+
];
|
|
918
|
+
parserOptions = {
|
|
919
|
+
sourceType: "module",
|
|
920
|
+
plugins: [
|
|
921
|
+
"typescript",
|
|
922
|
+
"jsx"
|
|
923
|
+
],
|
|
924
|
+
errorRecovery: true
|
|
925
|
+
};
|
|
926
|
+
async analyze(context) {
|
|
927
|
+
const startTime = performance.now();
|
|
928
|
+
const issues = [];
|
|
929
|
+
let filesAnalyzed = 0;
|
|
930
|
+
let nodesVisited = 0;
|
|
931
|
+
const parseErrors = [];
|
|
932
|
+
for (const [file, content] of context.contents) {
|
|
933
|
+
if (!this.shouldAnalyzeFile(file)) continue;
|
|
934
|
+
filesAnalyzed++;
|
|
935
|
+
try {
|
|
936
|
+
const ast = parse(content, {
|
|
937
|
+
...this.parserOptions,
|
|
938
|
+
plugins: this.getPluginsForFile(file)
|
|
939
|
+
});
|
|
940
|
+
const fileIssues = this.analyzeAST(ast, content, file);
|
|
941
|
+
issues.push(...fileIssues.issues);
|
|
942
|
+
nodesVisited += fileIssues.nodesVisited;
|
|
943
|
+
} catch (error) {
|
|
944
|
+
parseErrors.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
945
|
+
issues.push({
|
|
946
|
+
id: `security/parse-error/${file}`,
|
|
947
|
+
severity: "info",
|
|
948
|
+
type: "PARSE_ERROR",
|
|
949
|
+
message: `Could not parse for security analysis: ${error instanceof Error ? error.message : String(error)}`,
|
|
950
|
+
file
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
analyzer: this.id,
|
|
956
|
+
success: true,
|
|
957
|
+
issues,
|
|
958
|
+
coverage: filesAnalyzed / Math.max(context.files.length, 1),
|
|
959
|
+
duration: performance.now() - startTime,
|
|
960
|
+
metadata: {
|
|
961
|
+
filesAnalyzed,
|
|
962
|
+
nodesVisited,
|
|
963
|
+
patternsChecked: [
|
|
964
|
+
"UNSAFE_EVAL",
|
|
965
|
+
"PATH_TRAVERSAL",
|
|
966
|
+
"MISSING_SIGNAL_HANDLER",
|
|
967
|
+
"COMMAND_INJECTION",
|
|
968
|
+
"SQL_INJECTION",
|
|
969
|
+
"XSS_RISK",
|
|
970
|
+
"HARDCODED_SECRET",
|
|
971
|
+
"UNSAFE_REGEX"
|
|
972
|
+
],
|
|
973
|
+
parseErrors
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
shouldRun(context) {
|
|
978
|
+
return context.files.some((f) => this.shouldAnalyzeFile(f));
|
|
979
|
+
}
|
|
980
|
+
shouldAnalyzeFile(file) {
|
|
981
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
982
|
+
return [
|
|
983
|
+
"ts",
|
|
984
|
+
"tsx",
|
|
985
|
+
"js",
|
|
986
|
+
"jsx"
|
|
987
|
+
].includes(ext || "");
|
|
988
|
+
}
|
|
989
|
+
getPluginsForFile(file) {
|
|
990
|
+
const plugins = [
|
|
991
|
+
"typescript"
|
|
992
|
+
];
|
|
993
|
+
if (file.endsWith(".tsx") || file.endsWith(".jsx")) {
|
|
994
|
+
plugins.push("jsx");
|
|
995
|
+
}
|
|
996
|
+
return plugins;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Analyze AST for security issues
|
|
1000
|
+
*/
|
|
1001
|
+
analyzeAST(ast, content, file) {
|
|
1002
|
+
const issues = [];
|
|
1003
|
+
let nodesVisited = 0;
|
|
1004
|
+
const fileContext = {
|
|
1005
|
+
isDaemon: false,
|
|
1006
|
+
hasSignalHandler: false};
|
|
1007
|
+
fileContext.isDaemon = content.includes(".listen(") || file.includes("daemon") || file.includes("server") || file.includes("worker");
|
|
1008
|
+
traverse(ast, {
|
|
1009
|
+
enter() {
|
|
1010
|
+
nodesVisited++;
|
|
1011
|
+
},
|
|
1012
|
+
// Detect eval()
|
|
1013
|
+
CallExpression: /* @__PURE__ */ __name((path) => {
|
|
1014
|
+
const callee = path.node.callee;
|
|
1015
|
+
if (callee.type === "Identifier" && callee.name === "eval") {
|
|
1016
|
+
issues.push({
|
|
1017
|
+
id: `security/eval/${file}/${path.node.loc?.start.line}`,
|
|
1018
|
+
severity: "critical",
|
|
1019
|
+
type: "UNSAFE_EVAL",
|
|
1020
|
+
message: "eval() allows arbitrary code execution",
|
|
1021
|
+
file,
|
|
1022
|
+
line: path.node.loc?.start.line,
|
|
1023
|
+
column: path.node.loc?.start.column,
|
|
1024
|
+
fix: "Use JSON.parse() for data or refactor logic to avoid eval"
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (callee.type === "Identifier" && callee.name === "Function") {
|
|
1028
|
+
issues.push({
|
|
1029
|
+
id: `security/function-constructor/${file}/${path.node.loc?.start.line}`,
|
|
1030
|
+
severity: "critical",
|
|
1031
|
+
type: "UNSAFE_EVAL",
|
|
1032
|
+
message: "new Function() is equivalent to eval() and allows arbitrary code execution",
|
|
1033
|
+
file,
|
|
1034
|
+
line: path.node.loc?.start.line,
|
|
1035
|
+
column: path.node.loc?.start.column,
|
|
1036
|
+
fix: "Refactor to avoid dynamic code generation"
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
if (callee.type === "Identifier" && (callee.name === "setTimeout" || callee.name === "setInterval")) {
|
|
1040
|
+
const firstArg = path.node.arguments[0];
|
|
1041
|
+
if (firstArg && firstArg.type === "StringLiteral") {
|
|
1042
|
+
issues.push({
|
|
1043
|
+
id: `security/string-timer/${file}/${path.node.loc?.start.line}`,
|
|
1044
|
+
severity: "high",
|
|
1045
|
+
type: "UNSAFE_EVAL",
|
|
1046
|
+
message: `${callee.name} with string argument executes code like eval()`,
|
|
1047
|
+
file,
|
|
1048
|
+
line: path.node.loc?.start.line,
|
|
1049
|
+
fix: "Pass a function instead of a string"
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (callee.type === "Identifier" && (callee.name === "exec" || callee.name === "execSync")) {
|
|
1054
|
+
const firstArg = path.node.arguments[0];
|
|
1055
|
+
if (firstArg && !this.isStaticString(firstArg)) {
|
|
1056
|
+
issues.push({
|
|
1057
|
+
id: `security/command-injection/${file}/${path.node.loc?.start.line}`,
|
|
1058
|
+
severity: "high",
|
|
1059
|
+
type: "COMMAND_INJECTION",
|
|
1060
|
+
message: "exec with dynamic command - potential command injection",
|
|
1061
|
+
file,
|
|
1062
|
+
line: path.node.loc?.start.line,
|
|
1063
|
+
fix: "Validate/sanitize input or use execFile with explicit arguments"
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "process" && callee.property.type === "Identifier" && callee.property.name === "on") {
|
|
1068
|
+
const firstArg = path.node.arguments[0];
|
|
1069
|
+
if (firstArg && firstArg.type === "StringLiteral") {
|
|
1070
|
+
if (firstArg.value === "SIGTERM" || firstArg.value === "SIGINT") {
|
|
1071
|
+
fileContext.hasSignalHandler = true;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}, "CallExpression"),
|
|
1076
|
+
// Detect fs operations with dynamic paths
|
|
1077
|
+
MemberExpression: /* @__PURE__ */ __name((path) => {
|
|
1078
|
+
const node = path.node;
|
|
1079
|
+
if (node.object.type === "Identifier" && (node.object.name === "fs" || node.object.name === "fsp")) {
|
|
1080
|
+
const parent = path.parentPath;
|
|
1081
|
+
if (parent.isCallExpression()) {
|
|
1082
|
+
const methodName = node.property.type === "Identifier" ? node.property.name : node.property.value;
|
|
1083
|
+
const pathMethods = [
|
|
1084
|
+
"readFile",
|
|
1085
|
+
"readFileSync",
|
|
1086
|
+
"writeFile",
|
|
1087
|
+
"writeFileSync",
|
|
1088
|
+
"readdir",
|
|
1089
|
+
"readdirSync",
|
|
1090
|
+
"stat",
|
|
1091
|
+
"statSync",
|
|
1092
|
+
"unlink",
|
|
1093
|
+
"unlinkSync",
|
|
1094
|
+
"mkdir",
|
|
1095
|
+
"mkdirSync",
|
|
1096
|
+
"rmdir",
|
|
1097
|
+
"rmdirSync",
|
|
1098
|
+
"access",
|
|
1099
|
+
"accessSync"
|
|
1100
|
+
];
|
|
1101
|
+
if (pathMethods.includes(methodName)) {
|
|
1102
|
+
const firstArg = parent.node.arguments[0];
|
|
1103
|
+
if (firstArg && !this.isStaticPath(firstArg)) {
|
|
1104
|
+
issues.push({
|
|
1105
|
+
id: `security/path-traversal/${file}/${path.node.loc?.start.line}`,
|
|
1106
|
+
severity: "high",
|
|
1107
|
+
type: "PATH_TRAVERSAL",
|
|
1108
|
+
message: `fs.${methodName} with dynamic path - potential path traversal`,
|
|
1109
|
+
file,
|
|
1110
|
+
line: path.node.loc?.start.line,
|
|
1111
|
+
fix: "Validate paths against workspace root before use"
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}, "MemberExpression"),
|
|
1118
|
+
// Check for dangerous regex patterns
|
|
1119
|
+
NewExpression: /* @__PURE__ */ __name((path) => {
|
|
1120
|
+
if (path.node.callee.type === "Identifier" && path.node.callee.name === "RegExp") {
|
|
1121
|
+
const firstArg = path.node.arguments[0];
|
|
1122
|
+
if (firstArg && !this.isStaticString(firstArg)) {
|
|
1123
|
+
issues.push({
|
|
1124
|
+
id: `security/unsafe-regex/${file}/${path.node.loc?.start.line}`,
|
|
1125
|
+
severity: "medium",
|
|
1126
|
+
type: "UNSAFE_REGEX",
|
|
1127
|
+
message: "Dynamic RegExp - potential ReDoS or injection vulnerability",
|
|
1128
|
+
file,
|
|
1129
|
+
line: path.node.loc?.start.line,
|
|
1130
|
+
fix: "Use static regex patterns or validate input"
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}, "NewExpression"),
|
|
1135
|
+
// Check for innerHTML/dangerouslySetInnerHTML (XSS)
|
|
1136
|
+
JSXAttribute: /* @__PURE__ */ __name((path) => {
|
|
1137
|
+
const name = path.node.name;
|
|
1138
|
+
if (name.type === "JSXIdentifier" && name.name === "dangerouslySetInnerHTML") {
|
|
1139
|
+
issues.push({
|
|
1140
|
+
id: `security/xss-risk/${file}/${path.node.loc?.start.line}`,
|
|
1141
|
+
severity: "high",
|
|
1142
|
+
type: "XSS_RISK",
|
|
1143
|
+
message: "dangerouslySetInnerHTML can lead to XSS if content is not sanitized",
|
|
1144
|
+
file,
|
|
1145
|
+
line: path.node.loc?.start.line,
|
|
1146
|
+
fix: "Sanitize HTML content before rendering or avoid using dangerouslySetInnerHTML"
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}, "JSXAttribute"),
|
|
1150
|
+
// Check for hardcoded secrets in variable declarations
|
|
1151
|
+
VariableDeclarator: /* @__PURE__ */ __name((path) => {
|
|
1152
|
+
const id = path.node.id;
|
|
1153
|
+
const init = path.node.init;
|
|
1154
|
+
if (id.type === "Identifier" && init) {
|
|
1155
|
+
this.checkForHardcodedSecret(id.name, init, file, path.node.loc?.start.line, issues);
|
|
1156
|
+
}
|
|
1157
|
+
}, "VariableDeclarator"),
|
|
1158
|
+
// Check for hardcoded secrets in class properties
|
|
1159
|
+
ClassProperty: /* @__PURE__ */ __name((path) => {
|
|
1160
|
+
const key = path.node.key;
|
|
1161
|
+
const value = path.node.value;
|
|
1162
|
+
if (key.type === "Identifier" && value) {
|
|
1163
|
+
this.checkForHardcodedSecret(key.name, value, file, path.node.loc?.start.line, issues);
|
|
1164
|
+
}
|
|
1165
|
+
}, "ClassProperty"),
|
|
1166
|
+
// After traversal is complete, check daemon-specific patterns
|
|
1167
|
+
Program: {
|
|
1168
|
+
exit: /* @__PURE__ */ __name(() => {
|
|
1169
|
+
if (fileContext.isDaemon && !fileContext.hasSignalHandler) {
|
|
1170
|
+
issues.push({
|
|
1171
|
+
id: `security/signal-handler/${file}`,
|
|
1172
|
+
severity: "high",
|
|
1173
|
+
type: "MISSING_SIGNAL_HANDLER",
|
|
1174
|
+
message: "Daemon/server missing signal handlers (SIGTERM/SIGINT)",
|
|
1175
|
+
file,
|
|
1176
|
+
fix: "Add process.on('SIGTERM', gracefulShutdown) for clean shutdown"
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}, "exit")
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
return {
|
|
1183
|
+
issues,
|
|
1184
|
+
nodesVisited
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Check if expression is a static string (safe)
|
|
1189
|
+
*/
|
|
1190
|
+
isStaticString(node) {
|
|
1191
|
+
if (node.type === "StringLiteral") return true;
|
|
1192
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return true;
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Check if expression is a static path (safe)
|
|
1197
|
+
*/
|
|
1198
|
+
isStaticPath(node) {
|
|
1199
|
+
if (node.type === "StringLiteral") return true;
|
|
1200
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return true;
|
|
1201
|
+
if (node.type === "CallExpression") {
|
|
1202
|
+
const callee = node.callee;
|
|
1203
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && callee.property.name === "join") {
|
|
1204
|
+
return node.arguments.every((arg) => {
|
|
1205
|
+
if (arg.type === "StringLiteral") return true;
|
|
1206
|
+
if (arg.type === "Identifier" && (arg.name === "__dirname" || arg.name === "__filename")) return true;
|
|
1207
|
+
return false;
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Check if a value looks like a hardcoded secret
|
|
1215
|
+
*/
|
|
1216
|
+
checkForHardcodedSecret(name, value, file, line, issues) {
|
|
1217
|
+
if (!value) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const varName = name.toLowerCase();
|
|
1221
|
+
const secretIndicators = [
|
|
1222
|
+
"apikey",
|
|
1223
|
+
"api_key",
|
|
1224
|
+
"secret",
|
|
1225
|
+
"password",
|
|
1226
|
+
"token",
|
|
1227
|
+
"credential",
|
|
1228
|
+
"auth",
|
|
1229
|
+
"key"
|
|
1230
|
+
];
|
|
1231
|
+
if (secretIndicators.some((s) => varName.includes(s))) {
|
|
1232
|
+
if (value.type === "StringLiteral" && value.value.length > 8) {
|
|
1233
|
+
const valueStr = value.value.toLowerCase();
|
|
1234
|
+
if (!valueStr.includes("placeholder") && !valueStr.includes("example") && !valueStr.includes("xxx") && !valueStr.includes("todo") && !valueStr.includes("your_") && !valueStr.includes("env.")) {
|
|
1235
|
+
issues.push({
|
|
1236
|
+
id: `security/hardcoded-secret/${file}/${line}`,
|
|
1237
|
+
severity: "critical",
|
|
1238
|
+
type: "HARDCODED_SECRET",
|
|
1239
|
+
message: `Possible hardcoded secret in "${name}"`,
|
|
1240
|
+
file,
|
|
1241
|
+
line,
|
|
1242
|
+
fix: "Use environment variables for secrets"
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// ../../packages/core/dist/analysis/static/OrphanDetector.js
|
|
1251
|
+
var DEFAULT_OPTIONS = {
|
|
1252
|
+
fileExtensions: [
|
|
1253
|
+
"ts",
|
|
1254
|
+
"tsx",
|
|
1255
|
+
"js",
|
|
1256
|
+
"jsx"
|
|
1257
|
+
],
|
|
1258
|
+
excludePatterns: [
|
|
1259
|
+
"node_modules",
|
|
1260
|
+
"dist",
|
|
1261
|
+
".next",
|
|
1262
|
+
"coverage",
|
|
1263
|
+
"**/*.test.*",
|
|
1264
|
+
"**/*.spec.*",
|
|
1265
|
+
"**/__tests__/**",
|
|
1266
|
+
"**/__mocks__/**"
|
|
1267
|
+
]
|
|
1268
|
+
};
|
|
1269
|
+
async function detectOrphans(entryPoint, options = {}) {
|
|
1270
|
+
const startTime = Date.now();
|
|
1271
|
+
const mergedOptions = {
|
|
1272
|
+
...DEFAULT_OPTIONS,
|
|
1273
|
+
...options
|
|
1274
|
+
};
|
|
1275
|
+
try {
|
|
1276
|
+
const madgeModule = await import('madge');
|
|
1277
|
+
const madge = madgeModule.default || madgeModule;
|
|
1278
|
+
const result = await madge(entryPoint, {
|
|
1279
|
+
fileExtensions: mergedOptions.fileExtensions,
|
|
1280
|
+
excludeRegExp: mergedOptions.excludePatterns.map((p) => {
|
|
1281
|
+
const regexPattern = p.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\./g, "\\.");
|
|
1282
|
+
return new RegExp(regexPattern);
|
|
1283
|
+
}),
|
|
1284
|
+
tsConfig: mergedOptions.tsConfigPath,
|
|
1285
|
+
detectiveOptions: {
|
|
1286
|
+
ts: {
|
|
1287
|
+
skipTypeImports: true
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
const orphans = result.orphans();
|
|
1292
|
+
const allFiles = Object.keys(result.obj());
|
|
1293
|
+
return {
|
|
1294
|
+
orphans,
|
|
1295
|
+
totalFiles: allFiles.length,
|
|
1296
|
+
success: true,
|
|
1297
|
+
duration: Date.now() - startTime
|
|
1298
|
+
};
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
return {
|
|
1301
|
+
orphans: [],
|
|
1302
|
+
totalFiles: 0,
|
|
1303
|
+
success: false,
|
|
1304
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1305
|
+
duration: Date.now() - startTime
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
__name(detectOrphans, "detectOrphans");
|
|
1310
|
+
function filterOrphansToFiles(orphanResult, targetFiles) {
|
|
1311
|
+
if (!orphanResult.success) {
|
|
1312
|
+
return [];
|
|
1313
|
+
}
|
|
1314
|
+
const targetSet = new Set(targetFiles.map((f) => f.replace(/\\/g, "/")));
|
|
1315
|
+
return orphanResult.orphans.filter((orphan) => {
|
|
1316
|
+
const normalizedOrphan = orphan.replace(/\\/g, "/");
|
|
1317
|
+
return targetSet.has(normalizedOrphan) || targetFiles.some((t) => normalizedOrphan.endsWith(t));
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
__name(filterOrphansToFiles, "filterOrphansToFiles");
|
|
1321
|
+
async function checkFilesForOrphanStatus(files, workspaceRoot) {
|
|
1322
|
+
const result = await detectOrphans(workspaceRoot, {
|
|
1323
|
+
baseDir: workspaceRoot
|
|
1324
|
+
});
|
|
1325
|
+
if (!result.success) {
|
|
1326
|
+
return {
|
|
1327
|
+
orphans: [],
|
|
1328
|
+
success: false,
|
|
1329
|
+
error: result.error
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const orphans = filterOrphansToFiles(result, files);
|
|
1333
|
+
return {
|
|
1334
|
+
orphans,
|
|
1335
|
+
success: true
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
__name(checkFilesForOrphanStatus, "checkFilesForOrphanStatus");
|
|
1339
|
+
|
|
1340
|
+
// ../../packages/core/dist/analysis/static/index.js
|
|
1341
|
+
async function runStaticAnalysis(files, _workspaceRoot, options = {}) {
|
|
1342
|
+
const startTime = Date.now();
|
|
1343
|
+
const result = {
|
|
1344
|
+
skippedTests: [],
|
|
1345
|
+
orphanedFiles: [],
|
|
1346
|
+
duration: 0,
|
|
1347
|
+
success: true,
|
|
1348
|
+
errors: []
|
|
1349
|
+
};
|
|
1350
|
+
if (!options.skipTestDetection) {
|
|
1351
|
+
try {
|
|
1352
|
+
const { analyzeSkippedTests: analyzeSkippedTests2 } = await import('./SkippedTestDetector-JY4EF5BN.js');
|
|
1353
|
+
const testResults = analyzeSkippedTests2(files);
|
|
1354
|
+
for (const testResult of testResults) {
|
|
1355
|
+
if (!testResult.parsed && testResult.error) {
|
|
1356
|
+
result.errors.push(`Parse error in ${testResult.file}: ${testResult.error}`);
|
|
1357
|
+
}
|
|
1358
|
+
for (const skipped of testResult.skipped) {
|
|
1359
|
+
result.skippedTests.push({
|
|
1360
|
+
file: skipped.file,
|
|
1361
|
+
type: skipped.type,
|
|
1362
|
+
name: skipped.name,
|
|
1363
|
+
line: skipped.line
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
result.errors.push(`Skipped test detection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
if (!options.skipOrphanDetection) ;
|
|
1372
|
+
result.duration = Date.now() - startTime;
|
|
1373
|
+
result.success = result.errors.length === 0;
|
|
1374
|
+
return result;
|
|
1375
|
+
}
|
|
1376
|
+
__name(runStaticAnalysis, "runStaticAnalysis");
|
|
1377
|
+
|
|
1378
|
+
export { ChangeImpactAnalyzer, CompletenessAnalyzer, SecurityAnalyzer, SyntaxAnalyzer, checkFilesForOrphanStatus, createChangeImpactAnalyzer, detectOrphans, filterOrphansToFiles, runStaticAnalysis };
|
|
1379
|
+
//# sourceMappingURL=chunk-VSJ33PLA.js.map
|
|
1380
|
+
//# sourceMappingURL=chunk-VSJ33PLA.js.map
|