react-code-smell-detector 1.1.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 +48 -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 +3 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +4 -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/htmlReporter.d.ts +6 -0
- package/dist/htmlReporter.d.ts.map +1 -0
- package/dist/htmlReporter.js +453 -0
- package/dist/reporter.js +13 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +4 -0
- package/package.json +2 -2
- package/src/analyzer.ts +56 -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 +4 -0
- package/src/detectors/security.ts +179 -0
- package/src/htmlReporter.ts +464 -0
- package/src/reporter.ts +13 -0
- package/src/types/index.ts +21 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { getCodeSnippet } from '../parser/index.js';
|
|
3
|
+
import { DEFAULT_CONFIG } from '../types/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Detects security vulnerabilities:
|
|
6
|
+
* - dangerouslySetInnerHTML usage
|
|
7
|
+
* - eval() and Function() constructor
|
|
8
|
+
* - innerHTML assignments
|
|
9
|
+
* - Unsafe URLs (javascript:, data:)
|
|
10
|
+
* - Exposed secrets/API keys
|
|
11
|
+
*/
|
|
12
|
+
export function detectSecurityIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
|
|
13
|
+
if (!config.checkSecurity)
|
|
14
|
+
return [];
|
|
15
|
+
const smells = [];
|
|
16
|
+
// Detect dangerouslySetInnerHTML
|
|
17
|
+
component.path.traverse({
|
|
18
|
+
JSXAttribute(path) {
|
|
19
|
+
if (t.isJSXIdentifier(path.node.name) &&
|
|
20
|
+
path.node.name.name === 'dangerouslySetInnerHTML') {
|
|
21
|
+
const loc = path.node.loc;
|
|
22
|
+
smells.push({
|
|
23
|
+
type: 'security-xss',
|
|
24
|
+
severity: 'error',
|
|
25
|
+
message: `dangerouslySetInnerHTML is a security risk in "${component.name}"`,
|
|
26
|
+
file: filePath,
|
|
27
|
+
line: loc?.start.line || 0,
|
|
28
|
+
column: loc?.start.column || 0,
|
|
29
|
+
suggestion: 'Sanitize HTML with DOMPurify or use a safe alternative like converting to React elements',
|
|
30
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
// Detect eval() and Function() constructor
|
|
36
|
+
component.path.traverse({
|
|
37
|
+
CallExpression(path) {
|
|
38
|
+
const { callee } = path.node;
|
|
39
|
+
// eval()
|
|
40
|
+
if (t.isIdentifier(callee) && callee.name === 'eval') {
|
|
41
|
+
const loc = path.node.loc;
|
|
42
|
+
smells.push({
|
|
43
|
+
type: 'security-eval',
|
|
44
|
+
severity: 'error',
|
|
45
|
+
message: `eval() is a critical security risk in "${component.name}"`,
|
|
46
|
+
file: filePath,
|
|
47
|
+
line: loc?.start.line || 0,
|
|
48
|
+
column: loc?.start.column || 0,
|
|
49
|
+
suggestion: 'Never use eval(). Parse JSON with JSON.parse() or restructure logic to avoid dynamic code execution.',
|
|
50
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
// new Function()
|
|
55
|
+
NewExpression(path) {
|
|
56
|
+
const { callee } = path.node;
|
|
57
|
+
if (t.isIdentifier(callee) && callee.name === 'Function') {
|
|
58
|
+
const loc = path.node.loc;
|
|
59
|
+
smells.push({
|
|
60
|
+
type: 'security-eval',
|
|
61
|
+
severity: 'error',
|
|
62
|
+
message: `new Function() is equivalent to eval() and is a security risk`,
|
|
63
|
+
file: filePath,
|
|
64
|
+
line: loc?.start.line || 0,
|
|
65
|
+
column: loc?.start.column || 0,
|
|
66
|
+
suggestion: 'Avoid creating functions from strings. Restructure to use static function definitions.',
|
|
67
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
// Detect innerHTML assignments
|
|
73
|
+
component.path.traverse({
|
|
74
|
+
AssignmentExpression(path) {
|
|
75
|
+
const { left } = path.node;
|
|
76
|
+
if (t.isMemberExpression(left) && t.isIdentifier(left.property)) {
|
|
77
|
+
if (left.property.name === 'innerHTML' || left.property.name === 'outerHTML') {
|
|
78
|
+
const loc = path.node.loc;
|
|
79
|
+
smells.push({
|
|
80
|
+
type: 'security-xss',
|
|
81
|
+
severity: 'warning',
|
|
82
|
+
message: `Direct ${left.property.name} assignment can lead to XSS`,
|
|
83
|
+
file: filePath,
|
|
84
|
+
line: loc?.start.line || 0,
|
|
85
|
+
column: loc?.start.column || 0,
|
|
86
|
+
suggestion: 'Use textContent for plain text, or sanitize HTML with DOMPurify',
|
|
87
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
// Detect unsafe URLs (javascript:, data:)
|
|
94
|
+
component.path.traverse({
|
|
95
|
+
JSXAttribute(path) {
|
|
96
|
+
if (!t.isJSXIdentifier(path.node.name))
|
|
97
|
+
return;
|
|
98
|
+
const propName = path.node.name.name;
|
|
99
|
+
if (!['href', 'src', 'action'].includes(propName))
|
|
100
|
+
return;
|
|
101
|
+
const value = path.node.value;
|
|
102
|
+
if (t.isStringLiteral(value)) {
|
|
103
|
+
const url = value.value.toLowerCase().trim();
|
|
104
|
+
if (url.startsWith('javascript:')) {
|
|
105
|
+
const loc = path.node.loc;
|
|
106
|
+
smells.push({
|
|
107
|
+
type: 'security-xss',
|
|
108
|
+
severity: 'error',
|
|
109
|
+
message: `javascript: URLs are a security risk`,
|
|
110
|
+
file: filePath,
|
|
111
|
+
line: loc?.start.line || 0,
|
|
112
|
+
column: loc?.start.column || 0,
|
|
113
|
+
suggestion: 'Use onClick handlers instead of javascript: URLs',
|
|
114
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (url.startsWith('data:') && propName === 'href') {
|
|
118
|
+
const loc = path.node.loc;
|
|
119
|
+
smells.push({
|
|
120
|
+
type: 'security-xss',
|
|
121
|
+
severity: 'warning',
|
|
122
|
+
message: `data: URLs in href can be a security risk`,
|
|
123
|
+
file: filePath,
|
|
124
|
+
line: loc?.start.line || 0,
|
|
125
|
+
column: loc?.start.column || 0,
|
|
126
|
+
suggestion: 'Validate and sanitize data URLs, or use blob URLs instead',
|
|
127
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
// Detect potential exposed secrets
|
|
134
|
+
const secretPatterns = [
|
|
135
|
+
{ pattern: /['"](?:sk[-_]live|pk[-_]live|api[-_]?key|secret[-_]?key|access[-_]?token|auth[-_]?token)['"]\s*[:=]\s*['"][a-zA-Z0-9-_]{20,}/i, name: 'API key' },
|
|
136
|
+
{ pattern: /['"](?:ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{36,}['"]/i, name: 'GitHub token' },
|
|
137
|
+
{ pattern: /['"]AKIA[A-Z0-9]{16}['"]/i, name: 'AWS access key' },
|
|
138
|
+
{ pattern: /password\s*[:=]\s*['"][^'"]{8,}['"]/i, name: 'Hardcoded password' },
|
|
139
|
+
];
|
|
140
|
+
const lines = sourceCode.split('\n');
|
|
141
|
+
lines.forEach((line, index) => {
|
|
142
|
+
const lineNum = index + 1;
|
|
143
|
+
if (lineNum < component.startLine || lineNum > component.endLine)
|
|
144
|
+
return;
|
|
145
|
+
secretPatterns.forEach(({ pattern, name }) => {
|
|
146
|
+
if (pattern.test(line)) {
|
|
147
|
+
smells.push({
|
|
148
|
+
type: 'security-secrets',
|
|
149
|
+
severity: 'error',
|
|
150
|
+
message: `Potential ${name} exposed in code`,
|
|
151
|
+
file: filePath,
|
|
152
|
+
line: lineNum,
|
|
153
|
+
column: 0,
|
|
154
|
+
suggestion: 'Move secrets to environment variables (.env) and never commit them to version control',
|
|
155
|
+
codeSnippet: getCodeSnippet(sourceCode, lineNum),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
return smells;
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"htmlReporter.d.ts","sourceRoot":"","sources":["../src/htmlReporter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAiB,MAAM,kBAAkB,CAAC;AAGjE;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA8YlF"}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a beautiful HTML report with charts and styling
|
|
4
|
+
*/
|
|
5
|
+
export function generateHTMLReport(result, rootDir) {
|
|
6
|
+
const { summary, debtScore, files } = result;
|
|
7
|
+
// Generate chart data
|
|
8
|
+
const smellTypeData = Object.entries(summary.smellsByType)
|
|
9
|
+
.filter(([_, count]) => count > 0)
|
|
10
|
+
.sort((a, b) => b[1] - a[1])
|
|
11
|
+
.slice(0, 10);
|
|
12
|
+
const severityColors = {
|
|
13
|
+
error: '#ef4444',
|
|
14
|
+
warning: '#f59e0b',
|
|
15
|
+
info: '#3b82f6',
|
|
16
|
+
};
|
|
17
|
+
const gradeColors = {
|
|
18
|
+
A: '#22c55e',
|
|
19
|
+
B: '#84cc16',
|
|
20
|
+
C: '#eab308',
|
|
21
|
+
D: '#f97316',
|
|
22
|
+
F: '#ef4444',
|
|
23
|
+
};
|
|
24
|
+
return `<!DOCTYPE html>
|
|
25
|
+
<html lang="en">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="UTF-8">
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
29
|
+
<title>Code Smell Detector Report</title>
|
|
30
|
+
<style>
|
|
31
|
+
* {
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
39
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
color: #e2e8f0;
|
|
42
|
+
line-height: 1.6;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.container {
|
|
46
|
+
max-width: 1200px;
|
|
47
|
+
margin: 0 auto;
|
|
48
|
+
padding: 2rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
header {
|
|
52
|
+
text-align: center;
|
|
53
|
+
padding: 3rem 0;
|
|
54
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
55
|
+
margin-bottom: 2rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
h1 {
|
|
59
|
+
font-size: 2.5rem;
|
|
60
|
+
margin-bottom: 0.5rem;
|
|
61
|
+
background: linear-gradient(90deg, #60a5fa, #a78bfa);
|
|
62
|
+
-webkit-background-clip: text;
|
|
63
|
+
-webkit-text-fill-color: transparent;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.subtitle {
|
|
67
|
+
color: #94a3b8;
|
|
68
|
+
font-size: 1.1rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.dashboard {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
74
|
+
gap: 1.5rem;
|
|
75
|
+
margin-bottom: 2rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.card {
|
|
79
|
+
background: rgba(255,255,255,0.05);
|
|
80
|
+
border-radius: 1rem;
|
|
81
|
+
padding: 1.5rem;
|
|
82
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
83
|
+
backdrop-filter: blur(10px);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.card h2 {
|
|
87
|
+
font-size: 1rem;
|
|
88
|
+
text-transform: uppercase;
|
|
89
|
+
letter-spacing: 0.1em;
|
|
90
|
+
color: #94a3b8;
|
|
91
|
+
margin-bottom: 1rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.grade-container {
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 2rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.grade {
|
|
101
|
+
font-size: 5rem;
|
|
102
|
+
font-weight: 800;
|
|
103
|
+
color: ${gradeColors[debtScore.grade]};
|
|
104
|
+
text-shadow: 0 0 30px ${gradeColors[debtScore.grade]}40;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.score-details {
|
|
108
|
+
flex: 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.score-bar {
|
|
112
|
+
height: 8px;
|
|
113
|
+
background: rgba(255,255,255,0.1);
|
|
114
|
+
border-radius: 4px;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
margin: 0.5rem 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.score-bar-fill {
|
|
120
|
+
height: 100%;
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
transition: width 1s ease;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.score-label {
|
|
126
|
+
display: flex;
|
|
127
|
+
justify-content: space-between;
|
|
128
|
+
font-size: 0.9rem;
|
|
129
|
+
color: #94a3b8;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.stat-grid {
|
|
133
|
+
display: grid;
|
|
134
|
+
grid-template-columns: repeat(3, 1fr);
|
|
135
|
+
gap: 1rem;
|
|
136
|
+
text-align: center;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.stat-value {
|
|
140
|
+
font-size: 2rem;
|
|
141
|
+
font-weight: 700;
|
|
142
|
+
color: #60a5fa;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.stat-label {
|
|
146
|
+
font-size: 0.8rem;
|
|
147
|
+
color: #94a3b8;
|
|
148
|
+
text-transform: uppercase;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.severity-badges {
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 1rem;
|
|
154
|
+
margin-top: 1rem;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.severity-badge {
|
|
158
|
+
padding: 0.5rem 1rem;
|
|
159
|
+
border-radius: 2rem;
|
|
160
|
+
font-size: 0.9rem;
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.severity-error { background: rgba(239,68,68,0.2); color: #ef4444; }
|
|
165
|
+
.severity-warning { background: rgba(245,158,11,0.2); color: #f59e0b; }
|
|
166
|
+
.severity-info { background: rgba(59,130,246,0.2); color: #3b82f6; }
|
|
167
|
+
|
|
168
|
+
.issues-by-type {
|
|
169
|
+
margin-top: 1rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.type-bar {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
margin: 0.75rem 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.type-name {
|
|
179
|
+
flex: 1;
|
|
180
|
+
font-size: 0.9rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.type-bar-bg {
|
|
184
|
+
width: 150px;
|
|
185
|
+
height: 20px;
|
|
186
|
+
background: rgba(255,255,255,0.1);
|
|
187
|
+
border-radius: 4px;
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
margin: 0 1rem;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.type-bar-fill {
|
|
193
|
+
height: 100%;
|
|
194
|
+
background: linear-gradient(90deg, #60a5fa, #a78bfa);
|
|
195
|
+
border-radius: 4px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.type-count {
|
|
199
|
+
width: 40px;
|
|
200
|
+
text-align: right;
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.findings-section {
|
|
205
|
+
margin-top: 2rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.findings-section h2 {
|
|
209
|
+
font-size: 1.5rem;
|
|
210
|
+
margin-bottom: 1rem;
|
|
211
|
+
padding-bottom: 0.5rem;
|
|
212
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.file-group {
|
|
216
|
+
margin-bottom: 1.5rem;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.file-header {
|
|
220
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
221
|
+
font-size: 0.9rem;
|
|
222
|
+
color: #60a5fa;
|
|
223
|
+
padding: 0.75rem 1rem;
|
|
224
|
+
background: rgba(96,165,250,0.1);
|
|
225
|
+
border-radius: 0.5rem 0.5rem 0 0;
|
|
226
|
+
border: 1px solid rgba(96,165,250,0.2);
|
|
227
|
+
border-bottom: none;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.issue {
|
|
231
|
+
padding: 1rem;
|
|
232
|
+
background: rgba(255,255,255,0.03);
|
|
233
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
234
|
+
border-top: none;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.issue:last-child {
|
|
238
|
+
border-radius: 0 0 0.5rem 0.5rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.issue-header {
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: flex-start;
|
|
244
|
+
gap: 0.75rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.issue-icon {
|
|
248
|
+
font-size: 1rem;
|
|
249
|
+
padding: 0.25rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.issue-message {
|
|
253
|
+
flex: 1;
|
|
254
|
+
font-weight: 500;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.issue-line {
|
|
258
|
+
font-family: monospace;
|
|
259
|
+
font-size: 0.8rem;
|
|
260
|
+
color: #94a3b8;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.issue-suggestion {
|
|
264
|
+
margin-top: 0.5rem;
|
|
265
|
+
padding: 0.5rem 0.75rem;
|
|
266
|
+
background: rgba(167,139,250,0.1);
|
|
267
|
+
border-left: 3px solid #a78bfa;
|
|
268
|
+
font-size: 0.9rem;
|
|
269
|
+
color: #c4b5fd;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.code-snippet {
|
|
273
|
+
margin-top: 0.5rem;
|
|
274
|
+
padding: 0.75rem;
|
|
275
|
+
background: #0d1117;
|
|
276
|
+
border-radius: 0.5rem;
|
|
277
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
278
|
+
font-size: 0.8rem;
|
|
279
|
+
overflow-x: auto;
|
|
280
|
+
white-space: pre;
|
|
281
|
+
color: #c9d1d9;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.refactor-time {
|
|
285
|
+
font-size: 1.1rem;
|
|
286
|
+
color: #f59e0b;
|
|
287
|
+
margin-top: 0.5rem;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
footer {
|
|
291
|
+
text-align: center;
|
|
292
|
+
padding: 2rem;
|
|
293
|
+
color: #64748b;
|
|
294
|
+
font-size: 0.9rem;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@keyframes fadeIn {
|
|
298
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
299
|
+
to { opacity: 1; transform: translateY(0); }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.card, .file-group {
|
|
303
|
+
animation: fadeIn 0.5s ease forwards;
|
|
304
|
+
}
|
|
305
|
+
</style>
|
|
306
|
+
</head>
|
|
307
|
+
<body>
|
|
308
|
+
<div class="container">
|
|
309
|
+
<header>
|
|
310
|
+
<h1>đ Code Smell Detector</h1>
|
|
311
|
+
<p class="subtitle">Analysis Report - ${new Date().toLocaleDateString()}</p>
|
|
312
|
+
</header>
|
|
313
|
+
|
|
314
|
+
<div class="dashboard">
|
|
315
|
+
<div class="card">
|
|
316
|
+
<h2>Technical Debt Score</h2>
|
|
317
|
+
<div class="grade-container">
|
|
318
|
+
<div class="grade">${debtScore.grade}</div>
|
|
319
|
+
<div class="score-details">
|
|
320
|
+
<div class="score-label"><span>Overall Score</span><span>${debtScore.score}/100</span></div>
|
|
321
|
+
<div class="score-bar">
|
|
322
|
+
<div class="score-bar-fill" style="width: ${debtScore.score}%; background: ${gradeColors[debtScore.grade]}"></div>
|
|
323
|
+
</div>
|
|
324
|
+
${generateBreakdownBars(debtScore.breakdown)}
|
|
325
|
+
<p class="refactor-time">âąī¸ Est. refactor time: ${debtScore.estimatedRefactorTime}</p>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div class="card">
|
|
331
|
+
<h2>Summary</h2>
|
|
332
|
+
<div class="stat-grid">
|
|
333
|
+
<div>
|
|
334
|
+
<div class="stat-value">${summary.totalFiles}</div>
|
|
335
|
+
<div class="stat-label">Files</div>
|
|
336
|
+
</div>
|
|
337
|
+
<div>
|
|
338
|
+
<div class="stat-value">${summary.totalComponents}</div>
|
|
339
|
+
<div class="stat-label">Components</div>
|
|
340
|
+
</div>
|
|
341
|
+
<div>
|
|
342
|
+
<div class="stat-value">${summary.totalSmells}</div>
|
|
343
|
+
<div class="stat-label">Issues</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="severity-badges">
|
|
347
|
+
<span class="severity-badge severity-error">${summary.smellsBySeverity.error} Errors</span>
|
|
348
|
+
<span class="severity-badge severity-warning">${summary.smellsBySeverity.warning} Warnings</span>
|
|
349
|
+
<span class="severity-badge severity-info">${summary.smellsBySeverity.info} Info</span>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<div class="card">
|
|
354
|
+
<h2>Issues by Type</h2>
|
|
355
|
+
<div class="issues-by-type">
|
|
356
|
+
${smellTypeData.map(([type, count]) => {
|
|
357
|
+
const maxCount = Math.max(...smellTypeData.map(([_, c]) => c));
|
|
358
|
+
const percentage = (count / maxCount) * 100;
|
|
359
|
+
return `
|
|
360
|
+
<div class="type-bar">
|
|
361
|
+
<span class="type-name">${formatTypeLabel(type)}</span>
|
|
362
|
+
<div class="type-bar-bg">
|
|
363
|
+
<div class="type-bar-fill" style="width: ${percentage}%"></div>
|
|
364
|
+
</div>
|
|
365
|
+
<span class="type-count">${count}</span>
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
}).join('')}
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<section class="findings-section">
|
|
374
|
+
<h2>đ Detailed Findings</h2>
|
|
375
|
+
${files.filter(f => f.smells.length > 0).map(file => `
|
|
376
|
+
<div class="file-group">
|
|
377
|
+
<div class="file-header">${path.relative(rootDir, file.file)}</div>
|
|
378
|
+
${file.smells.map(smell => `
|
|
379
|
+
<div class="issue">
|
|
380
|
+
<div class="issue-header">
|
|
381
|
+
<span class="issue-icon">${getSeverityIcon(smell.severity)}</span>
|
|
382
|
+
<span class="issue-message" style="color: ${severityColors[smell.severity]}">${escapeHtml(smell.message)}</span>
|
|
383
|
+
<span class="issue-line">Line ${smell.line}</span>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="issue-suggestion">đĄ ${escapeHtml(smell.suggestion)}</div>
|
|
386
|
+
${smell.codeSnippet ? `<div class="code-snippet">${escapeHtml(smell.codeSnippet)}</div>` : ''}
|
|
387
|
+
</div>
|
|
388
|
+
`).join('')}
|
|
389
|
+
</div>
|
|
390
|
+
`).join('')}
|
|
391
|
+
</section>
|
|
392
|
+
|
|
393
|
+
<footer>
|
|
394
|
+
Generated by React Code Smell Detector âĸ ${new Date().toISOString()}
|
|
395
|
+
</footer>
|
|
396
|
+
</div>
|
|
397
|
+
</body>
|
|
398
|
+
</html>`;
|
|
399
|
+
}
|
|
400
|
+
function generateBreakdownBars(breakdown) {
|
|
401
|
+
const items = [
|
|
402
|
+
{ label: 'useEffect', score: breakdown.useEffectScore },
|
|
403
|
+
{ label: 'Prop Drilling', score: breakdown.propDrillingScore },
|
|
404
|
+
{ label: 'Component Size', score: breakdown.componentSizeScore },
|
|
405
|
+
{ label: 'Memoization', score: breakdown.memoizationScore },
|
|
406
|
+
];
|
|
407
|
+
return items.map(({ label, score }) => {
|
|
408
|
+
const color = score >= 80 ? '#22c55e' : score >= 60 ? '#eab308' : '#ef4444';
|
|
409
|
+
return `
|
|
410
|
+
<div class="score-label" style="margin-top: 0.5rem"><span>${label}</span><span>${score}</span></div>
|
|
411
|
+
<div class="score-bar">
|
|
412
|
+
<div class="score-bar-fill" style="width: ${score}%; background: ${color}"></div>
|
|
413
|
+
</div>
|
|
414
|
+
`;
|
|
415
|
+
}).join('');
|
|
416
|
+
}
|
|
417
|
+
function formatTypeLabel(type) {
|
|
418
|
+
const labels = {
|
|
419
|
+
'useEffect-overuse': '⥠useEffect',
|
|
420
|
+
'prop-drilling': 'đ Prop Drilling',
|
|
421
|
+
'large-component': 'đ Large Component',
|
|
422
|
+
'unmemoized-calculation': 'đž Unmemoized',
|
|
423
|
+
'inline-function-prop': 'đ Inline Func',
|
|
424
|
+
'deep-nesting': 'đ Deep Nesting',
|
|
425
|
+
'missing-key': 'đ Missing Key',
|
|
426
|
+
'magic-value': 'đĸ Magic Value',
|
|
427
|
+
'debug-statement': 'đ Debug',
|
|
428
|
+
'todo-comment': 'đ TODO',
|
|
429
|
+
'security-xss': 'đ Security XSS',
|
|
430
|
+
'security-eval': 'đ Security Eval',
|
|
431
|
+
'security-secrets': 'đ Secrets',
|
|
432
|
+
'a11y-missing-alt': 'âŋ Missing Alt',
|
|
433
|
+
'a11y-missing-label': 'âŋ Missing Label',
|
|
434
|
+
'ts-any-usage': 'đˇ TS any',
|
|
435
|
+
'ts-missing-return-type': 'đˇ TS Return Type',
|
|
436
|
+
};
|
|
437
|
+
return labels[type] || type.replace(/-/g, ' ');
|
|
438
|
+
}
|
|
439
|
+
function getSeverityIcon(severity) {
|
|
440
|
+
switch (severity) {
|
|
441
|
+
case 'error': return 'â';
|
|
442
|
+
case 'warning': return 'â ī¸';
|
|
443
|
+
case 'info': return 'âšī¸';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function escapeHtml(text) {
|
|
447
|
+
return text
|
|
448
|
+
.replace(/&/g, '&')
|
|
449
|
+
.replace(/</g, '<')
|
|
450
|
+
.replace(/>/g, '>')
|
|
451
|
+
.replace(/"/g, '"')
|
|
452
|
+
.replace(/'/g, ''');
|
|
453
|
+
}
|
package/dist/reporter.js
CHANGED
|
@@ -242,6 +242,19 @@ function formatSmellType(type) {
|
|
|
242
242
|
'ts-missing-return-type': 'đˇ TS Missing Return Type',
|
|
243
243
|
'ts-non-null-assertion': 'đˇ TS Non-null Assertion',
|
|
244
244
|
'ts-type-assertion': 'đˇ TS Type Assertion',
|
|
245
|
+
// Debug
|
|
246
|
+
'debug-statement': 'đ Debug Statement',
|
|
247
|
+
'todo-comment': 'đ TODO/FIXME Comment',
|
|
248
|
+
// Security
|
|
249
|
+
'security-xss': 'đ XSS Vulnerability',
|
|
250
|
+
'security-eval': 'đ Eval Usage',
|
|
251
|
+
'security-secrets': 'đ Exposed Secret',
|
|
252
|
+
// Accessibility
|
|
253
|
+
'a11y-missing-alt': 'âŋ Missing Alt Text',
|
|
254
|
+
'a11y-missing-label': 'âŋ Missing Label',
|
|
255
|
+
'a11y-interactive-role': 'âŋ Interactive Role',
|
|
256
|
+
'a11y-keyboard': 'âŋ Keyboard Handler',
|
|
257
|
+
'a11y-semantic': 'âŋ Semantic HTML',
|
|
245
258
|
};
|
|
246
259
|
return labels[type] || type;
|
|
247
260
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type SmellSeverity = 'error' | 'warning' | 'info';
|
|
2
|
-
export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting' | 'missing-key' | 'hooks-rules-violation' | 'dependency-array-issue' | 'nested-ternary' | 'dead-code' | 'magic-value' | 'nextjs-client-server-boundary' | 'nextjs-missing-metadata' | 'nextjs-image-unoptimized' | 'nextjs-router-misuse' | 'rn-inline-style' | 'rn-missing-accessibility' | 'rn-performance-issue' | 'nodejs-callback-hell' | 'nodejs-unhandled-promise' | 'nodejs-sync-io' | 'nodejs-missing-error-handling' | 'js-var-usage' | 'js-loose-equality' | 'js-implicit-coercion' | 'js-global-pollution' | 'ts-any-usage' | 'ts-missing-return-type' | 'ts-non-null-assertion' | 'ts-type-assertion';
|
|
2
|
+
export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting' | 'missing-key' | 'hooks-rules-violation' | 'dependency-array-issue' | 'nested-ternary' | 'dead-code' | 'magic-value' | 'debug-statement' | 'todo-comment' | 'security-xss' | 'security-eval' | 'security-secrets' | 'a11y-missing-alt' | 'a11y-missing-label' | 'a11y-interactive-role' | 'a11y-keyboard' | 'a11y-semantic' | 'nextjs-client-server-boundary' | 'nextjs-missing-metadata' | 'nextjs-image-unoptimized' | 'nextjs-router-misuse' | 'rn-inline-style' | 'rn-missing-accessibility' | 'rn-performance-issue' | 'nodejs-callback-hell' | 'nodejs-unhandled-promise' | 'nodejs-sync-io' | 'nodejs-missing-error-handling' | 'js-var-usage' | 'js-loose-equality' | 'js-implicit-coercion' | 'js-global-pollution' | 'ts-any-usage' | 'ts-missing-return-type' | 'ts-non-null-assertion' | 'ts-type-assertion';
|
|
3
3
|
export interface CodeSmell {
|
|
4
4
|
type: SmellType;
|
|
5
5
|
severity: SmellSeverity;
|
|
@@ -72,6 +72,9 @@ export interface DetectorConfig {
|
|
|
72
72
|
checkJavascript: boolean;
|
|
73
73
|
checkTypescript: boolean;
|
|
74
74
|
maxCallbackDepth: number;
|
|
75
|
+
checkDebugStatements: boolean;
|
|
76
|
+
checkSecurity: boolean;
|
|
77
|
+
checkAccessibility: boolean;
|
|
75
78
|
}
|
|
76
79
|
export declare const DEFAULT_CONFIG: DetectorConfig;
|
|
77
80
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|