react-code-smell-detector 1.4.2 → 1.5.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/README.md +227 -22
- package/dist/__tests__/aiRefactoring.test.d.ts +2 -0
- package/dist/__tests__/aiRefactoring.test.d.ts.map +1 -0
- package/dist/__tests__/aiRefactoring.test.js +86 -0
- package/dist/__tests__/analyzer-real.test.d.ts +2 -0
- package/dist/__tests__/analyzer-real.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer-real.test.js +149 -0
- package/dist/__tests__/analyzer.test.d.ts +2 -0
- package/dist/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer.test.js +173 -0
- package/dist/__tests__/baseline.test.d.ts +2 -0
- package/dist/__tests__/baseline.test.d.ts.map +1 -0
- package/dist/__tests__/baseline.test.js +136 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts +2 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts.map +1 -0
- package/dist/__tests__/bundleAnalyzer.test.js +182 -0
- package/dist/__tests__/customRules.test.d.ts +2 -0
- package/dist/__tests__/customRules.test.d.ts.map +1 -0
- package/dist/__tests__/customRules.test.js +283 -0
- package/dist/__tests__/detectors/index.test.d.ts +2 -0
- package/dist/__tests__/detectors/index.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/index.test.js +1012 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts +2 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/newDetectors.test.js +333 -0
- package/dist/__tests__/docGenerator.test.d.ts +2 -0
- package/dist/__tests__/docGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/docGenerator.test.js +157 -0
- package/dist/__tests__/fixer.test.d.ts +2 -0
- package/dist/__tests__/fixer.test.d.ts.map +1 -0
- package/dist/__tests__/fixer.test.js +193 -0
- package/dist/__tests__/git.test.d.ts +2 -0
- package/dist/__tests__/git.test.d.ts.map +1 -0
- package/dist/__tests__/git.test.js +38 -0
- package/dist/__tests__/graphGenerator.test.d.ts +2 -0
- package/dist/__tests__/graphGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/graphGenerator.test.js +190 -0
- package/dist/__tests__/htmlReporter.test.d.ts +2 -0
- package/dist/__tests__/htmlReporter.test.d.ts.map +1 -0
- package/dist/__tests__/htmlReporter.test.js +258 -0
- package/dist/__tests__/interactiveFixer.test.d.ts +2 -0
- package/dist/__tests__/interactiveFixer.test.d.ts.map +1 -0
- package/dist/__tests__/interactiveFixer.test.js +231 -0
- package/dist/__tests__/parser.test.d.ts +2 -0
- package/dist/__tests__/parser.test.d.ts.map +1 -0
- package/dist/__tests__/parser.test.js +56 -0
- package/dist/__tests__/performanceBudget.test.d.ts +2 -0
- package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
- package/dist/__tests__/performanceBudget.test.js +242 -0
- package/dist/__tests__/prComments.test.d.ts +2 -0
- package/dist/__tests__/prComments.test.d.ts.map +1 -0
- package/dist/__tests__/prComments.test.js +118 -0
- package/dist/__tests__/reporter.test.d.ts +2 -0
- package/dist/__tests__/reporter.test.d.ts.map +1 -0
- package/dist/__tests__/reporter.test.js +136 -0
- package/dist/__tests__/watcher.test.d.ts +2 -0
- package/dist/__tests__/watcher.test.d.ts.map +1 -0
- package/dist/__tests__/watcher.test.js +161 -0
- package/dist/__tests__/webhooks.test.d.ts +2 -0
- package/dist/__tests__/webhooks.test.d.ts.map +1 -0
- package/dist/__tests__/webhooks.test.js +209 -0
- package/dist/aiRefactoring.d.ts +29 -0
- package/dist/aiRefactoring.d.ts.map +1 -0
- package/dist/aiRefactoring.js +290 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +33 -1
- package/dist/cli.js +123 -1
- package/dist/detectors/contextApi.d.ts +11 -0
- package/dist/detectors/contextApi.d.ts.map +1 -0
- package/dist/detectors/contextApi.js +151 -0
- package/dist/detectors/errorBoundary.d.ts +11 -0
- package/dist/detectors/errorBoundary.d.ts.map +1 -0
- package/dist/detectors/errorBoundary.js +167 -0
- package/dist/detectors/formValidation.d.ts +11 -0
- package/dist/detectors/formValidation.d.ts.map +1 -0
- package/dist/detectors/formValidation.js +193 -0
- package/dist/detectors/index.d.ts +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +12 -0
- package/dist/detectors/serverComponents.d.ts +11 -0
- package/dist/detectors/serverComponents.d.ts.map +1 -0
- package/dist/detectors/serverComponents.js +222 -0
- package/dist/detectors/stateManagement.d.ts +11 -0
- package/dist/detectors/stateManagement.d.ts.map +1 -0
- package/dist/detectors/stateManagement.js +193 -0
- package/dist/detectors/testingGaps.d.ts +15 -0
- package/dist/detectors/testingGaps.d.ts.map +1 -0
- package/dist/detectors/testingGaps.js +182 -0
- package/dist/docGenerator.d.ts +37 -0
- package/dist/docGenerator.d.ts.map +1 -0
- package/dist/docGenerator.js +306 -0
- package/dist/guide.d.ts +9 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +922 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/interactiveFixer.d.ts +20 -0
- package/dist/interactiveFixer.d.ts.map +1 -0
- package/dist/interactiveFixer.js +178 -0
- package/dist/performanceBudget.d.ts +54 -0
- package/dist/performanceBudget.d.ts.map +1 -0
- package/dist/performanceBudget.js +218 -0
- package/dist/prComments.d.ts +47 -0
- package/dist/prComments.d.ts.map +1 -0
- package/dist/prComments.js +233 -0
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +18 -0
- package/package.json +10 -4
package/dist/guide.js
ADDED
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
/**
|
|
6
|
+
* Interactive guide for React Code Smell Detector
|
|
7
|
+
*/
|
|
8
|
+
export async function runGuide() {
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
});
|
|
13
|
+
const ask = (question) => {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
console.clear();
|
|
19
|
+
printHeader();
|
|
20
|
+
let running = true;
|
|
21
|
+
while (running) {
|
|
22
|
+
console.log('\n' + chalk.cyan('What would you like to learn about?') + '\n');
|
|
23
|
+
console.log(chalk.white(' 1. ') + 'Quick Start - Basic usage');
|
|
24
|
+
console.log(chalk.white(' 2. ') + 'Output Formats - JSON, Markdown, HTML');
|
|
25
|
+
console.log(chalk.white(' 3. ') + 'Auto-Fix - Fix simple issues automatically');
|
|
26
|
+
console.log(chalk.white(' 4. ') + 'Watch Mode - Real-time analysis');
|
|
27
|
+
console.log(chalk.white(' 5. ') + 'Git Integration - Analyze only changed files');
|
|
28
|
+
console.log(chalk.white(' 6. ') + 'CI/CD Integration - Pipelines & PR comments');
|
|
29
|
+
console.log(chalk.white(' 7. ') + 'Performance Budget - Enforce quality thresholds');
|
|
30
|
+
console.log(chalk.white(' 8. ') + 'Custom Rules - Define your own rules');
|
|
31
|
+
console.log(chalk.white(' 9. ') + 'Advanced Features - AI, Graphs, Bundle Analysis');
|
|
32
|
+
console.log(chalk.white(' 10. ') + 'Configuration - All config options');
|
|
33
|
+
console.log(chalk.white(' 11. ') + 'Create Demo Project');
|
|
34
|
+
console.log(chalk.white(' 0. ') + 'Exit\n');
|
|
35
|
+
const choice = await ask(chalk.yellow('Enter your choice (0-11): '));
|
|
36
|
+
switch (choice) {
|
|
37
|
+
case '1':
|
|
38
|
+
showQuickStart();
|
|
39
|
+
break;
|
|
40
|
+
case '2':
|
|
41
|
+
showOutputFormats();
|
|
42
|
+
break;
|
|
43
|
+
case '3':
|
|
44
|
+
showAutoFix();
|
|
45
|
+
break;
|
|
46
|
+
case '4':
|
|
47
|
+
showWatchMode();
|
|
48
|
+
break;
|
|
49
|
+
case '5':
|
|
50
|
+
showGitIntegration();
|
|
51
|
+
break;
|
|
52
|
+
case '6':
|
|
53
|
+
showCICD();
|
|
54
|
+
break;
|
|
55
|
+
case '7':
|
|
56
|
+
showPerformanceBudget();
|
|
57
|
+
break;
|
|
58
|
+
case '8':
|
|
59
|
+
showCustomRules();
|
|
60
|
+
break;
|
|
61
|
+
case '9':
|
|
62
|
+
showAdvancedFeatures();
|
|
63
|
+
break;
|
|
64
|
+
case '10':
|
|
65
|
+
showConfiguration();
|
|
66
|
+
break;
|
|
67
|
+
case '11':
|
|
68
|
+
rl.close();
|
|
69
|
+
await createDemoProject(process.cwd());
|
|
70
|
+
return;
|
|
71
|
+
case '0':
|
|
72
|
+
running = false;
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
console.log(chalk.red('\nInvalid choice. Please enter a number from 0-11.'));
|
|
76
|
+
}
|
|
77
|
+
if (running && choice !== '0') {
|
|
78
|
+
await ask(chalk.dim('\nPress Enter to continue...'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.green('\nHappy coding! 🚀\n'));
|
|
82
|
+
rl.close();
|
|
83
|
+
}
|
|
84
|
+
function printHeader() {
|
|
85
|
+
console.log(chalk.cyan.bold(`
|
|
86
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
87
|
+
║ 🔍 React Code Smell Detector Guide 🔍 ║
|
|
88
|
+
║ Interactive Tutorial ║
|
|
89
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
90
|
+
`));
|
|
91
|
+
console.log(chalk.white('Welcome! This guide will help you learn all features of'));
|
|
92
|
+
console.log(chalk.white('React Code Smell Detector with practical examples.\n'));
|
|
93
|
+
}
|
|
94
|
+
function showQuickStart() {
|
|
95
|
+
console.log(chalk.cyan.bold('\n═══ Quick Start ═══\n'));
|
|
96
|
+
console.log(chalk.white('1. Analyze current directory:'));
|
|
97
|
+
console.log(chalk.green(' $ react-smell .'));
|
|
98
|
+
console.log(chalk.white('\n2. Analyze a specific directory:'));
|
|
99
|
+
console.log(chalk.green(' $ react-smell ./src'));
|
|
100
|
+
console.log(chalk.white('\n3. Show code snippets with issues:'));
|
|
101
|
+
console.log(chalk.green(' $ react-smell ./src -s'));
|
|
102
|
+
console.log(chalk.white('\n4. Use a configuration file:'));
|
|
103
|
+
console.log(chalk.green(' $ react-smell ./src -c .smellrc.json'));
|
|
104
|
+
console.log(chalk.white('\n5. Create default configuration:'));
|
|
105
|
+
console.log(chalk.green(' $ react-smell init'));
|
|
106
|
+
printBox('Example Output', `
|
|
107
|
+
📊 Analysis Summary
|
|
108
|
+
├── Files analyzed: 24
|
|
109
|
+
├── Components found: 45
|
|
110
|
+
├── Code smells: 12
|
|
111
|
+
└── Technical Debt Grade: B (82/100)
|
|
112
|
+
|
|
113
|
+
🔴 Error (2)
|
|
114
|
+
├── security-xss: Avoid dangerouslySetInnerHTML
|
|
115
|
+
└── missing-key: Add key prop to list items
|
|
116
|
+
|
|
117
|
+
🟡 Warning (7)
|
|
118
|
+
├── useEffect-overuse: Too many effects
|
|
119
|
+
└── prop-drilling: Props passed through 4 levels
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
122
|
+
function showOutputFormats() {
|
|
123
|
+
console.log(chalk.cyan.bold('\n═══ Output Formats ═══\n'));
|
|
124
|
+
console.log(chalk.white('1. Console (default) - Colored terminal output:'));
|
|
125
|
+
console.log(chalk.green(' $ react-smell ./src'));
|
|
126
|
+
console.log(chalk.white('\n2. JSON - Machine-readable for CI/CD:'));
|
|
127
|
+
console.log(chalk.green(' $ react-smell ./src -f json'));
|
|
128
|
+
console.log(chalk.green(' $ react-smell ./src -f json -o report.json'));
|
|
129
|
+
console.log(chalk.white('\n3. Markdown - For documentation:'));
|
|
130
|
+
console.log(chalk.green(' $ react-smell ./src -f markdown -o report.md'));
|
|
131
|
+
console.log(chalk.white('\n4. HTML - Beautiful visual report:'));
|
|
132
|
+
console.log(chalk.green(' $ react-smell ./src -f html -o report.html'));
|
|
133
|
+
printBox('JSON Output Example', `
|
|
134
|
+
{
|
|
135
|
+
"summary": {
|
|
136
|
+
"totalFiles": 24,
|
|
137
|
+
"totalComponents": 45,
|
|
138
|
+
"totalSmells": 12
|
|
139
|
+
},
|
|
140
|
+
"debtScore": {
|
|
141
|
+
"score": 82,
|
|
142
|
+
"grade": "B"
|
|
143
|
+
},
|
|
144
|
+
"files": [...]
|
|
145
|
+
}
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
function showAutoFix() {
|
|
149
|
+
console.log(chalk.cyan.bold('\n═══ Auto-Fix Features ═══\n'));
|
|
150
|
+
console.log(chalk.white('Fixable issues: console.log, var→let, ==→===, missing alt\n'));
|
|
151
|
+
console.log(chalk.white('1. Auto-fix all fixable issues:'));
|
|
152
|
+
console.log(chalk.green(' $ react-smell ./src --fix'));
|
|
153
|
+
console.log(chalk.white('\n2. Interactive fix mode (review each fix):'));
|
|
154
|
+
console.log(chalk.green(' $ react-smell ./src --fix-interactive'));
|
|
155
|
+
console.log(chalk.white('\n3. Preview fixes without applying:'));
|
|
156
|
+
console.log(chalk.green(' $ react-smell ./src --fix-preview'));
|
|
157
|
+
printBox('Interactive Fix Example', `
|
|
158
|
+
📝 Fix 1/5: debug-statement in Header.tsx:15
|
|
159
|
+
|
|
160
|
+
Before: console.log('User logged in');
|
|
161
|
+
After: // removed
|
|
162
|
+
|
|
163
|
+
[Y] Apply [N] Skip [A] Apply All [Q] Quit
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
function showWatchMode() {
|
|
167
|
+
console.log(chalk.cyan.bold('\n═══ Watch Mode ═══\n'));
|
|
168
|
+
console.log(chalk.white('Re-analyze automatically when files change:\n'));
|
|
169
|
+
console.log(chalk.green(' $ react-smell ./src --watch'));
|
|
170
|
+
console.log(chalk.white('\nWith specific options:'));
|
|
171
|
+
console.log(chalk.green(' $ react-smell ./src --watch -s'));
|
|
172
|
+
console.log(chalk.green(' $ react-smell ./src --watch --max-effects 2'));
|
|
173
|
+
printBox('Watch Mode Output', `
|
|
174
|
+
👀 Watching for changes in ./src...
|
|
175
|
+
|
|
176
|
+
[14:32:01] File changed: src/components/Header.tsx
|
|
177
|
+
[14:32:01] Analyzing...
|
|
178
|
+
[14:32:02] ✓ 3 smells found (1 error, 2 warnings)
|
|
179
|
+
|
|
180
|
+
Press Ctrl+C to stop watching.
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
function showGitIntegration() {
|
|
184
|
+
console.log(chalk.cyan.bold('\n═══ Git Integration ═══\n'));
|
|
185
|
+
console.log(chalk.white('1. Analyze only git-modified files:'));
|
|
186
|
+
console.log(chalk.green(' $ react-smell --changed'));
|
|
187
|
+
console.log(chalk.white('\n2. Great for pre-commit hooks:'));
|
|
188
|
+
console.log(chalk.green(' # .husky/pre-commit'));
|
|
189
|
+
console.log(chalk.green(' react-smell --changed --ci --fail-on error'));
|
|
190
|
+
console.log(chalk.white('\n3. Baseline tracking (track trends over time):'));
|
|
191
|
+
console.log(chalk.green(' $ react-smell ./src --baseline'));
|
|
192
|
+
printBox('Trend Report', `
|
|
193
|
+
📈 Code Quality Trend (last 5 commits)
|
|
194
|
+
|
|
195
|
+
Commit Date Smells Grade
|
|
196
|
+
─────────────────────────────────────
|
|
197
|
+
a1b2c3d Feb 20 45 C
|
|
198
|
+
e4f5g6h Feb 19 42 C
|
|
199
|
+
i7j8k9l Feb 18 38 B
|
|
200
|
+
m0n1o2p Feb 17 35 B
|
|
201
|
+
q3r4s5t Feb 16 32 B
|
|
202
|
+
|
|
203
|
+
Trend: ⬆️ +13 smells (+40%) since Feb 16
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
function showCICD() {
|
|
207
|
+
console.log(chalk.cyan.bold('\n═══ CI/CD Integration ═══\n'));
|
|
208
|
+
console.log(chalk.white('1. Exit with error code when issues found:'));
|
|
209
|
+
console.log(chalk.green(' $ react-smell ./src --ci'));
|
|
210
|
+
console.log(chalk.white('\n2. Custom severity threshold:'));
|
|
211
|
+
console.log(chalk.green(' $ react-smell ./src --ci --fail-on warning'));
|
|
212
|
+
console.log(chalk.white('\n3. GitHub Actions PR Comments:'));
|
|
213
|
+
console.log(chalk.green(' $ react-smell ./src --pr-comment'));
|
|
214
|
+
console.log(chalk.white('\n4. Slack/Discord notifications:'));
|
|
215
|
+
console.log(chalk.green(' $ react-smell ./src --slack $SLACK_WEBHOOK_URL'));
|
|
216
|
+
console.log(chalk.green(' $ react-smell ./src --discord $DISCORD_WEBHOOK_URL'));
|
|
217
|
+
printBox('GitHub Actions Workflow', `
|
|
218
|
+
name: Code Quality
|
|
219
|
+
on: [pull_request]
|
|
220
|
+
jobs:
|
|
221
|
+
analyze:
|
|
222
|
+
runs-on: ubuntu-latest
|
|
223
|
+
steps:
|
|
224
|
+
- uses: actions/checkout@v4
|
|
225
|
+
- uses: actions/setup-node@v4
|
|
226
|
+
- run: npm install -g react-code-smell-detector
|
|
227
|
+
- run: react-smell ./src --ci --pr-comment
|
|
228
|
+
env:
|
|
229
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
230
|
+
`);
|
|
231
|
+
}
|
|
232
|
+
function showPerformanceBudget() {
|
|
233
|
+
console.log(chalk.cyan.bold('\n═══ Performance Budget ═══\n'));
|
|
234
|
+
console.log(chalk.white('Set quality thresholds and enforce in CI/CD:\n'));
|
|
235
|
+
console.log(chalk.white('1. Create budget configuration:'));
|
|
236
|
+
console.log(chalk.green(' $ react-smell init-budget'));
|
|
237
|
+
console.log(chalk.white('\n2. Check against budget:'));
|
|
238
|
+
console.log(chalk.green(' $ react-smell ./src --budget'));
|
|
239
|
+
console.log(chalk.white('\n3. Custom budget config:'));
|
|
240
|
+
console.log(chalk.green(' $ react-smell ./src --budget --budget-config budget.json'));
|
|
241
|
+
printBox('.smellbudget.json Example', `
|
|
242
|
+
{
|
|
243
|
+
"maxTotalSmells": 50,
|
|
244
|
+
"maxErrors": 0,
|
|
245
|
+
"maxWarnings": 30,
|
|
246
|
+
"minGrade": "B",
|
|
247
|
+
"maxSmellsPerFile": 10,
|
|
248
|
+
"maxByType": {
|
|
249
|
+
"security-xss": 0,
|
|
250
|
+
"security-eval": 0,
|
|
251
|
+
"debug-statement": 0
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
`);
|
|
255
|
+
}
|
|
256
|
+
function showCustomRules() {
|
|
257
|
+
console.log(chalk.cyan.bold('\n═══ Custom Rules ═══\n'));
|
|
258
|
+
console.log(chalk.white('Define project-specific code quality rules:\n'));
|
|
259
|
+
console.log(chalk.white('1. Create rules file:'));
|
|
260
|
+
console.log(chalk.green(' $ touch .smellrules.json'));
|
|
261
|
+
console.log(chalk.white('\n2. Use custom rules:'));
|
|
262
|
+
console.log(chalk.green(' $ react-smell ./src --rules .smellrules.json'));
|
|
263
|
+
printBox('.smellrules.json Example', `
|
|
264
|
+
{
|
|
265
|
+
"rules": [
|
|
266
|
+
{
|
|
267
|
+
"id": "no-deprecated-api",
|
|
268
|
+
"name": "No Deprecated API",
|
|
269
|
+
"description": "Avoid using deprecated React APIs",
|
|
270
|
+
"severity": "warning",
|
|
271
|
+
"pattern": {
|
|
272
|
+
"type": "text",
|
|
273
|
+
"value": "componentWillMount"
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"id": "no-inline-styles",
|
|
278
|
+
"name": "No Inline Styles",
|
|
279
|
+
"description": "Use CSS modules or styled-components",
|
|
280
|
+
"severity": "info",
|
|
281
|
+
"pattern": {
|
|
282
|
+
"type": "regex",
|
|
283
|
+
"value": "style=\\\\{\\\\{"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
`);
|
|
289
|
+
}
|
|
290
|
+
function showAdvancedFeatures() {
|
|
291
|
+
console.log(chalk.cyan.bold('\n═══ Advanced Features ═══\n'));
|
|
292
|
+
console.log(chalk.white('1. Dependency Graph Visualization:'));
|
|
293
|
+
console.log(chalk.green(' $ react-smell ./src --graph'));
|
|
294
|
+
console.log(chalk.green(' $ react-smell ./src --graph --graph-format svg -o deps.svg'));
|
|
295
|
+
console.log(chalk.white('\n2. Bundle Size Impact Analysis:'));
|
|
296
|
+
console.log(chalk.green(' $ react-smell ./src --bundle'));
|
|
297
|
+
console.log(chalk.white('\n3. Component Documentation Generation:'));
|
|
298
|
+
console.log(chalk.green(' $ react-smell docs ./src'));
|
|
299
|
+
console.log(chalk.green(' $ react-smell docs ./src -f html -o ./docs'));
|
|
300
|
+
console.log(chalk.white('\n4. AI-Powered Refactoring (requires API key):'));
|
|
301
|
+
console.log(chalk.green(' $ react-smell ./src --ai --ai-key $OPENAI_API_KEY'));
|
|
302
|
+
console.log(chalk.green(' $ react-smell ./src --ai --ai-model gpt-4'));
|
|
303
|
+
printBox('Bundle Analysis Output', `
|
|
304
|
+
📦 Bundle Size Impact by Component
|
|
305
|
+
|
|
306
|
+
Component Size Dependencies
|
|
307
|
+
────────────────────────────────────────────
|
|
308
|
+
Dashboard 45.2 KB lodash, recharts
|
|
309
|
+
UserProfile 12.8 KB formik, yup
|
|
310
|
+
Header 3.2 KB react-icons
|
|
311
|
+
Footer 1.5 KB -
|
|
312
|
+
|
|
313
|
+
⚠️ Suggestions:
|
|
314
|
+
- Dashboard: Consider code-splitting recharts
|
|
315
|
+
- UserProfile: formik can be tree-shaken
|
|
316
|
+
`);
|
|
317
|
+
}
|
|
318
|
+
function showConfiguration() {
|
|
319
|
+
console.log(chalk.cyan.bold('\n═══ Configuration Options ═══\n'));
|
|
320
|
+
console.log(chalk.white('All CLI Options:'));
|
|
321
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
322
|
+
const options = [
|
|
323
|
+
['-f, --format <fmt>', 'Output: console, json, markdown, html'],
|
|
324
|
+
['-s, --snippets', 'Show code snippets'],
|
|
325
|
+
['-c, --config <file>', 'Config file path'],
|
|
326
|
+
['--ci', 'CI mode (exit code 1 on issues)'],
|
|
327
|
+
['--fail-on <severity>', 'Exit threshold: error|warning|info'],
|
|
328
|
+
['--fix', 'Auto-fix simple issues'],
|
|
329
|
+
['--fix-interactive', 'Interactive fix mode'],
|
|
330
|
+
['--fix-preview', 'Preview fixes'],
|
|
331
|
+
['--watch', 'Watch mode'],
|
|
332
|
+
['--changed', 'Git changed files only'],
|
|
333
|
+
['--max-effects <n>', 'Max useEffects per component'],
|
|
334
|
+
['--max-props <n>', 'Max props before warning'],
|
|
335
|
+
['--max-lines <n>', 'Max lines per component'],
|
|
336
|
+
['--include <patterns>', 'Glob patterns to include'],
|
|
337
|
+
['--exclude <patterns>', 'Glob patterns to exclude'],
|
|
338
|
+
['-o, --output <file>', 'Write to file'],
|
|
339
|
+
['--baseline', 'Enable trend tracking'],
|
|
340
|
+
['--slack <url>', 'Slack webhook'],
|
|
341
|
+
['--discord <url>', 'Discord webhook'],
|
|
342
|
+
['--graph', 'Generate dependency graph'],
|
|
343
|
+
['--bundle', 'Bundle size analysis'],
|
|
344
|
+
['--budget', 'Check performance budget'],
|
|
345
|
+
['--docs', 'Generate documentation'],
|
|
346
|
+
['--ai', 'AI refactoring suggestions'],
|
|
347
|
+
];
|
|
348
|
+
for (const [flag, desc] of options) {
|
|
349
|
+
console.log(`${chalk.yellow(flag.padEnd(25))} ${chalk.white(desc)}`);
|
|
350
|
+
}
|
|
351
|
+
printBox('.smellrc.json Full Example', `
|
|
352
|
+
{
|
|
353
|
+
"maxUseEffectsPerComponent": 3,
|
|
354
|
+
"maxPropDrillingDepth": 3,
|
|
355
|
+
"maxComponentLines": 300,
|
|
356
|
+
"maxPropsCount": 7,
|
|
357
|
+
"checkMemoization": true,
|
|
358
|
+
"checkDebugStatements": true,
|
|
359
|
+
"checkSecurity": true,
|
|
360
|
+
"checkAccessibility": true,
|
|
361
|
+
"checkComplexity": true,
|
|
362
|
+
"maxCyclomaticComplexity": 10,
|
|
363
|
+
"checkMemoryLeaks": true,
|
|
364
|
+
"checkImports": true,
|
|
365
|
+
"checkContextApi": true,
|
|
366
|
+
"checkErrorBoundaries": true,
|
|
367
|
+
"checkForms": true,
|
|
368
|
+
"checkStateManagement": true,
|
|
369
|
+
"checkTestingGaps": true
|
|
370
|
+
}
|
|
371
|
+
`);
|
|
372
|
+
}
|
|
373
|
+
function printBox(title, content) {
|
|
374
|
+
console.log('\n' + chalk.cyan('┌─ ' + title + ' ' + '─'.repeat(Math.max(0, 50 - title.length)) + '┐'));
|
|
375
|
+
console.log(chalk.gray(content));
|
|
376
|
+
console.log(chalk.cyan('└' + '─'.repeat(55) + '┘'));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Create a demo project with examples of all detectable smells
|
|
380
|
+
*/
|
|
381
|
+
export async function createDemoProject(targetDir) {
|
|
382
|
+
const demoDir = path.join(targetDir, 'react-smell-demo');
|
|
383
|
+
console.log(chalk.cyan.bold('\n🚀 Creating Demo Project\n'));
|
|
384
|
+
try {
|
|
385
|
+
await fs.mkdir(demoDir, { recursive: true });
|
|
386
|
+
await fs.mkdir(path.join(demoDir, 'src', 'components'), { recursive: true });
|
|
387
|
+
await fs.mkdir(path.join(demoDir, 'src', 'hooks'), { recursive: true });
|
|
388
|
+
// Create package.json
|
|
389
|
+
await fs.writeFile(path.join(demoDir, 'package.json'), JSON.stringify({
|
|
390
|
+
name: 'react-smell-demo',
|
|
391
|
+
version: '1.0.0',
|
|
392
|
+
description: 'Demo project for React Code Smell Detector',
|
|
393
|
+
scripts: {
|
|
394
|
+
'analyze': 'react-smell ./src',
|
|
395
|
+
'analyze:fix': 'react-smell ./src --fix',
|
|
396
|
+
'analyze:html': 'react-smell ./src -f html -o report.html',
|
|
397
|
+
'analyze:watch': 'react-smell ./src --watch',
|
|
398
|
+
'analyze:ci': 'react-smell ./src --ci --budget',
|
|
399
|
+
},
|
|
400
|
+
}, null, 2));
|
|
401
|
+
// Create .smellrc.json
|
|
402
|
+
await fs.writeFile(path.join(demoDir, '.smellrc.json'), JSON.stringify({
|
|
403
|
+
maxUseEffectsPerComponent: 3,
|
|
404
|
+
maxPropDrillingDepth: 3,
|
|
405
|
+
maxComponentLines: 300,
|
|
406
|
+
checkDebugStatements: true,
|
|
407
|
+
checkSecurity: true,
|
|
408
|
+
checkAccessibility: true,
|
|
409
|
+
}, null, 2));
|
|
410
|
+
// Create .smellbudget.json
|
|
411
|
+
await fs.writeFile(path.join(demoDir, '.smellbudget.json'), JSON.stringify({
|
|
412
|
+
maxTotalSmells: 50,
|
|
413
|
+
maxErrors: 0,
|
|
414
|
+
minGrade: 'C',
|
|
415
|
+
maxByType: {
|
|
416
|
+
'security-xss': 0,
|
|
417
|
+
'security-eval': 0,
|
|
418
|
+
},
|
|
419
|
+
}, null, 2));
|
|
420
|
+
// Create demo components with various smells
|
|
421
|
+
await createDemoComponents(demoDir);
|
|
422
|
+
console.log(chalk.green('✓ Created demo project at: ' + demoDir));
|
|
423
|
+
console.log('\n' + chalk.cyan('📂 Project Structure:'));
|
|
424
|
+
console.log(`
|
|
425
|
+
${demoDir}/
|
|
426
|
+
├── package.json
|
|
427
|
+
├── .smellrc.json
|
|
428
|
+
├── .smellbudget.json
|
|
429
|
+
└── src/
|
|
430
|
+
├── components/
|
|
431
|
+
│ ├── GoodComponent.tsx (Clean code example)
|
|
432
|
+
│ ├── BadComponent.tsx (Multiple smells)
|
|
433
|
+
│ ├── SecurityIssues.tsx (Security vulnerabilities)
|
|
434
|
+
│ ├── AccessibilityBad.tsx (A11y issues)
|
|
435
|
+
│ ├── ProDrillingExample.tsx (Prop drilling)
|
|
436
|
+
│ ├── ContextExample.tsx (Context API issues)
|
|
437
|
+
│ ├── FormIssues.tsx (Form validation smells)
|
|
438
|
+
│ └── StateManagement.tsx (State management issues)
|
|
439
|
+
└── hooks/
|
|
440
|
+
└── useComplexHook.ts (Complex hook example)
|
|
441
|
+
`);
|
|
442
|
+
console.log(chalk.cyan('\n🏃 Try these commands:\n'));
|
|
443
|
+
console.log(chalk.green(' cd react-smell-demo'));
|
|
444
|
+
console.log(chalk.green(' react-smell ./src'));
|
|
445
|
+
console.log(chalk.green(' react-smell ./src -s'));
|
|
446
|
+
console.log(chalk.green(' react-smell ./src --fix'));
|
|
447
|
+
console.log(chalk.green(' react-smell ./src -f html -o report.html'));
|
|
448
|
+
console.log(chalk.green(' react-smell ./src --budget'));
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
console.error(chalk.red(`Error creating demo project: ${error.message}`));
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function createDemoComponents(demoDir) {
|
|
456
|
+
const componentsDir = path.join(demoDir, 'src', 'components');
|
|
457
|
+
const hooksDir = path.join(demoDir, 'src', 'hooks');
|
|
458
|
+
// Good component (clean code)
|
|
459
|
+
await fs.writeFile(path.join(componentsDir, 'GoodComponent.tsx'), `
|
|
460
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
461
|
+
|
|
462
|
+
interface Props {
|
|
463
|
+
title: string;
|
|
464
|
+
items: string[];
|
|
465
|
+
onItemClick: (item: string) => void;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* A well-structured React component following best practices.
|
|
470
|
+
* This component demonstrates clean code patterns.
|
|
471
|
+
*/
|
|
472
|
+
export function GoodComponent({ title, items, onItemClick }: Props) {
|
|
473
|
+
const [filter, setFilter] = useState('');
|
|
474
|
+
|
|
475
|
+
// Memoized filtered items
|
|
476
|
+
const filteredItems = useMemo(() =>
|
|
477
|
+
items.filter(item => item.toLowerCase().includes(filter.toLowerCase())),
|
|
478
|
+
[items, filter]
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// Memoized callback
|
|
482
|
+
const handleClick = useCallback((item: string) => {
|
|
483
|
+
onItemClick(item);
|
|
484
|
+
}, [onItemClick]);
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<div className="good-component">
|
|
488
|
+
<h2>{title}</h2>
|
|
489
|
+
<input
|
|
490
|
+
type="text"
|
|
491
|
+
value={filter}
|
|
492
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
493
|
+
placeholder="Filter items..."
|
|
494
|
+
aria-label="Filter items"
|
|
495
|
+
/>
|
|
496
|
+
<ul>
|
|
497
|
+
{filteredItems.map((item) => (
|
|
498
|
+
<li key={item}>
|
|
499
|
+
<button onClick={() => handleClick(item)} type="button">
|
|
500
|
+
{item}
|
|
501
|
+
</button>
|
|
502
|
+
</li>
|
|
503
|
+
))}
|
|
504
|
+
</ul>
|
|
505
|
+
</div>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
`);
|
|
509
|
+
// Bad component with multiple smells
|
|
510
|
+
await fs.writeFile(path.join(componentsDir, 'BadComponent.tsx'), `
|
|
511
|
+
import React, { useState, useEffect } from 'react';
|
|
512
|
+
|
|
513
|
+
// 🚨 This component demonstrates multiple code smells
|
|
514
|
+
|
|
515
|
+
export function BadComponent(props) {
|
|
516
|
+
const [data, setData] = useState(null);
|
|
517
|
+
const [loading, setLoading] = useState(false);
|
|
518
|
+
const [error, setError] = useState(null);
|
|
519
|
+
const [count, setCount] = useState(0);
|
|
520
|
+
|
|
521
|
+
// ⚠️ Smell: Multiple useEffects (useEffect-overuse)
|
|
522
|
+
useEffect(() => {
|
|
523
|
+
console.log('Component mounted'); // ⚠️ Smell: debug-statement
|
|
524
|
+
setLoading(true);
|
|
525
|
+
fetch('/api/data')
|
|
526
|
+
.then(r => r.json())
|
|
527
|
+
.then(setData)
|
|
528
|
+
.finally(() => setLoading(false));
|
|
529
|
+
}, []);
|
|
530
|
+
|
|
531
|
+
useEffect(() => {
|
|
532
|
+
console.log('Data changed:', data); // ⚠️ Smell: debug-statement
|
|
533
|
+
}, [data]);
|
|
534
|
+
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
document.title = 'Count: ' + count;
|
|
537
|
+
}, [count]);
|
|
538
|
+
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
// ⚠️ Smell: memory-leak-timer (no cleanup)
|
|
541
|
+
setInterval(() => {
|
|
542
|
+
setCount(c => c + 1);
|
|
543
|
+
}, 1000);
|
|
544
|
+
}, []);
|
|
545
|
+
|
|
546
|
+
// ⚠️ Smell: js-var-usage
|
|
547
|
+
var items = data?.items || [];
|
|
548
|
+
|
|
549
|
+
// ⚠️ Smell: js-loose-equality
|
|
550
|
+
if (error == null && loading == false) {
|
|
551
|
+
return (
|
|
552
|
+
<div>
|
|
553
|
+
<h1>Data loaded</h1>
|
|
554
|
+
{/* ⚠️ Smell: missing-key */}
|
|
555
|
+
{items.map(item => (
|
|
556
|
+
<div>{item.name}</div>
|
|
557
|
+
))}
|
|
558
|
+
{/* ⚠️ Smell: inline-function-prop */}
|
|
559
|
+
<button onClick={() => setCount(count + 1)}>
|
|
560
|
+
Count: {count}
|
|
561
|
+
</button>
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ⚠️ Smell: nested-ternary
|
|
567
|
+
return loading ? <div>Loading...</div> : error ? <div>Error!</div> : <div>Unknown state</div>;
|
|
568
|
+
}
|
|
569
|
+
`);
|
|
570
|
+
// Security issues component
|
|
571
|
+
await fs.writeFile(path.join(componentsDir, 'SecurityIssues.tsx'), `
|
|
572
|
+
import React from 'react';
|
|
573
|
+
|
|
574
|
+
// 🚨 This component demonstrates security vulnerabilities
|
|
575
|
+
|
|
576
|
+
export function SecurityIssues({ userInput, htmlContent }) {
|
|
577
|
+
// ⚠️ Smell: security-eval
|
|
578
|
+
const executeCode = (code: string) => {
|
|
579
|
+
return eval(code); // DANGEROUS!
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// ⚠️ Smell: security-secrets (exposed API key pattern)
|
|
583
|
+
const API_KEY = 'sk_live_abc123def456';
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<div>
|
|
587
|
+
{/* ⚠️ Smell: security-xss */}
|
|
588
|
+
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
|
589
|
+
|
|
590
|
+
<button onClick={() => executeCode(userInput)}>
|
|
591
|
+
Execute
|
|
592
|
+
</button>
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
`);
|
|
597
|
+
// Accessibility issues component
|
|
598
|
+
await fs.writeFile(path.join(componentsDir, 'AccessibilityBad.tsx'), `
|
|
599
|
+
import React from 'react';
|
|
600
|
+
|
|
601
|
+
// 🚨 This component demonstrates accessibility issues
|
|
602
|
+
|
|
603
|
+
export function AccessibilityBad() {
|
|
604
|
+
return (
|
|
605
|
+
<div>
|
|
606
|
+
{/* ⚠️ Smell: a11y-missing-alt */}
|
|
607
|
+
<img src="/logo.png" />
|
|
608
|
+
|
|
609
|
+
{/* ⚠️ Smell: a11y-missing-label */}
|
|
610
|
+
<input type="email" placeholder="Enter email" />
|
|
611
|
+
|
|
612
|
+
{/* ⚠️ Smell: a11y-keyboard (click without keyboard) */}
|
|
613
|
+
<div onClick={() => alert('clicked')}>
|
|
614
|
+
Click me
|
|
615
|
+
</div>
|
|
616
|
+
|
|
617
|
+
{/* ⚠️ Smell: a11y-semantic */}
|
|
618
|
+
<div className="button" onClick={() => {}}>
|
|
619
|
+
This should be a button
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
`);
|
|
625
|
+
// Prop drilling example
|
|
626
|
+
await fs.writeFile(path.join(componentsDir, 'PropDrillingExample.tsx'), `
|
|
627
|
+
import React from 'react';
|
|
628
|
+
|
|
629
|
+
// 🚨 This demonstrates prop drilling (passing props through many levels)
|
|
630
|
+
|
|
631
|
+
interface User {
|
|
632
|
+
name: string;
|
|
633
|
+
email: string;
|
|
634
|
+
avatar: string;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ⚠️ Smell: prop-drilling
|
|
638
|
+
export function PropDrillingExample({
|
|
639
|
+
user,
|
|
640
|
+
onUpdate,
|
|
641
|
+
onDelete,
|
|
642
|
+
onLogout,
|
|
643
|
+
theme,
|
|
644
|
+
language,
|
|
645
|
+
notifications,
|
|
646
|
+
settings
|
|
647
|
+
}: {
|
|
648
|
+
user: User;
|
|
649
|
+
onUpdate: () => void;
|
|
650
|
+
onDelete: () => void;
|
|
651
|
+
onLogout: () => void;
|
|
652
|
+
theme: string;
|
|
653
|
+
language: string;
|
|
654
|
+
notifications: boolean;
|
|
655
|
+
settings: object;
|
|
656
|
+
}) {
|
|
657
|
+
return (
|
|
658
|
+
<div>
|
|
659
|
+
<Level1
|
|
660
|
+
user={user}
|
|
661
|
+
onUpdate={onUpdate}
|
|
662
|
+
onDelete={onDelete}
|
|
663
|
+
onLogout={onLogout}
|
|
664
|
+
theme={theme}
|
|
665
|
+
language={language}
|
|
666
|
+
/>
|
|
667
|
+
</div>
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function Level1({ user, onUpdate, onDelete, onLogout, theme, language }) {
|
|
672
|
+
return (
|
|
673
|
+
<Level2
|
|
674
|
+
user={user}
|
|
675
|
+
onUpdate={onUpdate}
|
|
676
|
+
onDelete={onDelete}
|
|
677
|
+
onLogout={onLogout}
|
|
678
|
+
theme={theme}
|
|
679
|
+
/>
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function Level2({ user, onUpdate, onDelete, onLogout, theme }) {
|
|
684
|
+
return (
|
|
685
|
+
<Level3 user={user} onUpdate={onUpdate} onDelete={onDelete} />
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function Level3({ user, onUpdate, onDelete }) {
|
|
690
|
+
return (
|
|
691
|
+
<div>
|
|
692
|
+
<h3>{user.name}</h3>
|
|
693
|
+
<button onClick={onUpdate}>Update</button>
|
|
694
|
+
<button onClick={onDelete}>Delete</button>
|
|
695
|
+
</div>
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
`);
|
|
699
|
+
// Context API issues
|
|
700
|
+
await fs.writeFile(path.join(componentsDir, 'ContextExample.tsx'), `
|
|
701
|
+
import React, { useContext, createContext } from 'react';
|
|
702
|
+
|
|
703
|
+
// 🚨 This demonstrates Context API issues
|
|
704
|
+
|
|
705
|
+
const ThemeContext = createContext({});
|
|
706
|
+
const UserContext = createContext({});
|
|
707
|
+
const AuthContext = createContext({});
|
|
708
|
+
const SettingsContext = createContext({});
|
|
709
|
+
const LanguageContext = createContext({});
|
|
710
|
+
const CartContext = createContext({});
|
|
711
|
+
|
|
712
|
+
// ⚠️ Smell: context-overuse (too many contexts)
|
|
713
|
+
export function ContextHeavyComponent() {
|
|
714
|
+
const theme = useContext(ThemeContext);
|
|
715
|
+
const user = useContext(UserContext);
|
|
716
|
+
const auth = useContext(AuthContext);
|
|
717
|
+
const settings = useContext(SettingsContext);
|
|
718
|
+
const language = useContext(LanguageContext);
|
|
719
|
+
const cart = useContext(CartContext);
|
|
720
|
+
|
|
721
|
+
return (
|
|
722
|
+
<div style={{ color: theme.color }}>
|
|
723
|
+
<h1>Welcome, {user.name}</h1>
|
|
724
|
+
<p>Language: {language.current}</p>
|
|
725
|
+
<p>Cart items: {cart.items.length}</p>
|
|
726
|
+
</div>
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ⚠️ Smell: large-context-value (inline object in provider)
|
|
731
|
+
export function ContextProviderBad({ children }) {
|
|
732
|
+
return (
|
|
733
|
+
<ThemeContext.Provider value={{ color: 'blue', fontSize: 14, fontFamily: 'Arial' }}>
|
|
734
|
+
{children}
|
|
735
|
+
</ThemeContext.Provider>
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
`);
|
|
739
|
+
// Form issues
|
|
740
|
+
await fs.writeFile(path.join(componentsDir, 'FormIssues.tsx'), `
|
|
741
|
+
import React, { useState } from 'react';
|
|
742
|
+
|
|
743
|
+
// 🚨 This demonstrates form validation issues
|
|
744
|
+
|
|
745
|
+
export function FormIssues() {
|
|
746
|
+
const [email, setEmail] = useState('');
|
|
747
|
+
|
|
748
|
+
return (
|
|
749
|
+
<div>
|
|
750
|
+
{/* ⚠️ Smell: form-without-onsubmit */}
|
|
751
|
+
<form>
|
|
752
|
+
{/* ⚠️ Smell: uncontrolled-form (value without onChange) */}
|
|
753
|
+
<input type="text" value="fixed value" />
|
|
754
|
+
|
|
755
|
+
{/* ⚠️ Smell: input-without-label */}
|
|
756
|
+
<input
|
|
757
|
+
type="email"
|
|
758
|
+
value={email}
|
|
759
|
+
onChange={e => setEmail(e.target.value)}
|
|
760
|
+
/>
|
|
761
|
+
|
|
762
|
+
<button type="submit">Submit</button>
|
|
763
|
+
</form>
|
|
764
|
+
|
|
765
|
+
{/* ⚠️ Smell: missing-form-validation (no validation library) */}
|
|
766
|
+
<form onSubmit={(e) => {
|
|
767
|
+
e.preventDefault();
|
|
768
|
+
// No validation!
|
|
769
|
+
fetch('/api/submit', { method: 'POST', body: JSON.stringify({ email }) });
|
|
770
|
+
}}>
|
|
771
|
+
<input type="text" defaultValue="" />
|
|
772
|
+
<button type="submit">Send</button>
|
|
773
|
+
</form>
|
|
774
|
+
</div>
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
`);
|
|
778
|
+
// State management issues
|
|
779
|
+
await fs.writeFile(path.join(componentsDir, 'StateManagement.tsx'), `
|
|
780
|
+
import React, { useState, useEffect } from 'react';
|
|
781
|
+
|
|
782
|
+
// Simulating Redux hooks (these would come from react-redux)
|
|
783
|
+
const useSelector = (selector: any) => selector({ value: 1 });
|
|
784
|
+
const useDispatch = () => (action: any) => action;
|
|
785
|
+
|
|
786
|
+
// 🚨 This demonstrates state management anti-patterns
|
|
787
|
+
|
|
788
|
+
export function StateManagementBad() {
|
|
789
|
+
// ⚠️ Smell: excessive-redux-selectors (too many selectors)
|
|
790
|
+
const a = useSelector(state => state.a);
|
|
791
|
+
const b = useSelector(state => state.b);
|
|
792
|
+
const c = useSelector(state => state.c);
|
|
793
|
+
const d = useSelector(state => state.d);
|
|
794
|
+
|
|
795
|
+
const dispatch = useDispatch();
|
|
796
|
+
|
|
797
|
+
// ⚠️ Smell: derived-state-in-state
|
|
798
|
+
const [derivedValue, setDerivedValue] = useState(a + b);
|
|
799
|
+
|
|
800
|
+
// ⚠️ Smell: state-sync-anti-pattern
|
|
801
|
+
useEffect(() => {
|
|
802
|
+
setDerivedValue(a + b);
|
|
803
|
+
}, [a, b]);
|
|
804
|
+
|
|
805
|
+
return (
|
|
806
|
+
<div>
|
|
807
|
+
<p>Values: {a}, {b}, {c}, {d}</p>
|
|
808
|
+
<p>Derived: {derivedValue}</p>
|
|
809
|
+
{/* ⚠️ Smell: redux-in-render (inline dispatch) */}
|
|
810
|
+
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
|
|
811
|
+
Increment
|
|
812
|
+
</button>
|
|
813
|
+
</div>
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
`);
|
|
817
|
+
// Complex hook with testing gaps
|
|
818
|
+
await fs.writeFile(path.join(hooksDir, 'useComplexHook.ts'), `
|
|
819
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
820
|
+
|
|
821
|
+
// 🚨 This hook demonstrates testing gap issues
|
|
822
|
+
|
|
823
|
+
interface UseComplexHookReturn {
|
|
824
|
+
data: any;
|
|
825
|
+
loading: boolean;
|
|
826
|
+
error: Error | null;
|
|
827
|
+
refetch: () => void;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ⚠️ Smell: side-effect-heavy, tightly-coupled, complex-untestable
|
|
831
|
+
export function useComplexHook(url: string): UseComplexHookReturn {
|
|
832
|
+
const [data, setData] = useState(null);
|
|
833
|
+
const [loading, setLoading] = useState(false);
|
|
834
|
+
const [error, setError] = useState<Error | null>(null);
|
|
835
|
+
|
|
836
|
+
// Multiple side effects
|
|
837
|
+
useEffect(() => {
|
|
838
|
+
const controller = new AbortController();
|
|
839
|
+
|
|
840
|
+
setLoading(true);
|
|
841
|
+
fetch(url, { signal: controller.signal })
|
|
842
|
+
.then(r => r.json())
|
|
843
|
+
.then(setData)
|
|
844
|
+
.catch(setError)
|
|
845
|
+
.finally(() => setLoading(false));
|
|
846
|
+
|
|
847
|
+
// At least this has cleanup!
|
|
848
|
+
return () => controller.abort();
|
|
849
|
+
}, [url]);
|
|
850
|
+
|
|
851
|
+
useEffect(() => {
|
|
852
|
+
// Side effect: localStorage
|
|
853
|
+
if (data) {
|
|
854
|
+
localStorage.setItem('cachedData', JSON.stringify(data));
|
|
855
|
+
}
|
|
856
|
+
}, [data]);
|
|
857
|
+
|
|
858
|
+
useEffect(() => {
|
|
859
|
+
// Side effect: analytics
|
|
860
|
+
console.log('Analytics:', { url, hasData: !!data }); // ⚠️ debug-statement
|
|
861
|
+
}, [url, data]);
|
|
862
|
+
|
|
863
|
+
const refetch = useCallback(() => {
|
|
864
|
+
setLoading(true);
|
|
865
|
+
fetch(url)
|
|
866
|
+
.then(r => r.json())
|
|
867
|
+
.then(setData)
|
|
868
|
+
.catch(setError)
|
|
869
|
+
.finally(() => setLoading(false));
|
|
870
|
+
}, [url]);
|
|
871
|
+
|
|
872
|
+
return { data, loading, error, refetch };
|
|
873
|
+
}
|
|
874
|
+
`);
|
|
875
|
+
// Create a README for the demo
|
|
876
|
+
await fs.writeFile(path.join(demoDir, 'README.md'), `# React Code Smell Detector Demo
|
|
877
|
+
|
|
878
|
+
This demo project contains examples of various code smells that can be detected.
|
|
879
|
+
|
|
880
|
+
## Quick Start
|
|
881
|
+
|
|
882
|
+
\`\`\`bash
|
|
883
|
+
# Analyze the project
|
|
884
|
+
react-smell ./src
|
|
885
|
+
|
|
886
|
+
# With code snippets
|
|
887
|
+
react-smell ./src -s
|
|
888
|
+
|
|
889
|
+
# Auto-fix simple issues
|
|
890
|
+
react-smell ./src --fix
|
|
891
|
+
|
|
892
|
+
# Generate HTML report
|
|
893
|
+
react-smell ./src -f html -o report.html
|
|
894
|
+
|
|
895
|
+
# Check against budget
|
|
896
|
+
react-smell ./src --budget
|
|
897
|
+
\`\`\`
|
|
898
|
+
|
|
899
|
+
## Demo Files
|
|
900
|
+
|
|
901
|
+
| File | Demonstrates |
|
|
902
|
+
|------|--------------|
|
|
903
|
+
| GoodComponent.tsx | Clean code (no smells) |
|
|
904
|
+
| BadComponent.tsx | Multiple common smells |
|
|
905
|
+
| SecurityIssues.tsx | Security vulnerabilities |
|
|
906
|
+
| AccessibilityBad.tsx | A11y issues |
|
|
907
|
+
| PropDrillingExample.tsx | Prop drilling |
|
|
908
|
+
| ContextExample.tsx | Context API issues |
|
|
909
|
+
| FormIssues.tsx | Form validation smells |
|
|
910
|
+
| StateManagement.tsx | State management anti-patterns |
|
|
911
|
+
| useComplexHook.ts | Testing gap issues |
|
|
912
|
+
|
|
913
|
+
## Configuration Files
|
|
914
|
+
|
|
915
|
+
- \`.smellrc.json\` - Detector configuration
|
|
916
|
+
- \`.smellbudget.json\` - Performance budget
|
|
917
|
+
|
|
918
|
+
## Learn More
|
|
919
|
+
|
|
920
|
+
Run \`react-smell guide\` for an interactive tutorial.
|
|
921
|
+
`);
|
|
922
|
+
}
|