react-code-smell-detector 1.2.0 → 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 +46 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +18 -1
- package/dist/cli.js +93 -26
- 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/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 +3 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +4 -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/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/reporter.js +13 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +9 -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 -324
- package/src/cli.ts +0 -159
- package/src/detectors/accessibility.ts +0 -212
- package/src/detectors/deadCode.ts +0 -163
- package/src/detectors/debug.ts +0 -103
- package/src/detectors/dependencyArray.ts +0 -176
- package/src/detectors/hooksRules.ts +0 -101
- package/src/detectors/index.ts +0 -20
- 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/security.ts +0 -179
- package/src/detectors/typescript.ts +0 -151
- package/src/detectors/useEffect.ts +0 -117
- package/src/htmlReporter.ts +0 -464
- package/src/index.ts +0 -4
- package/src/parser/index.ts +0 -195
- package/src/reporter.ts +0 -291
- package/src/types/index.ts +0 -165
- package/tsconfig.json +0 -19
package/src/htmlReporter.ts
DELETED
|
@@ -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, '&')
|
|
460
|
-
.replace(/</g, '<')
|
|
461
|
-
.replace(/>/g, '>')
|
|
462
|
-
.replace(/"/g, '"')
|
|
463
|
-
.replace(/'/g, ''');
|
|
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';
|
package/src/parser/index.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import * as parser from '@babel/parser';
|
|
2
|
-
import _traverse, { NodePath } from '@babel/traverse';
|
|
3
|
-
import * as t from '@babel/types';
|
|
4
|
-
import fs from 'fs/promises';
|
|
5
|
-
|
|
6
|
-
// Handle ESM/CJS interop
|
|
7
|
-
const traverse = (_traverse as unknown as { default: typeof _traverse }).default || _traverse;
|
|
8
|
-
|
|
9
|
-
export interface ParsedComponent {
|
|
10
|
-
name: string;
|
|
11
|
-
startLine: number;
|
|
12
|
-
endLine: number;
|
|
13
|
-
node: t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression;
|
|
14
|
-
path: NodePath;
|
|
15
|
-
hooks: {
|
|
16
|
-
useEffect: t.CallExpression[];
|
|
17
|
-
useState: t.CallExpression[];
|
|
18
|
-
useMemo: t.CallExpression[];
|
|
19
|
-
useCallback: t.CallExpression[];
|
|
20
|
-
useRef: t.CallExpression[];
|
|
21
|
-
custom: t.CallExpression[];
|
|
22
|
-
};
|
|
23
|
-
props: string[];
|
|
24
|
-
jsxDepth: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ParseResult {
|
|
28
|
-
ast: t.File;
|
|
29
|
-
components: ParsedComponent[];
|
|
30
|
-
imports: string[];
|
|
31
|
-
sourceCode: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function parseFile(filePath: string): Promise<ParseResult> {
|
|
35
|
-
const sourceCode = await fs.readFile(filePath, 'utf-8');
|
|
36
|
-
return parseCode(sourceCode, filePath);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function parseCode(sourceCode: string, filePath: string = 'unknown'): ParseResult {
|
|
40
|
-
const ast = parser.parse(sourceCode, {
|
|
41
|
-
sourceType: 'module',
|
|
42
|
-
plugins: [
|
|
43
|
-
'jsx',
|
|
44
|
-
'typescript',
|
|
45
|
-
'decorators-legacy',
|
|
46
|
-
'classProperties',
|
|
47
|
-
'optionalChaining',
|
|
48
|
-
'nullishCoalescingOperator',
|
|
49
|
-
],
|
|
50
|
-
sourceFilename: filePath,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const components: ParsedComponent[] = [];
|
|
54
|
-
const imports: string[] = [];
|
|
55
|
-
|
|
56
|
-
traverse(ast, {
|
|
57
|
-
ImportDeclaration(path) {
|
|
58
|
-
imports.push(path.node.source.value);
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
FunctionDeclaration(path) {
|
|
62
|
-
if (isReactComponent(path.node.id?.name, path)) {
|
|
63
|
-
components.push(extractComponentInfo(path.node.id?.name || 'Anonymous', path));
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
VariableDeclarator(path) {
|
|
68
|
-
const init = path.node.init;
|
|
69
|
-
const id = path.node.id;
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
t.isIdentifier(id) &&
|
|
73
|
-
(t.isArrowFunctionExpression(init) || t.isFunctionExpression(init))
|
|
74
|
-
) {
|
|
75
|
-
if (isReactComponent(id.name, path)) {
|
|
76
|
-
components.push(extractComponentInfo(id.name, path, init));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
return { ast, components, imports, sourceCode };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isReactComponent(name: string | undefined, path: NodePath): boolean {
|
|
86
|
-
if (!name) return false;
|
|
87
|
-
|
|
88
|
-
// Component names start with uppercase
|
|
89
|
-
if (!/^[A-Z]/.test(name)) return false;
|
|
90
|
-
|
|
91
|
-
// Check if it returns JSX
|
|
92
|
-
let hasJSX = false;
|
|
93
|
-
path.traverse({
|
|
94
|
-
JSXElement() {
|
|
95
|
-
hasJSX = true;
|
|
96
|
-
},
|
|
97
|
-
JSXFragment() {
|
|
98
|
-
hasJSX = true;
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
return hasJSX;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function extractComponentInfo(
|
|
106
|
-
name: string,
|
|
107
|
-
path: NodePath,
|
|
108
|
-
node?: t.ArrowFunctionExpression | t.FunctionExpression
|
|
109
|
-
): ParsedComponent {
|
|
110
|
-
const actualNode = node || (path.node as t.FunctionDeclaration);
|
|
111
|
-
const loc = actualNode.loc;
|
|
112
|
-
|
|
113
|
-
const hooks = {
|
|
114
|
-
useEffect: [] as t.CallExpression[],
|
|
115
|
-
useState: [] as t.CallExpression[],
|
|
116
|
-
useMemo: [] as t.CallExpression[],
|
|
117
|
-
useCallback: [] as t.CallExpression[],
|
|
118
|
-
useRef: [] as t.CallExpression[],
|
|
119
|
-
custom: [] as t.CallExpression[],
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const props: string[] = [];
|
|
123
|
-
let jsxDepth = 0;
|
|
124
|
-
|
|
125
|
-
// Extract hooks
|
|
126
|
-
path.traverse({
|
|
127
|
-
CallExpression(callPath) {
|
|
128
|
-
const callee = callPath.node.callee;
|
|
129
|
-
if (t.isIdentifier(callee)) {
|
|
130
|
-
const hookName = callee.name;
|
|
131
|
-
if (hookName === 'useEffect') hooks.useEffect.push(callPath.node);
|
|
132
|
-
else if (hookName === 'useState') hooks.useState.push(callPath.node);
|
|
133
|
-
else if (hookName === 'useMemo') hooks.useMemo.push(callPath.node);
|
|
134
|
-
else if (hookName === 'useCallback') hooks.useCallback.push(callPath.node);
|
|
135
|
-
else if (hookName === 'useRef') hooks.useRef.push(callPath.node);
|
|
136
|
-
else if (hookName.startsWith('use')) hooks.custom.push(callPath.node);
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Extract props
|
|
142
|
-
const params = t.isFunctionDeclaration(actualNode)
|
|
143
|
-
? actualNode.params
|
|
144
|
-
: actualNode.params;
|
|
145
|
-
|
|
146
|
-
if (params.length > 0) {
|
|
147
|
-
const firstParam = params[0];
|
|
148
|
-
if (t.isObjectPattern(firstParam)) {
|
|
149
|
-
firstParam.properties.forEach(prop => {
|
|
150
|
-
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
151
|
-
props.push(prop.key.name);
|
|
152
|
-
} else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
153
|
-
props.push(`...${prop.argument.name}`);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
} else if (t.isIdentifier(firstParam)) {
|
|
157
|
-
props.push(firstParam.name);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Calculate JSX nesting depth
|
|
162
|
-
path.traverse({
|
|
163
|
-
JSXElement: {
|
|
164
|
-
enter() {
|
|
165
|
-
jsxDepth++;
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
name,
|
|
172
|
-
startLine: loc?.start.line || 0,
|
|
173
|
-
endLine: loc?.end.line || 0,
|
|
174
|
-
node: actualNode as any,
|
|
175
|
-
path,
|
|
176
|
-
hooks,
|
|
177
|
-
props,
|
|
178
|
-
jsxDepth: Math.floor(jsxDepth / 2), // Approximate depth
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function getCodeSnippet(sourceCode: string, line: number, context: number = 2): string {
|
|
183
|
-
const lines = sourceCode.split('\n');
|
|
184
|
-
const start = Math.max(0, line - context - 1);
|
|
185
|
-
const end = Math.min(lines.length, line + context);
|
|
186
|
-
|
|
187
|
-
return lines
|
|
188
|
-
.slice(start, end)
|
|
189
|
-
.map((l, i) => {
|
|
190
|
-
const lineNum = start + i + 1;
|
|
191
|
-
const marker = lineNum === line ? '>' : ' ';
|
|
192
|
-
return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;
|
|
193
|
-
})
|
|
194
|
-
.join('\n');
|
|
195
|
-
}
|