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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -11
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +65 -2
  5. package/dist/cli.js +134 -33
  6. package/dist/detectors/accessibility.d.ts +12 -0
  7. package/dist/detectors/accessibility.d.ts.map +1 -0
  8. package/dist/detectors/accessibility.js +191 -0
  9. package/dist/detectors/complexity.d.ts +17 -0
  10. package/dist/detectors/complexity.d.ts.map +1 -0
  11. package/dist/detectors/complexity.js +69 -0
  12. package/dist/detectors/debug.d.ts +10 -0
  13. package/dist/detectors/debug.d.ts.map +1 -0
  14. package/dist/detectors/debug.js +87 -0
  15. package/dist/detectors/imports.d.ts +22 -0
  16. package/dist/detectors/imports.d.ts.map +1 -0
  17. package/dist/detectors/imports.js +210 -0
  18. package/dist/detectors/index.d.ts +6 -0
  19. package/dist/detectors/index.d.ts.map +1 -1
  20. package/dist/detectors/index.js +8 -0
  21. package/dist/detectors/memoryLeak.d.ts +7 -0
  22. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  23. package/dist/detectors/memoryLeak.js +111 -0
  24. package/dist/detectors/security.d.ts +12 -0
  25. package/dist/detectors/security.d.ts.map +1 -0
  26. package/dist/detectors/security.js +161 -0
  27. package/dist/fixer.d.ts +23 -0
  28. package/dist/fixer.d.ts.map +1 -0
  29. package/dist/fixer.js +133 -0
  30. package/dist/git.d.ts +28 -0
  31. package/dist/git.d.ts.map +1 -0
  32. package/dist/git.js +117 -0
  33. package/dist/htmlReporter.d.ts +6 -0
  34. package/dist/htmlReporter.d.ts.map +1 -0
  35. package/dist/htmlReporter.js +453 -0
  36. package/dist/reporter.js +26 -0
  37. package/dist/types/index.d.ts +10 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/index.js +13 -0
  40. package/dist/watcher.d.ts +16 -0
  41. package/dist/watcher.d.ts.map +1 -0
  42. package/dist/watcher.js +89 -0
  43. package/package.json +8 -2
  44. package/src/analyzer.ts +0 -269
  45. package/src/cli.ts +0 -125
  46. package/src/detectors/deadCode.ts +0 -163
  47. package/src/detectors/dependencyArray.ts +0 -176
  48. package/src/detectors/hooksRules.ts +0 -101
  49. package/src/detectors/index.ts +0 -16
  50. package/src/detectors/javascript.ts +0 -169
  51. package/src/detectors/largeComponent.ts +0 -63
  52. package/src/detectors/magicValues.ts +0 -114
  53. package/src/detectors/memoization.ts +0 -177
  54. package/src/detectors/missingKey.ts +0 -105
  55. package/src/detectors/nestedTernary.ts +0 -75
  56. package/src/detectors/nextjs.ts +0 -124
  57. package/src/detectors/nodejs.ts +0 -199
  58. package/src/detectors/propDrilling.ts +0 -103
  59. package/src/detectors/reactNative.ts +0 -154
  60. package/src/detectors/typescript.ts +0 -151
  61. package/src/detectors/useEffect.ts +0 -117
  62. package/src/index.ts +0 -4
  63. package/src/parser/index.ts +0 -195
  64. package/src/reporter.ts +0 -278
  65. package/src/types/index.ts +0 -144
  66. 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 Markdown
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
- Use the JSON output format for CI pipelines:
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
- # GitHub Actions example
150
- - name: Check code smells
151
- run: |
152
- npx react-smell ./src -f json > smell-report.json
153
- if [ $(jq '.summary.smellsBySeverity.error' smell-report.json) -gt 0 ]; then
154
- echo "Code smell errors found!"
155
- exit 1
156
- fi
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
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAqBA,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;AAyLD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
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.0.0')
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
- const spinner = ora('Analyzing React project...').start();
35
- try {
36
- // Load config file if specified
37
- let fileConfig = {};
38
- if (options.config) {
39
- try {
40
- const configPath = path.resolve(process.cwd(), options.config);
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
- // Build config from options
50
- const config = {
51
- ...DEFAULT_CONFIG,
52
- ...fileConfig,
53
- ...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
54
- ...(options.maxProps && { maxPropsCount: options.maxProps }),
55
- ...(options.maxLines && { maxComponentLines: options.maxLines }),
56
- };
57
- const include = options.include?.split(',').map((p) => p.trim()) || undefined;
58
- const exclude = options.exclude?.split(',').map((p) => p.trim()) || undefined;
59
- const result = await analyzeProject({
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
- spinner.stop();
66
- const output = reportResults(result, {
67
- format: options.format,
68
- showCodeSnippets: options.snippets,
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
- // Exit with error code if there are errors
80
- if (result.summary.smellsBySeverity.error > 0) {
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"}