react-code-smell-detector 1.1.1 ā 1.3.1
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 +21 -0
- package/README.md +115 -11
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +65 -2
- package/dist/cli.js +134 -33
- package/dist/detectors/accessibility.d.ts +12 -0
- package/dist/detectors/accessibility.d.ts.map +1 -0
- package/dist/detectors/accessibility.js +191 -0
- package/dist/detectors/complexity.d.ts +17 -0
- package/dist/detectors/complexity.d.ts.map +1 -0
- package/dist/detectors/complexity.js +69 -0
- package/dist/detectors/debug.d.ts +10 -0
- package/dist/detectors/debug.d.ts.map +1 -0
- package/dist/detectors/debug.js +87 -0
- package/dist/detectors/imports.d.ts +22 -0
- package/dist/detectors/imports.d.ts.map +1 -0
- package/dist/detectors/imports.js +210 -0
- package/dist/detectors/index.d.ts +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +8 -0
- package/dist/detectors/memoryLeak.d.ts +7 -0
- package/dist/detectors/memoryLeak.d.ts.map +1 -0
- package/dist/detectors/memoryLeak.js +111 -0
- package/dist/detectors/security.d.ts +12 -0
- package/dist/detectors/security.d.ts.map +1 -0
- package/dist/detectors/security.js +161 -0
- package/dist/fixer.d.ts +23 -0
- package/dist/fixer.d.ts.map +1 -0
- package/dist/fixer.js +133 -0
- package/dist/git.d.ts +28 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +117 -0
- package/dist/htmlReporter.d.ts +6 -0
- package/dist/htmlReporter.d.ts.map +1 -0
- package/dist/htmlReporter.js +453 -0
- package/dist/reporter.js +26 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +13 -0
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +89 -0
- package/package.json +8 -2
- package/src/analyzer.ts +0 -269
- package/src/cli.ts +0 -125
- package/src/detectors/deadCode.ts +0 -163
- package/src/detectors/dependencyArray.ts +0 -176
- package/src/detectors/hooksRules.ts +0 -101
- package/src/detectors/index.ts +0 -16
- package/src/detectors/javascript.ts +0 -169
- package/src/detectors/largeComponent.ts +0 -63
- package/src/detectors/magicValues.ts +0 -114
- package/src/detectors/memoization.ts +0 -177
- package/src/detectors/missingKey.ts +0 -105
- package/src/detectors/nestedTernary.ts +0 -75
- package/src/detectors/nextjs.ts +0 -124
- package/src/detectors/nodejs.ts +0 -199
- package/src/detectors/propDrilling.ts +0 -103
- package/src/detectors/reactNative.ts +0 -154
- package/src/detectors/typescript.ts +0 -151
- package/src/detectors/useEffect.ts +0 -117
- package/src/index.ts +0 -4
- package/src/parser/index.ts +0 -195
- package/src/reporter.ts +0 -278
- package/src/types/index.ts +0 -144
- package/tsconfig.json +0 -19
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vsthakur101
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -7,7 +7,17 @@ A CLI tool that analyzes React projects and detects common code smells, providin
|
|
|
7
7
|
- š **Detect Code Smells**: Identifies common React anti-patterns
|
|
8
8
|
- š **Technical Debt Score**: Grades your codebase from A to F
|
|
9
9
|
- š” **Refactoring Suggestions**: Actionable recommendations for each issue
|
|
10
|
-
- š **Multiple Output Formats**: Console (colored), JSON, and
|
|
10
|
+
- š **Multiple Output Formats**: Console (colored), JSON, Markdown, and HTML
|
|
11
|
+
- š **Security Scanning**: Detects XSS vulnerabilities, eval usage, exposed secrets
|
|
12
|
+
- āæ **Accessibility Checks**: Missing alt text, labels, ARIA attributes
|
|
13
|
+
- š **Debug Cleanup**: console.log, debugger, TODO/FIXME detection
|
|
14
|
+
- š¤ **CI/CD Ready**: Exit codes and flags for pipeline integration
|
|
15
|
+
- š§ **Auto-Fix**: Automatically fix simple issues (console.log, var, ==, missing alt)
|
|
16
|
+
- š **Watch Mode**: Re-analyze on file changes
|
|
17
|
+
- š **Git Integration**: Analyze only modified files
|
|
18
|
+
- š§® **Complexity Metrics**: Cyclomatic and cognitive complexity scoring
|
|
19
|
+
- š§ **Memory Leak Detection**: Find missing cleanup in useEffect
|
|
20
|
+
- š **Import Analysis**: Detect circular dependencies and barrel file issues
|
|
11
21
|
|
|
12
22
|
### Detected Code Smells
|
|
13
23
|
|
|
@@ -18,6 +28,13 @@ A CLI tool that analyzes React projects and detects common code smells, providin
|
|
|
18
28
|
| **Large Components** | Components over 300 lines, deep JSX nesting |
|
|
19
29
|
| **Unmemoized Calculations** | `.map()`, `.filter()`, `.reduce()`, `JSON.parse()` without `useMemo` |
|
|
20
30
|
| **Inline Functions** | Callbacks defined inline in JSX props |
|
|
31
|
+
| **Security Issues** | dangerouslySetInnerHTML, eval(), innerHTML, exposed API keys |
|
|
32
|
+
| **Accessibility** | Missing alt text, form labels, keyboard handlers, semantic HTML |
|
|
33
|
+
| **Debug Statements** | console.log, debugger statements, TODO/FIXME comments |
|
|
34
|
+
| **Memory Leaks** | Missing cleanup for event listeners, timers, subscriptions |
|
|
35
|
+
| **Code Complexity** | Cyclomatic complexity, cognitive complexity, deep nesting |
|
|
36
|
+
| **Import Issues** | Circular dependencies, barrel file imports, excessive imports |
|
|
37
|
+
| **Framework-Specific** | Next.js, React Native, Node.js, TypeScript issues |
|
|
21
38
|
|
|
22
39
|
## Installation
|
|
23
40
|
|
|
@@ -56,6 +73,9 @@ react-smell ./src -f json
|
|
|
56
73
|
|
|
57
74
|
# Markdown (for documentation)
|
|
58
75
|
react-smell ./src -f markdown -o report.md
|
|
76
|
+
|
|
77
|
+
# HTML (beautiful visual report)
|
|
78
|
+
react-smell ./src -f html -o report.html
|
|
59
79
|
```
|
|
60
80
|
|
|
61
81
|
### Configuration
|
|
@@ -81,9 +101,14 @@ Or create manually:
|
|
|
81
101
|
|
|
82
102
|
| Option | Description | Default |
|
|
83
103
|
|--------|-------------|---------|
|
|
84
|
-
| `-f, --format <format>` | Output format: console, json, markdown | `console` |
|
|
104
|
+
| `-f, --format <format>` | Output format: console, json, markdown, html | `console` |
|
|
85
105
|
| `-s, --snippets` | Show code snippets in output | `false` |
|
|
86
106
|
| `-c, --config <file>` | Path to config file | `.smellrc.json` |
|
|
107
|
+
| `--ci` | CI mode: exit with code 1 if any issues | `false` |
|
|
108
|
+
| `--fail-on <severity>` | Exit code 1 threshold: error, warning, info | `error` |
|
|
109
|
+
| `--fix` | Auto-fix simple issues (console.log, var, ==, alt) | `false` |
|
|
110
|
+
| `--watch` | Watch mode: re-analyze on file changes | `false` |
|
|
111
|
+
| `--changed` | Only analyze git-modified files | `false` |
|
|
87
112
|
| `--max-effects <number>` | Max useEffects per component | `3` |
|
|
88
113
|
| `--max-props <number>` | Max props before warning | `7` |
|
|
89
114
|
| `--max-lines <number>` | Max lines per component | `300` |
|
|
@@ -91,6 +116,40 @@ Or create manually:
|
|
|
91
116
|
| `--exclude <patterns>` | Glob patterns to exclude | `node_modules,dist` |
|
|
92
117
|
| `-o, --output <file>` | Write output to file | - |
|
|
93
118
|
|
|
119
|
+
### Auto-Fix
|
|
120
|
+
|
|
121
|
+
Automatically fix simple issues:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Fix and show remaining issues
|
|
125
|
+
react-smell ./src --fix
|
|
126
|
+
|
|
127
|
+
# Fix only (no report)
|
|
128
|
+
react-smell ./src --fix -f json > /dev/null
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Fixable issues:
|
|
132
|
+
- `console.log`, `console.debug`, etc. ā Removed
|
|
133
|
+
- `var x = 1` ā `let x = 1`
|
|
134
|
+
- `a == b` ā `a === b`
|
|
135
|
+
- `<img src="...">` ā `<img src="..." alt="">`
|
|
136
|
+
|
|
137
|
+
### Watch Mode
|
|
138
|
+
|
|
139
|
+
Re-analyze on every file change:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
react-smell ./src --watch
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Git Integration
|
|
146
|
+
|
|
147
|
+
Only analyze modified files:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
react-smell ./src --changed
|
|
151
|
+
```
|
|
152
|
+
|
|
94
153
|
## Example Output
|
|
95
154
|
|
|
96
155
|
```
|
|
@@ -143,17 +202,62 @@ const report = reportResults(result, {
|
|
|
143
202
|
|
|
144
203
|
## CI/CD Integration
|
|
145
204
|
|
|
146
|
-
|
|
205
|
+
The tool provides flexible exit codes for CI/CD pipelines:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Fail on any issues (strict mode)
|
|
209
|
+
react-smell ./src --ci
|
|
210
|
+
|
|
211
|
+
# Fail on warnings and errors (default: errors only)
|
|
212
|
+
react-smell ./src --fail-on warning
|
|
213
|
+
|
|
214
|
+
# Generate HTML report and fail on errors
|
|
215
|
+
react-smell ./src -f html -o report.html --fail-on error
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### GitHub Actions Example
|
|
147
219
|
|
|
148
220
|
```yaml
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
221
|
+
name: Code Quality
|
|
222
|
+
|
|
223
|
+
on: [push, pull_request]
|
|
224
|
+
|
|
225
|
+
jobs:
|
|
226
|
+
code-smells:
|
|
227
|
+
runs-on: ubuntu-latest
|
|
228
|
+
steps:
|
|
229
|
+
- uses: actions/checkout@v4
|
|
230
|
+
- uses: actions/setup-node@v4
|
|
231
|
+
with:
|
|
232
|
+
node-version: '20'
|
|
233
|
+
|
|
234
|
+
- name: Install dependencies
|
|
235
|
+
run: npm ci
|
|
236
|
+
|
|
237
|
+
- name: Check code smells
|
|
238
|
+
run: npx react-smell ./src -f html -o smell-report.html --fail-on warning
|
|
239
|
+
|
|
240
|
+
- name: Upload report
|
|
241
|
+
uses: actions/upload-artifact@v4
|
|
242
|
+
if: always()
|
|
243
|
+
with:
|
|
244
|
+
name: code-smell-report
|
|
245
|
+
path: smell-report.html
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Ignoring Issues
|
|
249
|
+
|
|
250
|
+
Use `@smell-ignore` comments to suppress specific issues:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
// @smell-ignore
|
|
254
|
+
console.log('This debug statement is ignored');
|
|
255
|
+
|
|
256
|
+
// @smell-ignore debug-statement
|
|
257
|
+
console.log('Only ignores debug-statement type');
|
|
258
|
+
|
|
259
|
+
// @smell-ignore *
|
|
260
|
+
const risky = eval('1+1'); // All issues on next line ignored
|
|
157
261
|
```
|
|
158
262
|
|
|
159
263
|
## Technical Debt Score
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AA4BA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA0CtF;AA8PD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/analyzer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fg from 'fast-glob';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { parseFile } from './parser/index.js';
|
|
4
|
-
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, } from './detectors/index.js';
|
|
4
|
+
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, detectComplexity, detectMemoryLeaks, detectImportIssues, } from './detectors/index.js';
|
|
5
5
|
import { DEFAULT_CONFIG, } from './types/index.js';
|
|
6
6
|
export async function analyzeProject(options) {
|
|
7
7
|
const { rootDir, include = ['**/*.tsx', '**/*.jsx'], exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'], config: userConfig = {}, } = options;
|
|
@@ -74,16 +74,53 @@ function analyzeFile(parseResult, filePath, config) {
|
|
|
74
74
|
smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
|
|
75
75
|
smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
|
|
76
76
|
smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
|
|
77
|
+
// Debug, Security, Accessibility
|
|
78
|
+
smells.push(...detectDebugStatements(component, filePath, sourceCode, config));
|
|
79
|
+
smells.push(...detectSecurityIssues(component, filePath, sourceCode, config));
|
|
80
|
+
smells.push(...detectAccessibilityIssues(component, filePath, sourceCode, config));
|
|
81
|
+
// Complexity and Memory Leaks
|
|
82
|
+
smells.push(...detectComplexity(component, filePath, sourceCode, config));
|
|
83
|
+
smells.push(...detectMemoryLeaks(component, filePath, sourceCode, config));
|
|
84
|
+
smells.push(...detectImportIssues(component, filePath, sourceCode, config));
|
|
77
85
|
});
|
|
78
86
|
// Run cross-component analysis
|
|
79
87
|
smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
|
|
88
|
+
// Filter out smells with @smell-ignore comments
|
|
89
|
+
const filteredSmells = filterIgnoredSmells(smells, sourceCode);
|
|
80
90
|
return {
|
|
81
91
|
file: filePath,
|
|
82
92
|
components: componentInfos,
|
|
83
|
-
smells,
|
|
93
|
+
smells: filteredSmells,
|
|
84
94
|
imports,
|
|
85
95
|
};
|
|
86
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Filter out smells that have @smell-ignore comment on the preceding line
|
|
99
|
+
* Supports: // @smell-ignore, block comments with @smell-ignore, @smell-ignore [type]
|
|
100
|
+
*/
|
|
101
|
+
function filterIgnoredSmells(smells, sourceCode) {
|
|
102
|
+
const lines = sourceCode.split('\n');
|
|
103
|
+
return smells.filter(smell => {
|
|
104
|
+
if (smell.line <= 1)
|
|
105
|
+
return true;
|
|
106
|
+
// Check the line before and the same line for ignore comments
|
|
107
|
+
const lineIndex = smell.line - 1; // Convert to 0-indexed
|
|
108
|
+
const prevLine = lines[lineIndex - 1]?.trim() || '';
|
|
109
|
+
const currentLine = lines[lineIndex]?.trim() || '';
|
|
110
|
+
// Check for @smell-ignore patterns
|
|
111
|
+
const ignorePatterns = [
|
|
112
|
+
/@smell-ignore\s*$/, // @smell-ignore (ignore all)
|
|
113
|
+
/@smell-ignore\s+\*/, // @smell-ignore * (ignore all)
|
|
114
|
+
new RegExp(`@smell-ignore\\s+${smell.type}`), // @smell-ignore [specific-type]
|
|
115
|
+
];
|
|
116
|
+
for (const pattern of ignorePatterns) {
|
|
117
|
+
if (pattern.test(prevLine) || pattern.test(currentLine)) {
|
|
118
|
+
return false; // Filter out this smell
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return true; // Keep this smell
|
|
122
|
+
});
|
|
123
|
+
}
|
|
87
124
|
function calculateSummary(files) {
|
|
88
125
|
const smellsByType = {
|
|
89
126
|
'useEffect-overuse': 0,
|
|
@@ -124,6 +161,32 @@ function calculateSummary(files) {
|
|
|
124
161
|
'ts-missing-return-type': 0,
|
|
125
162
|
'ts-non-null-assertion': 0,
|
|
126
163
|
'ts-type-assertion': 0,
|
|
164
|
+
// Debug statements
|
|
165
|
+
'debug-statement': 0,
|
|
166
|
+
'todo-comment': 0,
|
|
167
|
+
// Security
|
|
168
|
+
'security-xss': 0,
|
|
169
|
+
'security-eval': 0,
|
|
170
|
+
'security-secrets': 0,
|
|
171
|
+
// Accessibility
|
|
172
|
+
'a11y-missing-alt': 0,
|
|
173
|
+
'a11y-missing-label': 0,
|
|
174
|
+
'a11y-interactive-role': 0,
|
|
175
|
+
'a11y-keyboard': 0,
|
|
176
|
+
'a11y-semantic': 0,
|
|
177
|
+
// Complexity
|
|
178
|
+
'high-cyclomatic-complexity': 0,
|
|
179
|
+
'high-cognitive-complexity': 0,
|
|
180
|
+
// Memory leaks
|
|
181
|
+
'memory-leak-event-listener': 0,
|
|
182
|
+
'memory-leak-subscription': 0,
|
|
183
|
+
'memory-leak-timer': 0,
|
|
184
|
+
'memory-leak-async': 0,
|
|
185
|
+
// Import issues
|
|
186
|
+
'circular-dependency': 0,
|
|
187
|
+
'barrel-file-import': 0,
|
|
188
|
+
'namespace-import': 0,
|
|
189
|
+
'excessive-imports': 0,
|
|
127
190
|
};
|
|
128
191
|
const smellsBySeverity = {
|
|
129
192
|
error: 0,
|
package/dist/cli.js
CHANGED
|
@@ -5,16 +5,25 @@ import ora from 'ora';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { analyzeProject, DEFAULT_CONFIG } from './analyzer.js';
|
|
7
7
|
import { reportResults } from './reporter.js';
|
|
8
|
+
import { generateHTMLReport } from './htmlReporter.js';
|
|
9
|
+
import { fixFile, isFixable } from './fixer.js';
|
|
10
|
+
import { startWatch } from './watcher.js';
|
|
11
|
+
import { getAllModifiedFiles, filterReactFiles, getGitInfo } from './git.js';
|
|
8
12
|
import fs from 'fs/promises';
|
|
9
13
|
const program = new Command();
|
|
10
14
|
program
|
|
11
15
|
.name('react-smell')
|
|
12
16
|
.description('Detect code smells in React projects')
|
|
13
|
-
.version('1.
|
|
17
|
+
.version('1.3.0')
|
|
14
18
|
.argument('[directory]', 'Directory to analyze', '.')
|
|
15
|
-
.option('-f, --format <format>', 'Output format: console, json, markdown', 'console')
|
|
19
|
+
.option('-f, --format <format>', 'Output format: console, json, markdown, html', 'console')
|
|
16
20
|
.option('-s, --snippets', 'Show code snippets in output', false)
|
|
17
21
|
.option('-c, --config <file>', 'Path to config file')
|
|
22
|
+
.option('--ci', 'CI mode: exit with code 1 if any issues found')
|
|
23
|
+
.option('--fail-on <severity>', 'Exit with code 1 if issues of this severity or higher (error, warning, info)', 'error')
|
|
24
|
+
.option('--fix', 'Auto-fix simple issues (console.log, var, ==, missing alt)')
|
|
25
|
+
.option('--watch', 'Watch mode: re-analyze on file changes')
|
|
26
|
+
.option('--changed', 'Only analyze git-modified files')
|
|
18
27
|
.option('--max-effects <number>', 'Max useEffects per component', parseInt)
|
|
19
28
|
.option('--max-props <number>', 'Max props before warning', parseInt)
|
|
20
29
|
.option('--max-lines <number>', 'Max lines per component', parseInt)
|
|
@@ -31,43 +40,114 @@ program
|
|
|
31
40
|
console.error(chalk.red(`Error: Directory "${rootDir}" does not exist.`));
|
|
32
41
|
process.exit(1);
|
|
33
42
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
42
|
-
fileConfig = JSON.parse(configContent);
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
spinner.fail(`Could not load config file: ${error.message}`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
43
|
+
// Load config file if specified
|
|
44
|
+
let fileConfig = {};
|
|
45
|
+
if (options.config) {
|
|
46
|
+
try {
|
|
47
|
+
const configPath = path.resolve(process.cwd(), options.config);
|
|
48
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
49
|
+
fileConfig = JSON.parse(configContent);
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error(chalk.red(`Could not load config file: ${error.message}`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Build config from options
|
|
57
|
+
const config = {
|
|
58
|
+
...DEFAULT_CONFIG,
|
|
59
|
+
...fileConfig,
|
|
60
|
+
...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
|
|
61
|
+
...(options.maxProps && { maxPropsCount: options.maxProps }),
|
|
62
|
+
...(options.maxLines && { maxComponentLines: options.maxLines }),
|
|
63
|
+
};
|
|
64
|
+
const include = options.include?.split(',').map((p) => p.trim()) || ['**/*.tsx', '**/*.jsx'];
|
|
65
|
+
const exclude = options.exclude?.split(',').map((p) => p.trim()) || ['**/node_modules/**', '**/dist/**'];
|
|
66
|
+
// Watch mode
|
|
67
|
+
if (options.watch) {
|
|
68
|
+
const watcher = startWatch({
|
|
60
69
|
rootDir,
|
|
61
70
|
include,
|
|
62
71
|
exclude,
|
|
63
72
|
config,
|
|
73
|
+
showSnippets: options.snippets,
|
|
64
74
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
// Handle Ctrl+C gracefully
|
|
76
|
+
process.on('SIGINT', () => {
|
|
77
|
+
watcher.close();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
return; // Don't continue to regular analysis
|
|
81
|
+
}
|
|
82
|
+
// Git changed files mode
|
|
83
|
+
let filesToAnalyze;
|
|
84
|
+
if (options.changed) {
|
|
85
|
+
const gitInfo = getGitInfo(rootDir);
|
|
86
|
+
if (!gitInfo.isGitRepo) {
|
|
87
|
+
console.error(chalk.red('Error: --changed requires a git repository'));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const modifiedFiles = getAllModifiedFiles(rootDir);
|
|
91
|
+
filesToAnalyze = filterReactFiles(modifiedFiles);
|
|
92
|
+
if (filesToAnalyze.length === 0) {
|
|
93
|
+
console.log(chalk.green('ā No modified React files to analyze'));
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
console.log(chalk.cyan(`\nš Analyzing ${filesToAnalyze.length} modified file(s)...\n`));
|
|
97
|
+
}
|
|
98
|
+
const spinner = ora('Analyzing React project...').start();
|
|
99
|
+
try {
|
|
100
|
+
const result = await analyzeProject({
|
|
69
101
|
rootDir,
|
|
102
|
+
include: filesToAnalyze ? undefined : include,
|
|
103
|
+
exclude: filesToAnalyze ? undefined : exclude,
|
|
104
|
+
config,
|
|
70
105
|
});
|
|
106
|
+
spinner.stop();
|
|
107
|
+
// Fix mode - apply auto-fixes
|
|
108
|
+
if (options.fix) {
|
|
109
|
+
const fixableSmells = result.files.flatMap(f => f.smells.filter(isFixable).map(s => ({ ...s, file: f.file })));
|
|
110
|
+
if (fixableSmells.length > 0) {
|
|
111
|
+
console.log(chalk.cyan(`\nš§ Auto-fixing ${fixableSmells.length} issue(s)...\n`));
|
|
112
|
+
// Group by file
|
|
113
|
+
const smellsByFile = new Map();
|
|
114
|
+
fixableSmells.forEach(smell => {
|
|
115
|
+
const existing = smellsByFile.get(smell.file) || [];
|
|
116
|
+
existing.push(smell);
|
|
117
|
+
smellsByFile.set(smell.file, existing);
|
|
118
|
+
});
|
|
119
|
+
let totalFixed = 0;
|
|
120
|
+
for (const [file, smells] of smellsByFile) {
|
|
121
|
+
const fixResult = await fixFile(file, smells);
|
|
122
|
+
if (fixResult.fixedSmells.length > 0) {
|
|
123
|
+
console.log(chalk.green(` ā Fixed ${fixResult.fixedSmells.length} issue(s) in ${path.relative(rootDir, file)}`));
|
|
124
|
+
totalFixed += fixResult.fixedSmells.length;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
console.log(chalk.green(`\nā Fixed ${totalFixed} issue(s) total\n`));
|
|
128
|
+
// Re-analyze after fixes
|
|
129
|
+
const newResult = await analyzeProject({ rootDir, include, exclude, config });
|
|
130
|
+
console.log(chalk.dim(`Remaining issues: ${newResult.summary.totalSmells}\n`));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(chalk.yellow('\nNo auto-fixable issues found\n'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let output;
|
|
137
|
+
if (options.format === 'html') {
|
|
138
|
+
output = generateHTMLReport(result, rootDir);
|
|
139
|
+
// Auto-set output file for HTML if not specified
|
|
140
|
+
if (!options.output) {
|
|
141
|
+
options.output = 'code-smell-report.html';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
output = reportResults(result, {
|
|
146
|
+
format: options.format,
|
|
147
|
+
showCodeSnippets: options.snippets,
|
|
148
|
+
rootDir,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
71
151
|
if (options.output) {
|
|
72
152
|
const outputPath = path.resolve(process.cwd(), options.output);
|
|
73
153
|
await fs.writeFile(outputPath, output, 'utf-8');
|
|
@@ -76,8 +156,29 @@ program
|
|
|
76
156
|
else {
|
|
77
157
|
console.log(output);
|
|
78
158
|
}
|
|
79
|
-
//
|
|
80
|
-
|
|
159
|
+
// CI/CD exit code handling
|
|
160
|
+
const { smellsBySeverity } = result.summary;
|
|
161
|
+
let shouldFail = false;
|
|
162
|
+
if (options.ci) {
|
|
163
|
+
// CI mode: fail on any issues
|
|
164
|
+
shouldFail = result.summary.totalSmells > 0;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// Check fail-on threshold
|
|
168
|
+
switch (options.failOn) {
|
|
169
|
+
case 'info':
|
|
170
|
+
shouldFail = smellsBySeverity.info > 0 || smellsBySeverity.warning > 0 || smellsBySeverity.error > 0;
|
|
171
|
+
break;
|
|
172
|
+
case 'warning':
|
|
173
|
+
shouldFail = smellsBySeverity.warning > 0 || smellsBySeverity.error > 0;
|
|
174
|
+
break;
|
|
175
|
+
case 'error':
|
|
176
|
+
default:
|
|
177
|
+
shouldFail = smellsBySeverity.error > 0;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (shouldFail) {
|
|
81
182
|
process.exit(1);
|
|
82
183
|
}
|
|
83
184
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects accessibility (a11y) issues:
|
|
5
|
+
* - Images without alt text
|
|
6
|
+
* - Form inputs without labels
|
|
7
|
+
* - Missing ARIA attributes on interactive elements
|
|
8
|
+
* - Click handlers without keyboard support
|
|
9
|
+
* - Improper heading hierarchy
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectAccessibilityIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
|
|
12
|
+
//# sourceMappingURL=accessibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../../src/detectors/accessibility.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAkMb"}
|