react-code-smell-detector 1.2.0 → 1.4.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -4
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +22 -1
  5. package/dist/baseline.d.ts +37 -0
  6. package/dist/baseline.d.ts.map +1 -0
  7. package/dist/baseline.js +112 -0
  8. package/dist/cli.js +125 -26
  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/imports.d.ts +22 -0
  13. package/dist/detectors/imports.d.ts.map +1 -0
  14. package/dist/detectors/imports.js +210 -0
  15. package/dist/detectors/index.d.ts +4 -0
  16. package/dist/detectors/index.d.ts.map +1 -1
  17. package/dist/detectors/index.js +5 -0
  18. package/dist/detectors/memoryLeak.d.ts +7 -0
  19. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  20. package/dist/detectors/memoryLeak.js +111 -0
  21. package/dist/detectors/unusedCode.d.ts +7 -0
  22. package/dist/detectors/unusedCode.d.ts.map +1 -0
  23. package/dist/detectors/unusedCode.js +78 -0
  24. package/dist/fixer.d.ts +23 -0
  25. package/dist/fixer.d.ts.map +1 -0
  26. package/dist/fixer.js +133 -0
  27. package/dist/git.d.ts +31 -0
  28. package/dist/git.d.ts.map +1 -0
  29. package/dist/git.js +137 -0
  30. package/dist/reporter.js +16 -0
  31. package/dist/types/index.d.ts +13 -1
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/index.js +18 -0
  34. package/dist/watcher.d.ts +16 -0
  35. package/dist/watcher.d.ts.map +1 -0
  36. package/dist/watcher.js +89 -0
  37. package/dist/webhooks.d.ts +20 -0
  38. package/dist/webhooks.d.ts.map +1 -0
  39. package/dist/webhooks.js +199 -0
  40. package/package.json +10 -2
  41. package/src/analyzer.ts +0 -324
  42. package/src/cli.ts +0 -159
  43. package/src/detectors/accessibility.ts +0 -212
  44. package/src/detectors/deadCode.ts +0 -163
  45. package/src/detectors/debug.ts +0 -103
  46. package/src/detectors/dependencyArray.ts +0 -176
  47. package/src/detectors/hooksRules.ts +0 -101
  48. package/src/detectors/index.ts +0 -20
  49. package/src/detectors/javascript.ts +0 -169
  50. package/src/detectors/largeComponent.ts +0 -63
  51. package/src/detectors/magicValues.ts +0 -114
  52. package/src/detectors/memoization.ts +0 -177
  53. package/src/detectors/missingKey.ts +0 -105
  54. package/src/detectors/nestedTernary.ts +0 -75
  55. package/src/detectors/nextjs.ts +0 -124
  56. package/src/detectors/nodejs.ts +0 -199
  57. package/src/detectors/propDrilling.ts +0 -103
  58. package/src/detectors/reactNative.ts +0 -154
  59. package/src/detectors/security.ts +0 -179
  60. package/src/detectors/typescript.ts +0 -151
  61. package/src/detectors/useEffect.ts +0 -117
  62. package/src/htmlReporter.ts +0 -464
  63. package/src/index.ts +0 -4
  64. package/src/parser/index.ts +0 -195
  65. package/src/reporter.ts +0 -291
  66. package/src/types/index.ts +0 -165
  67. package/tsconfig.json +0 -19
@@ -1,117 +0,0 @@
1
- import * as t from '@babel/types';
2
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
3
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
4
-
5
- export function detectUseEffectOveruse(
6
- component: ParsedComponent,
7
- filePath: string,
8
- sourceCode: string,
9
- config: DetectorConfig = DEFAULT_CONFIG
10
- ): CodeSmell[] {
11
- const smells: CodeSmell[] = [];
12
- const { useEffect } = component.hooks;
13
-
14
- // Check for too many useEffects
15
- if (useEffect.length > config.maxUseEffectsPerComponent) {
16
- smells.push({
17
- type: 'useEffect-overuse',
18
- severity: 'warning',
19
- message: `Component "${component.name}" has ${useEffect.length} useEffect hooks (max recommended: ${config.maxUseEffectsPerComponent})`,
20
- file: filePath,
21
- line: component.startLine,
22
- column: 0,
23
- suggestion: 'Consider extracting logic into custom hooks or combining related effects',
24
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
25
- });
26
- }
27
-
28
- // Check each useEffect for issues
29
- useEffect.forEach((effect, index) => {
30
- const loc = effect.loc;
31
- if (!loc) return;
32
-
33
- // Check for empty dependency array with async operations
34
- const deps = effect.arguments[1];
35
- const callback = effect.arguments[0];
36
-
37
- if (t.isArrayExpression(deps) && deps.elements.length === 0) {
38
- // Empty dependency array - check if it's justified
39
- let hasAsyncOperation = false;
40
- let hasStateUpdate = false;
41
-
42
- if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
43
- const body = callback.body;
44
- const checkNode = (node: t.Node) => {
45
- if (t.isAwaitExpression(node)) hasAsyncOperation = true;
46
- if (t.isCallExpression(node)) {
47
- const callee = node.callee;
48
- if (t.isIdentifier(callee) && callee.name.startsWith('set')) {
49
- hasStateUpdate = true;
50
- }
51
- }
52
- };
53
-
54
- if (t.isBlockStatement(body)) {
55
- body.body.forEach(stmt => {
56
- t.traverseFast(stmt, checkNode);
57
- });
58
- }
59
- }
60
-
61
- if (hasAsyncOperation && hasStateUpdate) {
62
- smells.push({
63
- type: 'useEffect-overuse',
64
- severity: 'info',
65
- message: `useEffect #${index + 1} in "${component.name}" has empty deps but contains async state updates`,
66
- file: filePath,
67
- line: loc.start.line,
68
- column: loc.start.column,
69
- suggestion: 'Consider using React Query, SWR, or a custom hook for data fetching',
70
- codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
71
- });
72
- }
73
- }
74
-
75
- // Check for missing cleanup
76
- if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
77
- let hasSubscription = false;
78
- let hasCleanup = false;
79
-
80
- const checkSubscription = (node: t.Node) => {
81
- if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
82
- const prop = node.callee.property;
83
- if (t.isIdentifier(prop)) {
84
- if (['addEventListener', 'subscribe', 'on', 'setInterval', 'setTimeout'].includes(prop.name)) {
85
- hasSubscription = true;
86
- }
87
- }
88
- }
89
- };
90
-
91
- const body = callback.body;
92
- if (t.isBlockStatement(body)) {
93
- body.body.forEach(stmt => {
94
- t.traverseFast(stmt, checkSubscription);
95
- if (t.isReturnStatement(stmt) && stmt.argument) {
96
- hasCleanup = true;
97
- }
98
- });
99
- }
100
-
101
- if (hasSubscription && !hasCleanup) {
102
- smells.push({
103
- type: 'useEffect-overuse',
104
- severity: 'error',
105
- message: `useEffect in "${component.name}" sets up subscription but has no cleanup function`,
106
- file: filePath,
107
- line: loc.start.line,
108
- column: loc.start.column,
109
- suggestion: 'Add a cleanup function to remove event listeners/subscriptions',
110
- codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
111
- });
112
- }
113
- }
114
- });
115
-
116
- return smells;
117
- }
@@ -1,464 +0,0 @@
1
- import { AnalysisResult, SmellSeverity } from './types/index.js';
2
- import path from 'path';
3
-
4
- /**
5
- * Generate a beautiful HTML report with charts and styling
6
- */
7
- export function generateHTMLReport(result: AnalysisResult, rootDir: string): string {
8
- const { summary, debtScore, files } = result;
9
-
10
- // Generate chart data
11
- const smellTypeData = Object.entries(summary.smellsByType)
12
- .filter(([_, count]) => count > 0)
13
- .sort((a, b) => b[1] - a[1])
14
- .slice(0, 10);
15
-
16
- const severityColors = {
17
- error: '#ef4444',
18
- warning: '#f59e0b',
19
- info: '#3b82f6',
20
- };
21
-
22
- const gradeColors: Record<string, string> = {
23
- A: '#22c55e',
24
- B: '#84cc16',
25
- C: '#eab308',
26
- D: '#f97316',
27
- F: '#ef4444',
28
- };
29
-
30
- return `<!DOCTYPE html>
31
- <html lang="en">
32
- <head>
33
- <meta charset="UTF-8">
34
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
35
- <title>Code Smell Detector Report</title>
36
- <style>
37
- * {
38
- margin: 0;
39
- padding: 0;
40
- box-sizing: border-box;
41
- }
42
-
43
- body {
44
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
45
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
46
- min-height: 100vh;
47
- color: #e2e8f0;
48
- line-height: 1.6;
49
- }
50
-
51
- .container {
52
- max-width: 1200px;
53
- margin: 0 auto;
54
- padding: 2rem;
55
- }
56
-
57
- header {
58
- text-align: center;
59
- padding: 3rem 0;
60
- border-bottom: 1px solid rgba(255,255,255,0.1);
61
- margin-bottom: 2rem;
62
- }
63
-
64
- h1 {
65
- font-size: 2.5rem;
66
- margin-bottom: 0.5rem;
67
- background: linear-gradient(90deg, #60a5fa, #a78bfa);
68
- -webkit-background-clip: text;
69
- -webkit-text-fill-color: transparent;
70
- }
71
-
72
- .subtitle {
73
- color: #94a3b8;
74
- font-size: 1.1rem;
75
- }
76
-
77
- .dashboard {
78
- display: grid;
79
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
80
- gap: 1.5rem;
81
- margin-bottom: 2rem;
82
- }
83
-
84
- .card {
85
- background: rgba(255,255,255,0.05);
86
- border-radius: 1rem;
87
- padding: 1.5rem;
88
- border: 1px solid rgba(255,255,255,0.1);
89
- backdrop-filter: blur(10px);
90
- }
91
-
92
- .card h2 {
93
- font-size: 1rem;
94
- text-transform: uppercase;
95
- letter-spacing: 0.1em;
96
- color: #94a3b8;
97
- margin-bottom: 1rem;
98
- }
99
-
100
- .grade-container {
101
- display: flex;
102
- align-items: center;
103
- gap: 2rem;
104
- }
105
-
106
- .grade {
107
- font-size: 5rem;
108
- font-weight: 800;
109
- color: ${gradeColors[debtScore.grade]};
110
- text-shadow: 0 0 30px ${gradeColors[debtScore.grade]}40;
111
- }
112
-
113
- .score-details {
114
- flex: 1;
115
- }
116
-
117
- .score-bar {
118
- height: 8px;
119
- background: rgba(255,255,255,0.1);
120
- border-radius: 4px;
121
- overflow: hidden;
122
- margin: 0.5rem 0;
123
- }
124
-
125
- .score-bar-fill {
126
- height: 100%;
127
- border-radius: 4px;
128
- transition: width 1s ease;
129
- }
130
-
131
- .score-label {
132
- display: flex;
133
- justify-content: space-between;
134
- font-size: 0.9rem;
135
- color: #94a3b8;
136
- }
137
-
138
- .stat-grid {
139
- display: grid;
140
- grid-template-columns: repeat(3, 1fr);
141
- gap: 1rem;
142
- text-align: center;
143
- }
144
-
145
- .stat-value {
146
- font-size: 2rem;
147
- font-weight: 700;
148
- color: #60a5fa;
149
- }
150
-
151
- .stat-label {
152
- font-size: 0.8rem;
153
- color: #94a3b8;
154
- text-transform: uppercase;
155
- }
156
-
157
- .severity-badges {
158
- display: flex;
159
- gap: 1rem;
160
- margin-top: 1rem;
161
- }
162
-
163
- .severity-badge {
164
- padding: 0.5rem 1rem;
165
- border-radius: 2rem;
166
- font-size: 0.9rem;
167
- font-weight: 600;
168
- }
169
-
170
- .severity-error { background: rgba(239,68,68,0.2); color: #ef4444; }
171
- .severity-warning { background: rgba(245,158,11,0.2); color: #f59e0b; }
172
- .severity-info { background: rgba(59,130,246,0.2); color: #3b82f6; }
173
-
174
- .issues-by-type {
175
- margin-top: 1rem;
176
- }
177
-
178
- .type-bar {
179
- display: flex;
180
- align-items: center;
181
- margin: 0.75rem 0;
182
- }
183
-
184
- .type-name {
185
- flex: 1;
186
- font-size: 0.9rem;
187
- }
188
-
189
- .type-bar-bg {
190
- width: 150px;
191
- height: 20px;
192
- background: rgba(255,255,255,0.1);
193
- border-radius: 4px;
194
- overflow: hidden;
195
- margin: 0 1rem;
196
- }
197
-
198
- .type-bar-fill {
199
- height: 100%;
200
- background: linear-gradient(90deg, #60a5fa, #a78bfa);
201
- border-radius: 4px;
202
- }
203
-
204
- .type-count {
205
- width: 40px;
206
- text-align: right;
207
- font-weight: 600;
208
- }
209
-
210
- .findings-section {
211
- margin-top: 2rem;
212
- }
213
-
214
- .findings-section h2 {
215
- font-size: 1.5rem;
216
- margin-bottom: 1rem;
217
- padding-bottom: 0.5rem;
218
- border-bottom: 1px solid rgba(255,255,255,0.1);
219
- }
220
-
221
- .file-group {
222
- margin-bottom: 1.5rem;
223
- }
224
-
225
- .file-header {
226
- font-family: 'Monaco', 'Menlo', monospace;
227
- font-size: 0.9rem;
228
- color: #60a5fa;
229
- padding: 0.75rem 1rem;
230
- background: rgba(96,165,250,0.1);
231
- border-radius: 0.5rem 0.5rem 0 0;
232
- border: 1px solid rgba(96,165,250,0.2);
233
- border-bottom: none;
234
- }
235
-
236
- .issue {
237
- padding: 1rem;
238
- background: rgba(255,255,255,0.03);
239
- border: 1px solid rgba(255,255,255,0.1);
240
- border-top: none;
241
- }
242
-
243
- .issue:last-child {
244
- border-radius: 0 0 0.5rem 0.5rem;
245
- }
246
-
247
- .issue-header {
248
- display: flex;
249
- align-items: flex-start;
250
- gap: 0.75rem;
251
- }
252
-
253
- .issue-icon {
254
- font-size: 1rem;
255
- padding: 0.25rem;
256
- }
257
-
258
- .issue-message {
259
- flex: 1;
260
- font-weight: 500;
261
- }
262
-
263
- .issue-line {
264
- font-family: monospace;
265
- font-size: 0.8rem;
266
- color: #94a3b8;
267
- }
268
-
269
- .issue-suggestion {
270
- margin-top: 0.5rem;
271
- padding: 0.5rem 0.75rem;
272
- background: rgba(167,139,250,0.1);
273
- border-left: 3px solid #a78bfa;
274
- font-size: 0.9rem;
275
- color: #c4b5fd;
276
- }
277
-
278
- .code-snippet {
279
- margin-top: 0.5rem;
280
- padding: 0.75rem;
281
- background: #0d1117;
282
- border-radius: 0.5rem;
283
- font-family: 'Monaco', 'Menlo', monospace;
284
- font-size: 0.8rem;
285
- overflow-x: auto;
286
- white-space: pre;
287
- color: #c9d1d9;
288
- }
289
-
290
- .refactor-time {
291
- font-size: 1.1rem;
292
- color: #f59e0b;
293
- margin-top: 0.5rem;
294
- }
295
-
296
- footer {
297
- text-align: center;
298
- padding: 2rem;
299
- color: #64748b;
300
- font-size: 0.9rem;
301
- }
302
-
303
- @keyframes fadeIn {
304
- from { opacity: 0; transform: translateY(10px); }
305
- to { opacity: 1; transform: translateY(0); }
306
- }
307
-
308
- .card, .file-group {
309
- animation: fadeIn 0.5s ease forwards;
310
- }
311
- </style>
312
- </head>
313
- <body>
314
- <div class="container">
315
- <header>
316
- <h1>🔍 Code Smell Detector</h1>
317
- <p class="subtitle">Analysis Report - ${new Date().toLocaleDateString()}</p>
318
- </header>
319
-
320
- <div class="dashboard">
321
- <div class="card">
322
- <h2>Technical Debt Score</h2>
323
- <div class="grade-container">
324
- <div class="grade">${debtScore.grade}</div>
325
- <div class="score-details">
326
- <div class="score-label"><span>Overall Score</span><span>${debtScore.score}/100</span></div>
327
- <div class="score-bar">
328
- <div class="score-bar-fill" style="width: ${debtScore.score}%; background: ${gradeColors[debtScore.grade]}"></div>
329
- </div>
330
- ${generateBreakdownBars(debtScore.breakdown)}
331
- <p class="refactor-time">⏱️ Est. refactor time: ${debtScore.estimatedRefactorTime}</p>
332
- </div>
333
- </div>
334
- </div>
335
-
336
- <div class="card">
337
- <h2>Summary</h2>
338
- <div class="stat-grid">
339
- <div>
340
- <div class="stat-value">${summary.totalFiles}</div>
341
- <div class="stat-label">Files</div>
342
- </div>
343
- <div>
344
- <div class="stat-value">${summary.totalComponents}</div>
345
- <div class="stat-label">Components</div>
346
- </div>
347
- <div>
348
- <div class="stat-value">${summary.totalSmells}</div>
349
- <div class="stat-label">Issues</div>
350
- </div>
351
- </div>
352
- <div class="severity-badges">
353
- <span class="severity-badge severity-error">${summary.smellsBySeverity.error} Errors</span>
354
- <span class="severity-badge severity-warning">${summary.smellsBySeverity.warning} Warnings</span>
355
- <span class="severity-badge severity-info">${summary.smellsBySeverity.info} Info</span>
356
- </div>
357
- </div>
358
-
359
- <div class="card">
360
- <h2>Issues by Type</h2>
361
- <div class="issues-by-type">
362
- ${smellTypeData.map(([type, count]) => {
363
- const maxCount = Math.max(...smellTypeData.map(([_, c]) => c));
364
- const percentage = (count / maxCount) * 100;
365
- return `
366
- <div class="type-bar">
367
- <span class="type-name">${formatTypeLabel(type)}</span>
368
- <div class="type-bar-bg">
369
- <div class="type-bar-fill" style="width: ${percentage}%"></div>
370
- </div>
371
- <span class="type-count">${count}</span>
372
- </div>
373
- `;
374
- }).join('')}
375
- </div>
376
- </div>
377
- </div>
378
-
379
- <section class="findings-section">
380
- <h2>📋 Detailed Findings</h2>
381
- ${files.filter(f => f.smells.length > 0).map(file => `
382
- <div class="file-group">
383
- <div class="file-header">${path.relative(rootDir, file.file)}</div>
384
- ${file.smells.map(smell => `
385
- <div class="issue">
386
- <div class="issue-header">
387
- <span class="issue-icon">${getSeverityIcon(smell.severity)}</span>
388
- <span class="issue-message" style="color: ${severityColors[smell.severity]}">${escapeHtml(smell.message)}</span>
389
- <span class="issue-line">Line ${smell.line}</span>
390
- </div>
391
- <div class="issue-suggestion">💡 ${escapeHtml(smell.suggestion)}</div>
392
- ${smell.codeSnippet ? `<div class="code-snippet">${escapeHtml(smell.codeSnippet)}</div>` : ''}
393
- </div>
394
- `).join('')}
395
- </div>
396
- `).join('')}
397
- </section>
398
-
399
- <footer>
400
- Generated by React Code Smell Detector • ${new Date().toISOString()}
401
- </footer>
402
- </div>
403
- </body>
404
- </html>`;
405
- }
406
-
407
- function generateBreakdownBars(breakdown: { useEffectScore: number; propDrillingScore: number; componentSizeScore: number; memoizationScore: number }): string {
408
- const items = [
409
- { label: 'useEffect', score: breakdown.useEffectScore },
410
- { label: 'Prop Drilling', score: breakdown.propDrillingScore },
411
- { label: 'Component Size', score: breakdown.componentSizeScore },
412
- { label: 'Memoization', score: breakdown.memoizationScore },
413
- ];
414
-
415
- return items.map(({ label, score }) => {
416
- const color = score >= 80 ? '#22c55e' : score >= 60 ? '#eab308' : '#ef4444';
417
- return `
418
- <div class="score-label" style="margin-top: 0.5rem"><span>${label}</span><span>${score}</span></div>
419
- <div class="score-bar">
420
- <div class="score-bar-fill" style="width: ${score}%; background: ${color}"></div>
421
- </div>
422
- `;
423
- }).join('');
424
- }
425
-
426
- function formatTypeLabel(type: string): string {
427
- const labels: Record<string, string> = {
428
- 'useEffect-overuse': '⚡ useEffect',
429
- 'prop-drilling': '🔗 Prop Drilling',
430
- 'large-component': '📐 Large Component',
431
- 'unmemoized-calculation': '💾 Unmemoized',
432
- 'inline-function-prop': '📎 Inline Func',
433
- 'deep-nesting': '📊 Deep Nesting',
434
- 'missing-key': '🔑 Missing Key',
435
- 'magic-value': '🔢 Magic Value',
436
- 'debug-statement': '🐛 Debug',
437
- 'todo-comment': '📝 TODO',
438
- 'security-xss': '🔒 Security XSS',
439
- 'security-eval': '🔒 Security Eval',
440
- 'security-secrets': '🔑 Secrets',
441
- 'a11y-missing-alt': '♿ Missing Alt',
442
- 'a11y-missing-label': '♿ Missing Label',
443
- 'ts-any-usage': '🔷 TS any',
444
- 'ts-missing-return-type': '🔷 TS Return Type',
445
- };
446
- return labels[type] || type.replace(/-/g, ' ');
447
- }
448
-
449
- function getSeverityIcon(severity: SmellSeverity): string {
450
- switch (severity) {
451
- case 'error': return '❌';
452
- case 'warning': return '⚠️';
453
- case 'info': return 'ℹ️';
454
- }
455
- }
456
-
457
- function escapeHtml(text: string): string {
458
- return text
459
- .replace(/&/g, '&amp;')
460
- .replace(/</g, '&lt;')
461
- .replace(/>/g, '&gt;')
462
- .replace(/"/g, '&quot;')
463
- .replace(/'/g, '&#039;');
464
- }
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export { analyzeProject, type AnalyzerOptions } from './analyzer.js';
2
- export { reportResults, type ReporterOptions } from './reporter.js';
3
- export * from './types/index.js';
4
- export { parseFile, parseCode, type ParseResult, type ParsedComponent } from './parser/index.js';