@vibecheckai/cli 3.4.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 +243 -152
- 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 +544 -19
- 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 +103 -11
- 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 +47 -0
- package/bin/runners/lib/security/owasp-scanner.js +939 -0
- package/bin/runners/lib/terminal-ui.js +113 -1
- package/bin/runners/lib/unified-cli-output.js +603 -430
- 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 -1286
- 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 +175 -56
- package/mcp-server/index.js +190 -14
- package/mcp-server/package.json +1 -1
- package/mcp-server/tools-v3.js +397 -64
- 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,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-Aware Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides smarter detection logic to reduce false positives in vibecheck scans.
|
|
5
|
+
* Handles cases like:
|
|
6
|
+
* - Tailwind CSS modifiers (placeholder:text-white/40)
|
|
7
|
+
* - UI button labels (Mock/Live toggles)
|
|
8
|
+
* - Valid example data (Todo App in project lists)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Patterns that indicate Tailwind CSS class context
|
|
13
|
+
* These are styling directives, not placeholder content
|
|
14
|
+
*/
|
|
15
|
+
const TAILWIND_MODIFIER_PATTERNS = [
|
|
16
|
+
// Tailwind placeholder: modifier (styles placeholder text in inputs)
|
|
17
|
+
/\bplaceholder:/,
|
|
18
|
+
// Other Tailwind state modifiers that might contain keywords
|
|
19
|
+
/\b(?:hover|focus|active|disabled|checked|invalid|required|empty|default):/,
|
|
20
|
+
// Tailwind arbitrary value syntax
|
|
21
|
+
/\[placeholder[:\]]/,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Patterns that indicate className or CSS context
|
|
26
|
+
*/
|
|
27
|
+
const CSS_CLASS_PATTERNS = [
|
|
28
|
+
// className="..." or className={...}
|
|
29
|
+
/className\s*[=:]\s*[{"`']/,
|
|
30
|
+
// class="..." (HTML)
|
|
31
|
+
/\bclass\s*=\s*["']/,
|
|
32
|
+
// Tailwind @apply directive
|
|
33
|
+
/@apply\s+/,
|
|
34
|
+
// CSS-in-JS style objects
|
|
35
|
+
/\bstyle\s*[=:]\s*\{/,
|
|
36
|
+
// clsx, classnames, cn utilities
|
|
37
|
+
/\b(?:clsx|classnames|cn)\s*\(/,
|
|
38
|
+
// twMerge, tailwind-merge
|
|
39
|
+
/\b(?:twMerge|tailwind-merge)\s*\(/,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Patterns for UI labels/buttons that legitimately use "mock", "todo", etc.
|
|
44
|
+
*/
|
|
45
|
+
const LEGITIMATE_UI_LABEL_PATTERNS = [
|
|
46
|
+
// Button onClick handlers with mode labels
|
|
47
|
+
/onClick\s*=.*['"](?:mock|live|demo|test)["']/i,
|
|
48
|
+
// Mode change handlers
|
|
49
|
+
/onModeChange\s*\?\.\s*\(['"](?:mock|live|demo)["']\)/i,
|
|
50
|
+
// Radio button or toggle values
|
|
51
|
+
/value\s*[=:]\s*['"](?:mock|live|demo|test)["']/i,
|
|
52
|
+
// UI state labels
|
|
53
|
+
/mode\s*===?\s*['"](?:mock|live|demo|test)["']/i,
|
|
54
|
+
// Tab or button labels in arrays/maps
|
|
55
|
+
/\[\s*['"](?:mock|live)["']\s*,/i,
|
|
56
|
+
// Option/select values
|
|
57
|
+
/option\s*value\s*=\s*['"](?:mock|live|demo)["']/i,
|
|
58
|
+
// Button children with mode text
|
|
59
|
+
/>\s*(?:Mock|Live|Demo|Test)\s*</i,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Patterns for legitimate example/template data
|
|
64
|
+
*/
|
|
65
|
+
const LEGITIMATE_EXAMPLE_DATA_PATTERNS = [
|
|
66
|
+
// Array of project/template names (common in UI)
|
|
67
|
+
/\[\s*['"](?:Todo\s*App|Landing\s*Page|Dashboard|Blog|Portfolio|E-?commerce)["']/i,
|
|
68
|
+
// Project name selectors or dropdowns
|
|
69
|
+
/projectName|templateName|appName|recentProjects/i,
|
|
70
|
+
// Example/sample data arrays in UI components
|
|
71
|
+
/examples?\s*[=:]\s*\[/i,
|
|
72
|
+
/templates?\s*[=:]\s*\[/i,
|
|
73
|
+
/presets?\s*[=:]\s*\[/i,
|
|
74
|
+
// Map over example items
|
|
75
|
+
/\.map\(\s*\(\s*(?:name|item|template)\s*\)/i,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a line contains Tailwind CSS placeholder modifier
|
|
80
|
+
*/
|
|
81
|
+
function isTailwindPlaceholderModifier(line) {
|
|
82
|
+
return TAILWIND_MODIFIER_PATTERNS.some(pattern => pattern.test(line));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if the context is a CSS class definition
|
|
87
|
+
*/
|
|
88
|
+
function isCSSClassContext(line, surroundingLines) {
|
|
89
|
+
// Check the current line
|
|
90
|
+
if (CSS_CLASS_PATTERNS.some(pattern => pattern.test(line))) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check surrounding lines for context
|
|
95
|
+
if (surroundingLines && surroundingLines.length > 0) {
|
|
96
|
+
const contextBlock = surroundingLines.join('\n');
|
|
97
|
+
return CSS_CLASS_PATTERNS.some(pattern => pattern.test(contextBlock));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if "mock" or similar words are used as legitimate UI labels
|
|
105
|
+
*/
|
|
106
|
+
function isLegitimateUILabel(line, surroundingLines) {
|
|
107
|
+
const fullContext = surroundingLines
|
|
108
|
+
? [...surroundingLines, line].join('\n')
|
|
109
|
+
: line;
|
|
110
|
+
|
|
111
|
+
return LEGITIMATE_UI_LABEL_PATTERNS.some(pattern => pattern.test(fullContext));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if the content is legitimate example/template data
|
|
116
|
+
*/
|
|
117
|
+
function isLegitimateExampleData(line, surroundingLines) {
|
|
118
|
+
const fullContext = surroundingLines
|
|
119
|
+
? [...surroundingLines, line].join('\n')
|
|
120
|
+
: line;
|
|
121
|
+
|
|
122
|
+
return LEGITIMATE_EXAMPLE_DATA_PATTERNS.some(pattern => pattern.test(fullContext));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if the match is in a JSX prop context (not user-visible content)
|
|
127
|
+
*/
|
|
128
|
+
function isJSXPropContext(line, matchedPattern) {
|
|
129
|
+
// Check if the pattern is part of a JSX prop name (not value)
|
|
130
|
+
const propNamePatterns = [
|
|
131
|
+
// Prop names that legitimately contain keywords
|
|
132
|
+
/\b(?:placeholder|mock|test)(?:Id|Key|Ref|Name|Type|Mode|Data)\s*[=:]/i,
|
|
133
|
+
// Data attributes
|
|
134
|
+
/data-(?:placeholder|mock|test)\s*=/i,
|
|
135
|
+
// Aria labels (visible content, but intentional)
|
|
136
|
+
/aria-(?:label|description)\s*=\s*["'][^"']*(?:placeholder|mock|test)/i,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
return propNamePatterns.some(pattern => pattern.test(line));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Main context detection function
|
|
144
|
+
* Analyzes a line and its context to determine if a match is a false positive
|
|
145
|
+
*/
|
|
146
|
+
function detectFalsePositive(line, matchedPattern, options = {}) {
|
|
147
|
+
const { lineIndex, allLines, filePath, patternType } = options;
|
|
148
|
+
|
|
149
|
+
// Get surrounding lines for context (2 before, 2 after)
|
|
150
|
+
let surroundingLines = [];
|
|
151
|
+
if (allLines && lineIndex !== undefined) {
|
|
152
|
+
const start = Math.max(0, lineIndex - 2);
|
|
153
|
+
const end = Math.min(allLines.length, lineIndex + 3);
|
|
154
|
+
surroundingLines = allLines.slice(start, end);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check for Tailwind CSS placeholder modifier
|
|
158
|
+
const lowerPattern = (matchedPattern || '').toLowerCase();
|
|
159
|
+
if (lowerPattern.includes('placeholder') || patternType === 'placeholder') {
|
|
160
|
+
if (isTailwindPlaceholderModifier(line)) {
|
|
161
|
+
return {
|
|
162
|
+
isFalsePositive: true,
|
|
163
|
+
reason: 'Tailwind CSS placeholder: modifier (styles input placeholder text)',
|
|
164
|
+
confidence: 0.95,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check for CSS class context
|
|
170
|
+
if (isCSSClassContext(line, surroundingLines)) {
|
|
171
|
+
return {
|
|
172
|
+
isFalsePositive: true,
|
|
173
|
+
reason: 'CSS class/className context (styling, not content)',
|
|
174
|
+
confidence: 0.9,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for legitimate UI labels (Mock/Live toggles, etc.)
|
|
179
|
+
if (lowerPattern.includes('mock') || patternType === 'mock') {
|
|
180
|
+
if (isLegitimateUILabel(line, surroundingLines)) {
|
|
181
|
+
return {
|
|
182
|
+
isFalsePositive: true,
|
|
183
|
+
reason: 'Legitimate UI label (mode toggle, button text)',
|
|
184
|
+
confidence: 0.85,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for legitimate example data ("Todo App", etc.)
|
|
190
|
+
if (lowerPattern.includes('todo') || patternType === 'todo') {
|
|
191
|
+
if (isLegitimateExampleData(line, surroundingLines)) {
|
|
192
|
+
return {
|
|
193
|
+
isFalsePositive: true,
|
|
194
|
+
reason: 'Legitimate example/template data in UI',
|
|
195
|
+
confidence: 0.8,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for JSX props that are not content
|
|
201
|
+
if (isJSXPropContext(line, matchedPattern)) {
|
|
202
|
+
return {
|
|
203
|
+
isFalsePositive: true,
|
|
204
|
+
reason: 'JSX prop context (attribute, not visible content)',
|
|
205
|
+
confidence: 0.75,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Not a false positive
|
|
210
|
+
return {
|
|
211
|
+
isFalsePositive: false,
|
|
212
|
+
confidence: 0.5,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Quick check if a line is likely a false positive
|
|
218
|
+
* Use this for fast pre-filtering before detailed analysis
|
|
219
|
+
*/
|
|
220
|
+
function quickFalsePositiveCheck(line) {
|
|
221
|
+
// Fast check for common false positive patterns
|
|
222
|
+
const quickPatterns = [
|
|
223
|
+
/className.*placeholder:/,
|
|
224
|
+
/class=.*placeholder:/,
|
|
225
|
+
/placeholder:text-/,
|
|
226
|
+
/placeholder:opacity-/,
|
|
227
|
+
/placeholder:italic/,
|
|
228
|
+
/onModeChange.*['"]mock["']/i,
|
|
229
|
+
/onClick.*['"]mock["']/i,
|
|
230
|
+
/value=['"](?:mock|live)["']/i,
|
|
231
|
+
/\[['"]Todo App["']/i,
|
|
232
|
+
/\[['"]Mock["'],\s*['"]Live["']\]/i,
|
|
233
|
+
/>\s*Mock\s*</i,
|
|
234
|
+
/>\s*Live\s*</i,
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
return quickPatterns.some(pattern => pattern.test(line));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Filter findings to remove false positives
|
|
242
|
+
*/
|
|
243
|
+
function filterFalsePositives(findings, allLines, options = {}) {
|
|
244
|
+
return findings.filter(finding => {
|
|
245
|
+
const lineContent = finding.content || finding.codeSnippet || allLines[finding.line - 1] || '';
|
|
246
|
+
const result = detectFalsePositive(lineContent, lineContent, {
|
|
247
|
+
lineIndex: finding.line - 1,
|
|
248
|
+
allLines,
|
|
249
|
+
...options,
|
|
250
|
+
});
|
|
251
|
+
return !result.isFalsePositive;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
isTailwindPlaceholderModifier,
|
|
257
|
+
isCSSClassContext,
|
|
258
|
+
isLegitimateUILabel,
|
|
259
|
+
isLegitimateExampleData,
|
|
260
|
+
isJSXPropContext,
|
|
261
|
+
detectFalsePositive,
|
|
262
|
+
quickFalsePositiveCheck,
|
|
263
|
+
filterFalsePositives,
|
|
264
|
+
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-File Analysis Engine
|
|
3
3
|
* Analyzes relationships between files: unused exports, circular dependencies, import consistency
|
|
4
|
+
* Enhanced with monorepo support:
|
|
5
|
+
* - tsconfig.json path alias resolution
|
|
6
|
+
* - package.json exports field support
|
|
7
|
+
* - Workspace package detection
|
|
8
|
+
* - Better barrel export handling
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
const fs = require("fs");
|
|
@@ -9,6 +14,162 @@ const { getAST } = require("./ast-cache");
|
|
|
9
14
|
const traverse = require("@babel/traverse").default;
|
|
10
15
|
const t = require("@babel/types");
|
|
11
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Load and cache tsconfig.json paths
|
|
19
|
+
*/
|
|
20
|
+
let cachedPathAliases = null;
|
|
21
|
+
let cachedPathsRoot = null;
|
|
22
|
+
|
|
23
|
+
function loadPathAliases(repoRoot) {
|
|
24
|
+
if (cachedPathAliases && cachedPathsRoot === repoRoot) {
|
|
25
|
+
return cachedPathAliases;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cachedPathsRoot = repoRoot;
|
|
29
|
+
cachedPathAliases = new Map();
|
|
30
|
+
|
|
31
|
+
// Try to load tsconfig.json
|
|
32
|
+
const tsconfigPaths = [
|
|
33
|
+
path.join(repoRoot, "tsconfig.json"),
|
|
34
|
+
path.join(repoRoot, "tsconfig.base.json"),
|
|
35
|
+
path.join(repoRoot, "jsconfig.json"),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const tsconfigPath of tsconfigPaths) {
|
|
39
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
40
|
+
try {
|
|
41
|
+
// Remove comments from JSON (tsconfig allows them)
|
|
42
|
+
const content = fs.readFileSync(tsconfigPath, "utf8")
|
|
43
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
44
|
+
.replace(/\/\/.*$/gm, "");
|
|
45
|
+
const tsconfig = JSON.parse(content);
|
|
46
|
+
const compilerOptions = tsconfig.compilerOptions || {};
|
|
47
|
+
const baseUrl = compilerOptions.baseUrl || ".";
|
|
48
|
+
const paths = compilerOptions.paths || {};
|
|
49
|
+
|
|
50
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
51
|
+
// Convert alias pattern to regex
|
|
52
|
+
const aliasPattern = alias.replace(/\*/g, "(.*)");
|
|
53
|
+
const aliasRegex = new RegExp(`^${aliasPattern}$`);
|
|
54
|
+
|
|
55
|
+
cachedPathAliases.set(aliasRegex, {
|
|
56
|
+
alias,
|
|
57
|
+
targets: targets.map(t => path.join(repoRoot, baseUrl, t)),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
break; // Use first found config
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Ignore parse errors
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cachedPathAliases;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve path alias to actual path
|
|
72
|
+
*/
|
|
73
|
+
function resolvePathAlias(importPath, repoRoot) {
|
|
74
|
+
const pathAliases = loadPathAliases(repoRoot);
|
|
75
|
+
|
|
76
|
+
for (const [aliasRegex, { alias, targets }] of pathAliases.entries()) {
|
|
77
|
+
const match = importPath.match(aliasRegex);
|
|
78
|
+
if (match) {
|
|
79
|
+
const captured = match[1] || "";
|
|
80
|
+
|
|
81
|
+
for (const target of targets) {
|
|
82
|
+
const resolved = target.replace(/\*/g, captured);
|
|
83
|
+
|
|
84
|
+
// Try common extensions
|
|
85
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
86
|
+
for (const ext of extensions) {
|
|
87
|
+
const candidate = resolved + ext;
|
|
88
|
+
if (fs.existsSync(candidate)) {
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Detect workspace packages in monorepo
|
|
101
|
+
*/
|
|
102
|
+
let cachedWorkspaces = null;
|
|
103
|
+
|
|
104
|
+
function detectWorkspaces(repoRoot) {
|
|
105
|
+
if (cachedWorkspaces) return cachedWorkspaces;
|
|
106
|
+
|
|
107
|
+
cachedWorkspaces = new Map();
|
|
108
|
+
const packageJsonPath = path.join(repoRoot, "package.json");
|
|
109
|
+
|
|
110
|
+
if (!fs.existsSync(packageJsonPath)) return cachedWorkspaces;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
114
|
+
const workspaces = packageJson.workspaces || [];
|
|
115
|
+
|
|
116
|
+
// Handle both array and object format
|
|
117
|
+
const workspacePatterns = Array.isArray(workspaces)
|
|
118
|
+
? workspaces
|
|
119
|
+
: workspaces.packages || [];
|
|
120
|
+
|
|
121
|
+
for (const pattern of workspacePatterns) {
|
|
122
|
+
// Simple glob matching (handles common patterns like "packages/*")
|
|
123
|
+
const baseDir = pattern.replace(/\*.*$/, "");
|
|
124
|
+
const fullBaseDir = path.join(repoRoot, baseDir);
|
|
125
|
+
|
|
126
|
+
if (fs.existsSync(fullBaseDir)) {
|
|
127
|
+
const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
const pkgJsonPath = path.join(fullBaseDir, entry.name, "package.json");
|
|
131
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
132
|
+
try {
|
|
133
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
134
|
+
cachedWorkspaces.set(pkgJson.name, {
|
|
135
|
+
name: pkgJson.name,
|
|
136
|
+
path: path.join(baseDir, entry.name),
|
|
137
|
+
exports: pkgJson.exports || {},
|
|
138
|
+
});
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// Ignore parse errors
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// Ignore errors
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return cachedWorkspaces;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if import is a workspace package
|
|
156
|
+
*/
|
|
157
|
+
function isWorkspaceImport(importPath, repoRoot) {
|
|
158
|
+
const workspaces = detectWorkspaces(repoRoot);
|
|
159
|
+
return workspaces.has(importPath) ||
|
|
160
|
+
Array.from(workspaces.keys()).some(ws => importPath.startsWith(ws + "/"));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if file is a barrel/index file (re-exports)
|
|
165
|
+
*/
|
|
166
|
+
function isBarrelFile(fileRel) {
|
|
167
|
+
return fileRel.endsWith("/index.ts") ||
|
|
168
|
+
fileRel.endsWith("/index.tsx") ||
|
|
169
|
+
fileRel.endsWith("/index.js") ||
|
|
170
|
+
fileRel.endsWith("/index.jsx");
|
|
171
|
+
}
|
|
172
|
+
|
|
12
173
|
/**
|
|
13
174
|
* Build export map for all files
|
|
14
175
|
*/
|
|
@@ -98,8 +259,43 @@ function buildExportMap(files, repoRoot) {
|
|
|
98
259
|
|
|
99
260
|
/**
|
|
100
261
|
* Resolve import path to actual file path
|
|
262
|
+
* Enhanced with path alias and monorepo support
|
|
101
263
|
*/
|
|
102
264
|
function resolveImportPath(importPath, fromFile, repoRoot) {
|
|
265
|
+
// Skip external packages (node_modules)
|
|
266
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/") && !importPath.startsWith("@/")) {
|
|
267
|
+
// Check if it's a workspace import
|
|
268
|
+
if (isWorkspaceImport(importPath, repoRoot)) {
|
|
269
|
+
// Resolve workspace import
|
|
270
|
+
const workspaces = detectWorkspaces(repoRoot);
|
|
271
|
+
const wsName = Array.from(workspaces.keys()).find(ws =>
|
|
272
|
+
importPath === ws || importPath.startsWith(ws + "/")
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (wsName) {
|
|
276
|
+
const ws = workspaces.get(wsName);
|
|
277
|
+
const subPath = importPath.slice(wsName.length + 1) || "index";
|
|
278
|
+
const wsFullPath = path.join(repoRoot, ws.path, "src", subPath);
|
|
279
|
+
|
|
280
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx"];
|
|
281
|
+
for (const ext of extensions) {
|
|
282
|
+
const candidate = wsFullPath + ext;
|
|
283
|
+
if (fs.existsSync(candidate)) {
|
|
284
|
+
return path.relative(repoRoot, candidate).replace(/\\/g, "/");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Try path alias resolution
|
|
291
|
+
const aliasResolved = resolvePathAlias(importPath, repoRoot);
|
|
292
|
+
if (aliasResolved) {
|
|
293
|
+
return path.relative(repoRoot, aliasResolved).replace(/\\/g, "/");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return null; // External package
|
|
297
|
+
}
|
|
298
|
+
|
|
103
299
|
const dir = path.dirname(path.join(repoRoot, fromFile));
|
|
104
300
|
let resolved = path.resolve(dir, importPath);
|
|
105
301
|
|
|
@@ -157,17 +353,41 @@ function analyzeCrossFile(files, repoRoot) {
|
|
|
157
353
|
|
|
158
354
|
for (const exp of exports) {
|
|
159
355
|
if (!used.has(exp) && exp !== "default") {
|
|
160
|
-
//
|
|
161
|
-
|
|
356
|
+
// Skip known patterns that legitimately have unused exports
|
|
357
|
+
const shouldSkip =
|
|
358
|
+
// Test/spec files
|
|
359
|
+
fileRel.includes(".test.") ||
|
|
360
|
+
fileRel.includes(".spec.") ||
|
|
361
|
+
fileRel.includes("__tests__") ||
|
|
362
|
+
// Config files
|
|
363
|
+
fileRel.includes("config") ||
|
|
364
|
+
fileRel.includes(".config.") ||
|
|
365
|
+
// Type definitions
|
|
366
|
+
fileRel.endsWith(".d.ts") ||
|
|
367
|
+
// Barrel/index files (re-exports are intentionally not all used)
|
|
368
|
+
isBarrelFile(fileRel) ||
|
|
369
|
+
// Entry points
|
|
370
|
+
fileRel.includes("/pages/") ||
|
|
371
|
+
fileRel.includes("/app/") ||
|
|
372
|
+
fileRel.endsWith("/index.ts") ||
|
|
373
|
+
// API routes (exported handlers are used by framework)
|
|
374
|
+
fileRel.includes("/api/") ||
|
|
375
|
+
// Stories
|
|
376
|
+
fileRel.includes(".stories.") ||
|
|
377
|
+
// Migrations
|
|
378
|
+
fileRel.includes("/migrations/");
|
|
379
|
+
|
|
380
|
+
if (!shouldSkip) {
|
|
162
381
|
findings.push({
|
|
163
382
|
type: "unused_export",
|
|
164
|
-
severity: "
|
|
383
|
+
severity: "INFO", // Downgraded from WARN - often intentional
|
|
165
384
|
category: "CodeQuality",
|
|
166
385
|
file: fileRel,
|
|
167
|
-
line: 0,
|
|
168
|
-
title: `
|
|
169
|
-
message: `Export "${exp}" from ${fileRel} is
|
|
170
|
-
confidence: "
|
|
386
|
+
line: 0,
|
|
387
|
+
title: `Potentially unused export: ${exp}`,
|
|
388
|
+
message: `Export "${exp}" from ${fileRel} is not imported by other analyzed files`,
|
|
389
|
+
confidence: "low", // Low confidence - might be used externally
|
|
390
|
+
fixHint: "Verify if this export is used externally or remove if truly unused",
|
|
171
391
|
});
|
|
172
392
|
}
|
|
173
393
|
}
|
|
@@ -211,20 +431,42 @@ function analyzeCrossFile(files, repoRoot) {
|
|
|
211
431
|
return null;
|
|
212
432
|
}
|
|
213
433
|
|
|
434
|
+
const reportedCycles = new Set();
|
|
435
|
+
|
|
214
436
|
for (const fileRel of exportMap.keys()) {
|
|
215
437
|
if (!visited.has(fileRel)) {
|
|
216
438
|
const cycle = detectCycle(fileRel);
|
|
217
439
|
if (cycle && cycle.length > 2) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
440
|
+
// Create a normalized cycle key to avoid duplicate reports
|
|
441
|
+
const cycleKey = [...cycle].sort().join("|");
|
|
442
|
+
|
|
443
|
+
if (!reportedCycles.has(cycleKey)) {
|
|
444
|
+
reportedCycles.add(cycleKey);
|
|
445
|
+
|
|
446
|
+
// Determine severity based on cycle type
|
|
447
|
+
// Type-only imports often don't cause runtime issues
|
|
448
|
+
let severity = "WARN";
|
|
449
|
+
let confidence = "high";
|
|
450
|
+
|
|
451
|
+
// Check if any files in the cycle are barrel files (common and often harmless)
|
|
452
|
+
const hasBarrel = cycle.some(f => isBarrelFile(f));
|
|
453
|
+
if (hasBarrel) {
|
|
454
|
+
severity = "INFO";
|
|
455
|
+
confidence = "med";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
findings.push({
|
|
459
|
+
type: "circular_dependency",
|
|
460
|
+
severity,
|
|
461
|
+
category: "CodeQuality",
|
|
462
|
+
file: cycle[0],
|
|
463
|
+
line: 0,
|
|
464
|
+
title: `Circular dependency detected (${cycle.length - 1} files)`,
|
|
465
|
+
message: `Circular dependency: ${cycle.join(" → ")}`,
|
|
466
|
+
confidence,
|
|
467
|
+
fixHint: "Break the cycle by extracting shared code to a separate module, or use dynamic imports",
|
|
468
|
+
});
|
|
469
|
+
}
|
|
228
470
|
}
|
|
229
471
|
}
|
|
230
472
|
}
|
|
@@ -246,23 +488,46 @@ function analyzeCrossFile(files, repoRoot) {
|
|
|
246
488
|
|
|
247
489
|
for (const [targetFile, styles] of importStyles.entries()) {
|
|
248
490
|
if (styles.size > 1) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
491
|
+
// Filter out path alias variations that resolve to the same file
|
|
492
|
+
const uniqueStyles = new Set(
|
|
493
|
+
Array.from(styles).filter(s => !s.startsWith("@/") || styles.size === 1)
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
if (uniqueStyles.size > 1) {
|
|
497
|
+
findings.push({
|
|
498
|
+
type: "inconsistent_imports",
|
|
499
|
+
severity: "INFO", // Downgraded - often just style preference
|
|
500
|
+
category: "CodeQuality",
|
|
501
|
+
file: targetFile,
|
|
502
|
+
line: 0,
|
|
503
|
+
title: `Inconsistent import paths`,
|
|
504
|
+
message: `Module imported with ${uniqueStyles.size} different paths: ${Array.from(uniqueStyles).join(", ")}`,
|
|
505
|
+
confidence: "low",
|
|
506
|
+
fixHint: "Standardize import paths using path aliases (e.g., @/) for consistency",
|
|
507
|
+
});
|
|
508
|
+
}
|
|
259
509
|
}
|
|
260
510
|
}
|
|
261
511
|
|
|
262
512
|
return findings;
|
|
263
513
|
}
|
|
264
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Clear all caches (useful for testing)
|
|
517
|
+
*/
|
|
518
|
+
function clearCaches() {
|
|
519
|
+
cachedPathAliases = null;
|
|
520
|
+
cachedPathsRoot = null;
|
|
521
|
+
cachedWorkspaces = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
265
524
|
module.exports = {
|
|
266
525
|
analyzeCrossFile,
|
|
267
526
|
buildExportMap,
|
|
527
|
+
resolveImportPath,
|
|
528
|
+
loadPathAliases,
|
|
529
|
+
detectWorkspaces,
|
|
530
|
+
isWorkspaceImport,
|
|
531
|
+
isBarrelFile,
|
|
532
|
+
clearCaches,
|
|
268
533
|
};
|