react-code-smell-detector 1.0.1 â 1.2.0
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 +69 -11
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +78 -2
- package/dist/cli.js +43 -9
- package/dist/detectors/accessibility.d.ts +12 -0
- package/dist/detectors/accessibility.d.ts.map +1 -0
- package/dist/detectors/accessibility.js +191 -0
- package/dist/detectors/debug.d.ts +10 -0
- package/dist/detectors/debug.d.ts.map +1 -0
- package/dist/detectors/debug.js +87 -0
- package/dist/detectors/index.d.ts +8 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +10 -0
- package/dist/detectors/javascript.d.ts +11 -0
- package/dist/detectors/javascript.d.ts.map +1 -0
- package/dist/detectors/javascript.js +148 -0
- package/dist/detectors/nextjs.d.ts +11 -0
- package/dist/detectors/nextjs.d.ts.map +1 -0
- package/dist/detectors/nextjs.js +103 -0
- package/dist/detectors/nodejs.d.ts +11 -0
- package/dist/detectors/nodejs.d.ts.map +1 -0
- package/dist/detectors/nodejs.js +169 -0
- package/dist/detectors/reactNative.d.ts +10 -0
- package/dist/detectors/reactNative.d.ts.map +1 -0
- package/dist/detectors/reactNative.js +135 -0
- package/dist/detectors/security.d.ts +12 -0
- package/dist/detectors/security.d.ts.map +1 -0
- package/dist/detectors/security.js +161 -0
- package/dist/detectors/typescript.d.ts +11 -0
- package/dist/detectors/typescript.d.ts.map +1 -0
- package/dist/detectors/typescript.js +135 -0
- package/dist/htmlReporter.d.ts +6 -0
- package/dist/htmlReporter.d.ts.map +1 -0
- package/dist/htmlReporter.js +453 -0
- package/dist/reporter.js +37 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +11 -0
- package/package.json +2 -2
- package/src/analyzer.ts +91 -1
- package/src/cli.ts +43 -9
- package/src/detectors/accessibility.ts +212 -0
- package/src/detectors/debug.ts +103 -0
- package/src/detectors/index.ts +10 -0
- package/src/detectors/javascript.ts +169 -0
- package/src/detectors/nextjs.ts +124 -0
- package/src/detectors/nodejs.ts +199 -0
- package/src/detectors/reactNative.ts +154 -0
- package/src/detectors/security.ts +179 -0
- package/src/detectors/typescript.ts +151 -0
- package/src/htmlReporter.ts +464 -0
- package/src/reporter.ts +37 -0
- package/src/types/index.ts +61 -2
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Detects TypeScript-specific code smells:
|
|
7
|
+
* - Overuse of 'any' type
|
|
8
|
+
* - Missing return type on functions
|
|
9
|
+
* - Non-null assertion operator (!)
|
|
10
|
+
* - Type assertions (as) that could be avoided
|
|
11
|
+
*/
|
|
12
|
+
export function detectTypescriptIssues(
|
|
13
|
+
component: ParsedComponent,
|
|
14
|
+
filePath: string,
|
|
15
|
+
sourceCode: string,
|
|
16
|
+
config: DetectorConfig = DEFAULT_CONFIG
|
|
17
|
+
): CodeSmell[] {
|
|
18
|
+
if (!config.checkTypescript) return [];
|
|
19
|
+
|
|
20
|
+
// Only run on TypeScript files
|
|
21
|
+
if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx')) return [];
|
|
22
|
+
|
|
23
|
+
const smells: CodeSmell[] = [];
|
|
24
|
+
|
|
25
|
+
// Detect 'any' type usage
|
|
26
|
+
component.path.traverse({
|
|
27
|
+
TSAnyKeyword(path) {
|
|
28
|
+
const loc = path.node.loc;
|
|
29
|
+
smells.push({
|
|
30
|
+
type: 'ts-any-usage',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
message: `Using "any" type in "${component.name}"`,
|
|
33
|
+
file: filePath,
|
|
34
|
+
line: loc?.start.line || 0,
|
|
35
|
+
column: loc?.start.column || 0,
|
|
36
|
+
suggestion: 'Use a specific type, "unknown", or create an interface',
|
|
37
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Detect functions without return type (only in complex functions)
|
|
43
|
+
component.path.traverse({
|
|
44
|
+
FunctionDeclaration(path) {
|
|
45
|
+
// Skip if function has explicit return type
|
|
46
|
+
if (path.node.returnType) return;
|
|
47
|
+
|
|
48
|
+
// Check if function body has return statements
|
|
49
|
+
let hasReturn = false;
|
|
50
|
+
path.traverse({
|
|
51
|
+
ReturnStatement(returnPath) {
|
|
52
|
+
if (returnPath.node.argument) {
|
|
53
|
+
hasReturn = true;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (hasReturn) {
|
|
59
|
+
const loc = path.node.loc;
|
|
60
|
+
smells.push({
|
|
61
|
+
type: 'ts-missing-return-type',
|
|
62
|
+
severity: 'info',
|
|
63
|
+
message: `Function "${path.node.id?.name || 'anonymous'}" missing return type`,
|
|
64
|
+
file: filePath,
|
|
65
|
+
line: loc?.start.line || 0,
|
|
66
|
+
column: loc?.start.column || 0,
|
|
67
|
+
suggestion: 'Add explicit return type: function name(): ReturnType { ... }',
|
|
68
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
ArrowFunctionExpression(path) {
|
|
74
|
+
// Only check arrow functions assigned to variables with 5+ lines
|
|
75
|
+
if (!path.node.returnType) {
|
|
76
|
+
const body = path.node.body;
|
|
77
|
+
|
|
78
|
+
// Skip simple arrow functions (single expression)
|
|
79
|
+
if (!t.isBlockStatement(body)) return;
|
|
80
|
+
|
|
81
|
+
// Check complexity - only flag if function is substantial
|
|
82
|
+
const startLine = path.node.loc?.start.line || 0;
|
|
83
|
+
const endLine = path.node.loc?.end.line || 0;
|
|
84
|
+
|
|
85
|
+
if (endLine - startLine >= 5) {
|
|
86
|
+
// Check if parent is variable declarator (assigned to variable)
|
|
87
|
+
const parent = path.parent;
|
|
88
|
+
|
|
89
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
90
|
+
const loc = path.node.loc;
|
|
91
|
+
smells.push({
|
|
92
|
+
type: 'ts-missing-return-type',
|
|
93
|
+
severity: 'info',
|
|
94
|
+
message: `Arrow function "${parent.id.name}" missing return type`,
|
|
95
|
+
file: filePath,
|
|
96
|
+
line: loc?.start.line || 0,
|
|
97
|
+
column: loc?.start.column || 0,
|
|
98
|
+
suggestion: 'Add return type: const name = (): ReturnType => { ... }',
|
|
99
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Detect non-null assertion operator (!)
|
|
108
|
+
component.path.traverse({
|
|
109
|
+
TSNonNullExpression(path) {
|
|
110
|
+
const loc = path.node.loc;
|
|
111
|
+
smells.push({
|
|
112
|
+
type: 'ts-non-null-assertion',
|
|
113
|
+
severity: 'warning',
|
|
114
|
+
message: `Non-null assertion (!) bypasses type safety in "${component.name}"`,
|
|
115
|
+
file: filePath,
|
|
116
|
+
line: loc?.start.line || 0,
|
|
117
|
+
column: loc?.start.column || 0,
|
|
118
|
+
suggestion: 'Use optional chaining (?.) or proper null checks instead',
|
|
119
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Detect type assertions (as keyword) - these can hide type errors
|
|
125
|
+
component.path.traverse({
|
|
126
|
+
TSAsExpression(path) {
|
|
127
|
+
// Skip assertions to 'const' (used for const assertions)
|
|
128
|
+
if (t.isTSTypeReference(path.node.typeAnnotation)) {
|
|
129
|
+
const typeName = path.node.typeAnnotation.typeName;
|
|
130
|
+
if (t.isIdentifier(typeName) && typeName.name === 'const') return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Skip double assertions (as unknown as Type) - already flagged by TypeScript
|
|
134
|
+
if (t.isTSAsExpression(path.node.expression)) return;
|
|
135
|
+
|
|
136
|
+
const loc = path.node.loc;
|
|
137
|
+
smells.push({
|
|
138
|
+
type: 'ts-type-assertion',
|
|
139
|
+
severity: 'info',
|
|
140
|
+
message: `Type assertion (as) bypasses type checking in "${component.name}"`,
|
|
141
|
+
file: filePath,
|
|
142
|
+
line: loc?.start.line || 0,
|
|
143
|
+
column: loc?.start.column || 0,
|
|
144
|
+
suggestion: 'Consider using type guards or proper typing instead of assertions',
|
|
145
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return smells;
|
|
151
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
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/reporter.ts
CHANGED
|
@@ -249,6 +249,43 @@ function formatSmellType(type: string): string {
|
|
|
249
249
|
'nested-ternary': 'â Nested Ternary',
|
|
250
250
|
'dead-code': 'đ Dead Code',
|
|
251
251
|
'magic-value': 'đĸ Magic Value',
|
|
252
|
+
// Next.js
|
|
253
|
+
'nextjs-client-server-boundary': 'ⲠNext.js Client/Server Boundary',
|
|
254
|
+
'nextjs-missing-metadata': 'ⲠNext.js Missing Metadata',
|
|
255
|
+
'nextjs-image-unoptimized': 'ⲠNext.js Unoptimized Image',
|
|
256
|
+
'nextjs-router-misuse': 'ⲠNext.js Router Misuse',
|
|
257
|
+
// React Native
|
|
258
|
+
'rn-inline-style': 'đą RN Inline Style',
|
|
259
|
+
'rn-missing-accessibility': 'đą RN Missing Accessibility',
|
|
260
|
+
'rn-performance-issue': 'đą RN Performance Issue',
|
|
261
|
+
// Node.js
|
|
262
|
+
'nodejs-callback-hell': 'đĸ Node.js Callback Hell',
|
|
263
|
+
'nodejs-unhandled-promise': 'đĸ Node.js Unhandled Promise',
|
|
264
|
+
'nodejs-sync-io': 'đĸ Node.js Sync I/O',
|
|
265
|
+
'nodejs-missing-error-handling': 'đĸ Node.js Missing Error Handling',
|
|
266
|
+
// JavaScript
|
|
267
|
+
'js-var-usage': 'đ JS var Usage',
|
|
268
|
+
'js-loose-equality': 'đ JS Loose Equality',
|
|
269
|
+
'js-implicit-coercion': 'đ JS Implicit Coercion',
|
|
270
|
+
'js-global-pollution': 'đ JS Global Pollution',
|
|
271
|
+
// TypeScript
|
|
272
|
+
'ts-any-usage': 'đˇ TS any Usage',
|
|
273
|
+
'ts-missing-return-type': 'đˇ TS Missing Return Type',
|
|
274
|
+
'ts-non-null-assertion': 'đˇ TS Non-null Assertion',
|
|
275
|
+
'ts-type-assertion': 'đˇ TS Type Assertion',
|
|
276
|
+
// Debug
|
|
277
|
+
'debug-statement': 'đ Debug Statement',
|
|
278
|
+
'todo-comment': 'đ TODO/FIXME Comment',
|
|
279
|
+
// Security
|
|
280
|
+
'security-xss': 'đ XSS Vulnerability',
|
|
281
|
+
'security-eval': 'đ Eval Usage',
|
|
282
|
+
'security-secrets': 'đ Exposed Secret',
|
|
283
|
+
// Accessibility
|
|
284
|
+
'a11y-missing-alt': 'âŋ Missing Alt Text',
|
|
285
|
+
'a11y-missing-label': 'âŋ Missing Label',
|
|
286
|
+
'a11y-interactive-role': 'âŋ Interactive Role',
|
|
287
|
+
'a11y-keyboard': 'âŋ Keyboard Handler',
|
|
288
|
+
'a11y-semantic': 'âŋ Semantic HTML',
|
|
252
289
|
};
|
|
253
290
|
return labels[type] || type;
|
|
254
291
|
}
|