@vibecheckai/cli 3.3.0 → 3.5.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/bin/registry.js +389 -269
- package/bin/runners/cli-utils.js +2 -33
- package/bin/runners/context/generators/cursor.js +49 -2
- package/bin/runners/lib/agent-firewall/learning/learning-engine.js +849 -0
- package/bin/runners/lib/analyzers.js +599 -142
- package/bin/runners/lib/audit-logger.js +532 -0
- package/bin/runners/lib/authority/authorities/architecture.js +364 -0
- package/bin/runners/lib/authority/authorities/compliance.js +341 -0
- package/bin/runners/lib/authority/authorities/human.js +343 -0
- package/bin/runners/lib/authority/authorities/quality.js +420 -0
- package/bin/runners/lib/authority/authorities/security.js +228 -0
- package/bin/runners/lib/authority/index.js +293 -0
- package/bin/runners/lib/authority-badge.js +425 -425
- package/bin/runners/lib/bundle/bundle-intelligence.js +846 -0
- package/bin/runners/lib/cli-charts.js +368 -0
- package/bin/runners/lib/cli-config-display.js +405 -0
- package/bin/runners/lib/cli-demo.js +275 -0
- package/bin/runners/lib/cli-errors.js +438 -0
- package/bin/runners/lib/cli-help-formatter.js +439 -0
- package/bin/runners/lib/cli-interactive-menu.js +509 -0
- package/bin/runners/lib/cli-prompts.js +441 -0
- package/bin/runners/lib/cli-scan-cards.js +362 -0
- package/bin/runners/lib/compliance-reporter.js +710 -0
- package/bin/runners/lib/conductor/index.js +671 -0
- package/bin/runners/lib/easy/README.md +123 -0
- package/bin/runners/lib/easy/index.js +140 -0
- package/bin/runners/lib/easy/interactive-wizard.js +788 -0
- package/bin/runners/lib/easy/one-click-firewall.js +564 -0
- package/bin/runners/lib/easy/zero-config-reality.js +714 -0
- package/bin/runners/lib/engines/accessibility-engine.js +218 -18
- package/bin/runners/lib/engines/api-consistency-engine.js +335 -30
- package/bin/runners/lib/engines/async-patterns-engine.js +444 -0
- package/bin/runners/lib/engines/bundle-size-engine.js +433 -0
- package/bin/runners/lib/engines/confidence-scoring.js +276 -0
- package/bin/runners/lib/engines/context-detection.js +264 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +292 -27
- package/bin/runners/lib/engines/database-patterns-engine.js +429 -0
- package/bin/runners/lib/engines/duplicate-code-engine.js +354 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +127 -17
- package/bin/runners/lib/engines/env-variables-engine.js +458 -0
- package/bin/runners/lib/engines/error-handling-engine.js +437 -0
- package/bin/runners/lib/engines/false-positive-prevention.js +630 -0
- package/bin/runners/lib/engines/framework-adapters/index.js +607 -0
- package/bin/runners/lib/engines/framework-detection.js +508 -0
- package/bin/runners/lib/engines/import-order-engine.js +429 -0
- package/bin/runners/lib/engines/mock-data-engine.js +53 -10
- package/bin/runners/lib/engines/naming-conventions-engine.js +544 -0
- package/bin/runners/lib/engines/noise-reduction-engine.js +452 -0
- package/bin/runners/lib/engines/orchestrator.js +334 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +176 -36
- package/bin/runners/lib/engines/react-patterns-engine.js +457 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +382 -54
- package/bin/runners/lib/engines/type-aware-engine.js +263 -39
- package/bin/runners/lib/engines/vibecheck-engines/index.js +122 -13
- package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +806 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +373 -73
- package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +577 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +543 -0
- package/bin/runners/lib/engines/vibecheck-engines.js +514 -0
- package/bin/runners/lib/enhanced-features/index.js +305 -0
- package/bin/runners/lib/enhanced-output.js +631 -0
- package/bin/runners/lib/enterprise.js +300 -0
- package/bin/runners/lib/entitlements-v2.js +161 -478
- package/bin/runners/lib/firewall/command-validator.js +351 -0
- package/bin/runners/lib/firewall/config.js +341 -0
- package/bin/runners/lib/firewall/content-validator.js +519 -0
- package/bin/runners/lib/firewall/index.js +101 -0
- package/bin/runners/lib/firewall/path-validator.js +256 -0
- package/bin/runners/lib/html-proof-report.js +350 -700
- package/bin/runners/lib/intelligence/cross-repo-intelligence.js +817 -0
- package/bin/runners/lib/mcp-utils.js +425 -0
- package/bin/runners/lib/missions/plan.js +46 -6
- package/bin/runners/lib/missions/templates.js +232 -0
- package/bin/runners/lib/output/index.js +1022 -0
- package/bin/runners/lib/policy-engine.js +652 -0
- package/bin/runners/lib/polish/autofix/accessibility-fixes.js +333 -0
- package/bin/runners/lib/polish/autofix/async-handlers.js +273 -0
- package/bin/runners/lib/polish/autofix/dead-code.js +280 -0
- package/bin/runners/lib/polish/autofix/imports-optimizer.js +344 -0
- package/bin/runners/lib/polish/autofix/index.js +200 -0
- package/bin/runners/lib/polish/autofix/remove-consoles.js +209 -0
- package/bin/runners/lib/polish/autofix/strengthen-types.js +245 -0
- package/bin/runners/lib/polish/backend-checks.js +148 -0
- package/bin/runners/lib/polish/documentation-checks.js +111 -0
- package/bin/runners/lib/polish/frontend-checks.js +168 -0
- package/bin/runners/lib/polish/index.js +71 -0
- package/bin/runners/lib/polish/infrastructure-checks.js +131 -0
- package/bin/runners/lib/polish/library-detection.js +175 -0
- package/bin/runners/lib/polish/performance-checks.js +100 -0
- package/bin/runners/lib/polish/security-checks.js +148 -0
- package/bin/runners/lib/polish/utils.js +203 -0
- package/bin/runners/lib/prompt-builder.js +540 -0
- package/bin/runners/lib/proof-certificate.js +634 -0
- package/bin/runners/lib/reality/accessibility-audit.js +946 -0
- package/bin/runners/lib/reality/api-contract-validator.js +1012 -0
- package/bin/runners/lib/reality/chaos-engineering.js +1084 -0
- package/bin/runners/lib/reality/performance-tracker.js +1077 -0
- package/bin/runners/lib/reality/scenario-generator.js +1404 -0
- package/bin/runners/lib/reality/visual-regression.js +852 -0
- package/bin/runners/lib/reality-profiler.js +717 -0
- package/bin/runners/lib/replay/flight-recorder-viewer.js +1160 -0
- package/bin/runners/lib/review/ai-code-review.js +832 -0
- package/bin/runners/lib/rules/custom-rule-engine.js +985 -0
- package/bin/runners/lib/sbom-generator.js +641 -0
- package/bin/runners/lib/scan-output-enhanced.js +512 -0
- package/bin/runners/lib/scan-output.js +65 -19
- package/bin/runners/lib/security/owasp-scanner.js +939 -0
- package/bin/runners/lib/ship-output.js +18 -25
- package/bin/runners/lib/terminal-ui.js +113 -1
- package/bin/runners/lib/unified-cli-output.js +603 -430
- package/bin/runners/lib/upsell.js +90 -338
- package/bin/runners/lib/validators/contract-validator.js +283 -0
- package/bin/runners/lib/validators/dead-export-detector.js +279 -0
- package/bin/runners/lib/validators/dep-audit.js +245 -0
- package/bin/runners/lib/validators/env-validator.js +319 -0
- package/bin/runners/lib/validators/index.js +120 -0
- package/bin/runners/lib/validators/license-checker.js +252 -0
- package/bin/runners/lib/validators/route-validator.js +290 -0
- package/bin/runners/runAIAgent.js +5 -10
- package/bin/runners/runAgent.js +3 -0
- package/bin/runners/runApprove.js +1233 -1200
- package/bin/runners/runAuth.js +22 -1
- package/bin/runners/runAuthority.js +528 -0
- package/bin/runners/runCheckpoint.js +4 -24
- package/bin/runners/runClassify.js +862 -859
- package/bin/runners/runConductor.js +772 -0
- package/bin/runners/runContainer.js +366 -0
- package/bin/runners/runContext.js +3 -0
- package/bin/runners/runDoctor.js +28 -41
- package/bin/runners/runEasy.js +410 -0
- package/bin/runners/runFirewall.js +3 -0
- package/bin/runners/runFirewallHook.js +3 -0
- package/bin/runners/runFix.js +76 -66
- package/bin/runners/runGuard.js +411 -18
- package/bin/runners/runIaC.js +372 -0
- package/bin/runners/runInit.js +10 -60
- package/bin/runners/runMcp.js +11 -12
- package/bin/runners/runPolish.js +240 -64
- package/bin/runners/runPromptFirewall.js +5 -12
- package/bin/runners/runProve.js +20 -55
- package/bin/runners/runReality.js +68 -59
- package/bin/runners/runReport.js +31 -5
- package/bin/runners/runRuntime.js +5 -8
- package/bin/runners/runScan.js +194 -1273
- package/bin/runners/runShip.js +695 -47
- package/bin/runners/runTruth.js +3 -0
- package/bin/runners/runValidate.js +7 -11
- package/bin/runners/runVibe.js +791 -0
- package/bin/runners/runWatch.js +14 -23
- package/bin/vibecheck.js +179 -65
- package/mcp-server/index.js +202 -636
- package/mcp-server/lib/api-client.cjs +7 -299
- package/mcp-server/package.json +1 -1
- package/mcp-server/tier-auth.js +175 -574
- package/mcp-server/tools-v3.js +800 -505
- package/mcp-server/tools.js +495 -0
- package/package.json +1 -1
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
- package/mcp-server/index-v1.js +0 -698
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate Code Detection Engine
|
|
3
|
+
* Detects:
|
|
4
|
+
* - Exact duplicate code blocks
|
|
5
|
+
* - Similar code patterns (near-duplicates)
|
|
6
|
+
* - Copy-paste code between files
|
|
7
|
+
* - Repeated utility functions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { getAST } = require("./ast-cache");
|
|
11
|
+
const traverse = require("@babel/traverse").default;
|
|
12
|
+
const t = require("@babel/types");
|
|
13
|
+
const crypto = require("crypto");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimum lines for a duplicate to be reported
|
|
17
|
+
*/
|
|
18
|
+
const MIN_DUPLICATE_LINES = 5;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Minimum tokens for a duplicate to be considered significant
|
|
22
|
+
*/
|
|
23
|
+
const MIN_DUPLICATE_TOKENS = 20;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Similarity threshold for near-duplicates (0-1)
|
|
27
|
+
*/
|
|
28
|
+
const SIMILARITY_THRESHOLD = 0.8;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize code by removing variable names and literals
|
|
32
|
+
*/
|
|
33
|
+
function normalizeCode(code) {
|
|
34
|
+
// Remove comments
|
|
35
|
+
code = code.replace(/\/\/.*$/gm, "");
|
|
36
|
+
code = code.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
37
|
+
|
|
38
|
+
// Normalize whitespace
|
|
39
|
+
code = code.replace(/\s+/g, " ").trim();
|
|
40
|
+
|
|
41
|
+
// Remove string contents (keep quotes)
|
|
42
|
+
code = code.replace(/"[^"]*"/g, '""');
|
|
43
|
+
code = code.replace(/'[^']*'/g, "''");
|
|
44
|
+
code = code.replace(/`[^`]*`/g, "``");
|
|
45
|
+
|
|
46
|
+
// Normalize numbers
|
|
47
|
+
code = code.replace(/\b\d+\.?\d*\b/g, "0");
|
|
48
|
+
|
|
49
|
+
return code;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tokenize code for comparison
|
|
54
|
+
*/
|
|
55
|
+
function tokenize(code) {
|
|
56
|
+
const normalized = normalizeCode(code);
|
|
57
|
+
|
|
58
|
+
// Split into tokens
|
|
59
|
+
const tokens = normalized.split(/(\s+|[{}()\[\];,.])/g)
|
|
60
|
+
.filter(t => t.trim().length > 0);
|
|
61
|
+
|
|
62
|
+
return tokens;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Calculate hash of normalized code block
|
|
67
|
+
*/
|
|
68
|
+
function hashCodeBlock(code) {
|
|
69
|
+
const normalized = normalizeCode(code);
|
|
70
|
+
return crypto.createHash("md5").update(normalized).digest("hex");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Calculate similarity between two token arrays using Jaccard similarity
|
|
75
|
+
*/
|
|
76
|
+
function calculateSimilarity(tokens1, tokens2) {
|
|
77
|
+
if (tokens1.length === 0 || tokens2.length === 0) return 0;
|
|
78
|
+
|
|
79
|
+
const set1 = new Set(tokens1);
|
|
80
|
+
const set2 = new Set(tokens2);
|
|
81
|
+
|
|
82
|
+
let intersection = 0;
|
|
83
|
+
for (const token of set1) {
|
|
84
|
+
if (set2.has(token)) intersection++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const union = set1.size + set2.size - intersection;
|
|
88
|
+
return intersection / union;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract significant code blocks from AST
|
|
93
|
+
*/
|
|
94
|
+
function extractCodeBlocks(code, filePath) {
|
|
95
|
+
const ast = getAST(code, filePath);
|
|
96
|
+
if (!ast) return [];
|
|
97
|
+
|
|
98
|
+
const blocks = [];
|
|
99
|
+
const lines = code.split("\n");
|
|
100
|
+
|
|
101
|
+
traverse(ast, {
|
|
102
|
+
// Function declarations and expressions
|
|
103
|
+
FunctionDeclaration(path) {
|
|
104
|
+
extractBlock(path, blocks, code, lines, "function");
|
|
105
|
+
},
|
|
106
|
+
FunctionExpression(path) {
|
|
107
|
+
// Only capture if it's a standalone function (not inline callback)
|
|
108
|
+
if (path.parentPath.isVariableDeclarator()) {
|
|
109
|
+
extractBlock(path, blocks, code, lines, "function");
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
ArrowFunctionExpression(path) {
|
|
113
|
+
// Only capture if it's a standalone function
|
|
114
|
+
if (path.parentPath.isVariableDeclarator()) {
|
|
115
|
+
const body = path.node.body;
|
|
116
|
+
// Only capture functions with block body (not one-liners)
|
|
117
|
+
if (t.isBlockStatement(body)) {
|
|
118
|
+
extractBlock(path, blocks, code, lines, "arrow");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// Class methods
|
|
124
|
+
ClassMethod(path) {
|
|
125
|
+
extractBlock(path, blocks, code, lines, "method");
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// If statements with significant bodies
|
|
129
|
+
IfStatement(path) {
|
|
130
|
+
const bodyLines = getBlockLineCount(path.node);
|
|
131
|
+
if (bodyLines >= MIN_DUPLICATE_LINES) {
|
|
132
|
+
extractBlock(path, blocks, code, lines, "if-block");
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Switch statements
|
|
137
|
+
SwitchStatement(path) {
|
|
138
|
+
const bodyLines = getBlockLineCount(path.node);
|
|
139
|
+
if (bodyLines >= MIN_DUPLICATE_LINES) {
|
|
140
|
+
extractBlock(path, blocks, code, lines, "switch");
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return blocks;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get line count of a block
|
|
150
|
+
*/
|
|
151
|
+
function getBlockLineCount(node) {
|
|
152
|
+
if (!node.loc) return 0;
|
|
153
|
+
return node.loc.end.line - node.loc.start.line + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract a code block with metadata
|
|
158
|
+
*/
|
|
159
|
+
function extractBlock(path, blocks, code, lines, type) {
|
|
160
|
+
const node = path.node;
|
|
161
|
+
if (!node.loc) return;
|
|
162
|
+
|
|
163
|
+
const startLine = node.loc.start.line;
|
|
164
|
+
const endLine = node.loc.end.line;
|
|
165
|
+
const lineCount = endLine - startLine + 1;
|
|
166
|
+
|
|
167
|
+
// Skip small blocks
|
|
168
|
+
if (lineCount < MIN_DUPLICATE_LINES) return;
|
|
169
|
+
|
|
170
|
+
const blockCode = code.substring(node.start, node.end);
|
|
171
|
+
const tokens = tokenize(blockCode);
|
|
172
|
+
|
|
173
|
+
// Skip blocks with too few tokens
|
|
174
|
+
if (tokens.length < MIN_DUPLICATE_TOKENS) return;
|
|
175
|
+
|
|
176
|
+
// Get function/method name if available
|
|
177
|
+
let name = "<anonymous>";
|
|
178
|
+
if (node.id?.name) {
|
|
179
|
+
name = node.id.name;
|
|
180
|
+
} else if (node.key?.name) {
|
|
181
|
+
name = node.key.name;
|
|
182
|
+
} else if (path.parentPath?.isVariableDeclarator()) {
|
|
183
|
+
const declId = path.parentPath.node.id;
|
|
184
|
+
if (t.isIdentifier(declId)) {
|
|
185
|
+
name = declId.name;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
blocks.push({
|
|
190
|
+
type,
|
|
191
|
+
name,
|
|
192
|
+
startLine,
|
|
193
|
+
endLine,
|
|
194
|
+
lineCount,
|
|
195
|
+
code: blockCode,
|
|
196
|
+
tokens,
|
|
197
|
+
hash: hashCodeBlock(blockCode),
|
|
198
|
+
snippet: lines[startLine - 1]?.trim() || "",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Find duplicates within a single file
|
|
204
|
+
*/
|
|
205
|
+
function findDuplicatesInFile(code, filePath) {
|
|
206
|
+
const findings = [];
|
|
207
|
+
const blocks = extractCodeBlocks(code, filePath);
|
|
208
|
+
|
|
209
|
+
// Group by hash for exact duplicates
|
|
210
|
+
const hashGroups = new Map();
|
|
211
|
+
for (const block of blocks) {
|
|
212
|
+
if (!hashGroups.has(block.hash)) {
|
|
213
|
+
hashGroups.set(block.hash, []);
|
|
214
|
+
}
|
|
215
|
+
hashGroups.get(block.hash).push(block);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Report exact duplicates
|
|
219
|
+
for (const [hash, group] of hashGroups) {
|
|
220
|
+
if (group.length > 1) {
|
|
221
|
+
const first = group[0];
|
|
222
|
+
const locations = group.map(b => `line ${b.startLine}`).join(", ");
|
|
223
|
+
|
|
224
|
+
findings.push({
|
|
225
|
+
type: "exact_duplicate",
|
|
226
|
+
severity: "WARN",
|
|
227
|
+
category: "CodeQuality",
|
|
228
|
+
file: filePath,
|
|
229
|
+
line: first.startLine,
|
|
230
|
+
column: 0,
|
|
231
|
+
title: `Duplicate code block (${first.lineCount} lines, ${group.length} occurrences)`,
|
|
232
|
+
message: `Identical code found at: ${locations}. Consider extracting to a shared function.`,
|
|
233
|
+
codeSnippet: first.snippet,
|
|
234
|
+
confidence: "high",
|
|
235
|
+
fixHint: "Extract duplicate code into a reusable function",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Find near-duplicates (similar but not identical)
|
|
241
|
+
const reported = new Set();
|
|
242
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
243
|
+
for (let j = i + 1; j < blocks.length; j++) {
|
|
244
|
+
const block1 = blocks[i];
|
|
245
|
+
const block2 = blocks[j];
|
|
246
|
+
|
|
247
|
+
// Skip if already matched as exact duplicate
|
|
248
|
+
if (block1.hash === block2.hash) continue;
|
|
249
|
+
|
|
250
|
+
// Skip if already reported
|
|
251
|
+
const pairKey = `${block1.startLine}-${block2.startLine}`;
|
|
252
|
+
if (reported.has(pairKey)) continue;
|
|
253
|
+
|
|
254
|
+
const similarity = calculateSimilarity(block1.tokens, block2.tokens);
|
|
255
|
+
|
|
256
|
+
if (similarity >= SIMILARITY_THRESHOLD) {
|
|
257
|
+
reported.add(pairKey);
|
|
258
|
+
|
|
259
|
+
findings.push({
|
|
260
|
+
type: "similar_code",
|
|
261
|
+
severity: "INFO",
|
|
262
|
+
category: "CodeQuality",
|
|
263
|
+
file: filePath,
|
|
264
|
+
line: block1.startLine,
|
|
265
|
+
column: 0,
|
|
266
|
+
title: `Similar code blocks (${Math.round(similarity * 100)}% similar)`,
|
|
267
|
+
message: `'${block1.name}' (line ${block1.startLine}) and '${block2.name}' (line ${block2.startLine}) have similar structure.`,
|
|
268
|
+
codeSnippet: block1.snippet,
|
|
269
|
+
confidence: "med",
|
|
270
|
+
fixHint: "Consider refactoring to share common logic",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return findings;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Find duplicates across multiple files
|
|
281
|
+
*/
|
|
282
|
+
function findDuplicatesAcrossFiles(files, repoRoot) {
|
|
283
|
+
const fs = require("fs");
|
|
284
|
+
const path = require("path");
|
|
285
|
+
const findings = [];
|
|
286
|
+
const allBlocks = new Map(); // hash -> [{ file, block }]
|
|
287
|
+
|
|
288
|
+
// Collect blocks from all files
|
|
289
|
+
for (const fileAbs of files) {
|
|
290
|
+
try {
|
|
291
|
+
const code = fs.readFileSync(fileAbs, "utf8");
|
|
292
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
293
|
+
const blocks = extractCodeBlocks(code, fileRel);
|
|
294
|
+
|
|
295
|
+
for (const block of blocks) {
|
|
296
|
+
if (!allBlocks.has(block.hash)) {
|
|
297
|
+
allBlocks.set(block.hash, []);
|
|
298
|
+
}
|
|
299
|
+
allBlocks.get(block.hash).push({ file: fileRel, block });
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
// Skip files that can't be read
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Find cross-file duplicates
|
|
308
|
+
for (const [hash, occurrences] of allBlocks) {
|
|
309
|
+
// Get unique files
|
|
310
|
+
const uniqueFiles = new Set(occurrences.map(o => o.file));
|
|
311
|
+
|
|
312
|
+
if (uniqueFiles.size > 1) {
|
|
313
|
+
const first = occurrences[0];
|
|
314
|
+
const fileList = Array.from(uniqueFiles).slice(0, 5).join(", ");
|
|
315
|
+
const moreFiles = uniqueFiles.size > 5 ? ` and ${uniqueFiles.size - 5} more` : "";
|
|
316
|
+
|
|
317
|
+
findings.push({
|
|
318
|
+
type: "cross_file_duplicate",
|
|
319
|
+
severity: "WARN",
|
|
320
|
+
category: "CodeQuality",
|
|
321
|
+
file: first.file,
|
|
322
|
+
line: first.block.startLine,
|
|
323
|
+
column: 0,
|
|
324
|
+
title: `Duplicate code across ${uniqueFiles.size} files`,
|
|
325
|
+
message: `Same code block (${first.block.lineCount} lines) found in: ${fileList}${moreFiles}`,
|
|
326
|
+
codeSnippet: first.block.snippet,
|
|
327
|
+
confidence: "high",
|
|
328
|
+
fixHint: "Extract to a shared utility module",
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return findings;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Analyze duplicate code in a single file
|
|
338
|
+
*/
|
|
339
|
+
function analyzeDuplicateCode(code, filePath) {
|
|
340
|
+
return findDuplicatesInFile(code, filePath);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
analyzeDuplicateCode,
|
|
345
|
+
findDuplicatesInFile,
|
|
346
|
+
findDuplicatesAcrossFiles,
|
|
347
|
+
extractCodeBlocks,
|
|
348
|
+
calculateSimilarity,
|
|
349
|
+
normalizeCode,
|
|
350
|
+
tokenize,
|
|
351
|
+
hashCodeBlock,
|
|
352
|
+
MIN_DUPLICATE_LINES,
|
|
353
|
+
SIMILARITY_THRESHOLD,
|
|
354
|
+
};
|
|
@@ -1,12 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Empty Catch Block Detection Engine
|
|
3
3
|
* Uses AST analysis to detect empty or effectively empty catch blocks
|
|
4
|
+
* Enhanced with:
|
|
5
|
+
* - Intentional suppression comment detection
|
|
6
|
+
* - Structured logging detection (winston, pino, bunyan)
|
|
7
|
+
* - Error reporting service detection (Sentry, Bugsnag, Rollbar)
|
|
8
|
+
* - Recovery/cleanup pattern detection
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
const { getAST, parseCode } = require("./ast-cache");
|
|
7
12
|
const traverse = require("@babel/traverse").default;
|
|
8
13
|
const t = require("@babel/types");
|
|
9
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Comments that indicate intentional error suppression
|
|
17
|
+
*/
|
|
18
|
+
const INTENTIONAL_SUPPRESSION_PATTERNS = [
|
|
19
|
+
/intentionally?\s*(?:ignored?|suppress(?:ed)?|silent|swallow(?:ed)?)/i,
|
|
20
|
+
/error\s*(?:is\s*)?(?:expected|ok|fine|acceptable)/i,
|
|
21
|
+
/safe\s*to\s*ignore/i,
|
|
22
|
+
/this\s*(?:error|exception)\s*(?:can|is)\s*(?:safely\s*)?(?:be\s*)?ignored/i,
|
|
23
|
+
/ignore\s*(?:this\s*)?(?:error|exception)/i,
|
|
24
|
+
/suppress(?:ed)?\s*(?:error|exception)/i,
|
|
25
|
+
/best\s*effort/i,
|
|
26
|
+
/fallback/i,
|
|
27
|
+
/graceful(?:ly)?/i,
|
|
28
|
+
/optional/i,
|
|
29
|
+
/non-critical/i,
|
|
30
|
+
/not\s*(?:a\s*)?(?:critical|fatal)/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Structured logging calls (not console)
|
|
35
|
+
*/
|
|
36
|
+
const STRUCTURED_LOGGER_PATTERNS = [
|
|
37
|
+
// Logger method calls
|
|
38
|
+
/^logger\./i,
|
|
39
|
+
/^log\./i,
|
|
40
|
+
/^winston\./i,
|
|
41
|
+
/^pino\./i,
|
|
42
|
+
/^bunyan\./i,
|
|
43
|
+
/^console\.(error|warn)/i,
|
|
44
|
+
// Error tracking services
|
|
45
|
+
/Sentry\.capture/i,
|
|
46
|
+
/Bugsnag\.notify/i,
|
|
47
|
+
/Rollbar\.error/i,
|
|
48
|
+
/trackError/i,
|
|
49
|
+
/reportError/i,
|
|
50
|
+
/captureException/i,
|
|
51
|
+
/logError/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if code contains intentional suppression comment
|
|
56
|
+
*/
|
|
57
|
+
function hasIntentionalSuppressionComment(code, startLine, endLine, lines) {
|
|
58
|
+
// Check comments in and around the catch block
|
|
59
|
+
for (let i = Math.max(0, startLine - 2); i <= Math.min(lines.length - 1, endLine + 1); i++) {
|
|
60
|
+
const line = lines[i];
|
|
61
|
+
// Check for intentional suppression patterns in comments
|
|
62
|
+
if (INTENTIONAL_SUPPRESSION_PATTERNS.some(pattern => pattern.test(line))) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if expression is a structured logging call
|
|
71
|
+
*/
|
|
72
|
+
function isStructuredLoggingCall(expression, code) {
|
|
73
|
+
if (!t.isCallExpression(expression)) return false;
|
|
74
|
+
|
|
75
|
+
const callee = expression.callee;
|
|
76
|
+
let calleeStr = "";
|
|
77
|
+
|
|
78
|
+
// Build the callee string
|
|
79
|
+
if (t.isMemberExpression(callee)) {
|
|
80
|
+
if (t.isIdentifier(callee.object)) {
|
|
81
|
+
calleeStr = callee.object.name + ".";
|
|
82
|
+
}
|
|
83
|
+
if (t.isIdentifier(callee.property)) {
|
|
84
|
+
calleeStr += callee.property.name;
|
|
85
|
+
}
|
|
86
|
+
} else if (t.isIdentifier(callee)) {
|
|
87
|
+
calleeStr = callee.name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return STRUCTURED_LOGGER_PATTERNS.some(pattern => pattern.test(calleeStr));
|
|
91
|
+
}
|
|
92
|
+
|
|
10
93
|
/**
|
|
11
94
|
* Check if a catch block is effectively empty
|
|
12
95
|
*/
|
|
@@ -83,59 +166,86 @@ function analyzeEmptyCatch(code, filePath) {
|
|
|
83
166
|
|
|
84
167
|
if (!body) return;
|
|
85
168
|
|
|
169
|
+
const startLine = catchNode.loc.start.line;
|
|
170
|
+
const endLine = catchNode.loc.end.line;
|
|
171
|
+
|
|
172
|
+
// Check for intentional suppression comments first
|
|
173
|
+
if (hasIntentionalSuppressionComment(code, startLine - 1, endLine - 1, lines)) {
|
|
174
|
+
// Has intentional suppression comment - skip or low severity
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
86
178
|
// Check if completely empty
|
|
87
179
|
if (isEmptyCatchBlock(body)) {
|
|
88
|
-
const line = catchNode.loc.start.line;
|
|
89
180
|
findings.push({
|
|
90
181
|
type: "empty_catch",
|
|
91
182
|
severity: "WARN",
|
|
92
183
|
category: "EmptyCatch",
|
|
93
184
|
file: filePath,
|
|
94
|
-
line,
|
|
185
|
+
line: startLine,
|
|
95
186
|
column: catchNode.loc.start.column,
|
|
96
187
|
title: "Empty catch block",
|
|
97
188
|
message: "Catch block contains no error handling code",
|
|
98
|
-
codeSnippet: lines[
|
|
189
|
+
codeSnippet: lines[startLine - 1]?.trim(),
|
|
99
190
|
confidence: "high",
|
|
191
|
+
fixHint: "Add error handling, logging, or a comment explaining why the error is intentionally ignored",
|
|
100
192
|
});
|
|
101
193
|
return;
|
|
102
194
|
}
|
|
103
195
|
|
|
104
|
-
// Check if only has comments - but allow console.error/warn
|
|
196
|
+
// Check if only has comments - but allow console.error/warn, structured logging, return/throw
|
|
105
197
|
const statements = body.body || [];
|
|
106
198
|
const hasRealHandling = statements.some(stmt => {
|
|
107
|
-
// Allow return statements
|
|
199
|
+
// Allow return statements (with or without value)
|
|
108
200
|
if (t.isReturnStatement(stmt)) return true;
|
|
109
|
-
|
|
201
|
+
|
|
202
|
+
// Allow throw statements (re-throwing or wrapping)
|
|
110
203
|
if (t.isThrowStatement(stmt)) return true;
|
|
111
|
-
|
|
204
|
+
|
|
205
|
+
// Allow structured logging and error reporting
|
|
112
206
|
if (t.isExpressionStatement(stmt) && t.isCallExpression(stmt.expression)) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
t.isIdentifier(callee.object, { name: "console" }) &&
|
|
116
|
-
t.isIdentifier(callee.property)) {
|
|
117
|
-
if (["error", "warn"].includes(callee.property.name)) {
|
|
118
|
-
return true; // This is acceptable error handling
|
|
119
|
-
}
|
|
207
|
+
if (isStructuredLoggingCall(stmt.expression, code)) {
|
|
208
|
+
return true;
|
|
120
209
|
}
|
|
121
210
|
}
|
|
211
|
+
|
|
212
|
+
// Allow await expressions (might be async error handling)
|
|
213
|
+
if (t.isExpressionStatement(stmt) && t.isAwaitExpression(stmt.expression)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Allow variable assignments (might be setting error state)
|
|
218
|
+
if (t.isExpressionStatement(stmt) && t.isAssignmentExpression(stmt.expression)) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Allow variable declarations (const error = ...)
|
|
223
|
+
if (t.isVariableDeclaration(stmt)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Allow if statements (conditional error handling)
|
|
228
|
+
if (t.isIfStatement(stmt)) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
122
232
|
return false;
|
|
123
233
|
});
|
|
124
234
|
|
|
125
235
|
// Only flag if it truly has no handling
|
|
126
236
|
if (!hasRealHandling && onlyHasComments(body, code)) {
|
|
127
|
-
const line = catchNode.loc.start.line;
|
|
128
237
|
findings.push({
|
|
129
238
|
type: "comment_only_catch",
|
|
130
239
|
severity: "WARN",
|
|
131
240
|
category: "EmptyCatch",
|
|
132
241
|
file: filePath,
|
|
133
|
-
line,
|
|
242
|
+
line: startLine,
|
|
134
243
|
column: catchNode.loc.start.column,
|
|
135
244
|
title: "Catch block with only comments",
|
|
136
245
|
message: "Catch block contains only comments, no actual error handling",
|
|
137
|
-
codeSnippet: lines[
|
|
246
|
+
codeSnippet: lines[startLine - 1]?.trim(),
|
|
138
247
|
confidence: "med",
|
|
248
|
+
fixHint: "Add proper error handling or use a recognized suppression comment pattern",
|
|
139
249
|
});
|
|
140
250
|
}
|
|
141
251
|
},
|