react-code-smell-detector 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +227 -22
- package/dist/__tests__/aiRefactoring.test.d.ts +2 -0
- package/dist/__tests__/aiRefactoring.test.d.ts.map +1 -0
- package/dist/__tests__/aiRefactoring.test.js +86 -0
- package/dist/__tests__/analyzer-real.test.d.ts +2 -0
- package/dist/__tests__/analyzer-real.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer-real.test.js +149 -0
- package/dist/__tests__/analyzer.test.d.ts +2 -0
- package/dist/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer.test.js +173 -0
- package/dist/__tests__/baseline.test.d.ts +2 -0
- package/dist/__tests__/baseline.test.d.ts.map +1 -0
- package/dist/__tests__/baseline.test.js +136 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts +2 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts.map +1 -0
- package/dist/__tests__/bundleAnalyzer.test.js +182 -0
- package/dist/__tests__/customRules.test.d.ts +2 -0
- package/dist/__tests__/customRules.test.d.ts.map +1 -0
- package/dist/__tests__/customRules.test.js +283 -0
- package/dist/__tests__/detectors/index.test.d.ts +2 -0
- package/dist/__tests__/detectors/index.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/index.test.js +1012 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts +2 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/newDetectors.test.js +333 -0
- package/dist/__tests__/docGenerator.test.d.ts +2 -0
- package/dist/__tests__/docGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/docGenerator.test.js +157 -0
- package/dist/__tests__/fixer.test.d.ts +2 -0
- package/dist/__tests__/fixer.test.d.ts.map +1 -0
- package/dist/__tests__/fixer.test.js +193 -0
- package/dist/__tests__/git.test.d.ts +2 -0
- package/dist/__tests__/git.test.d.ts.map +1 -0
- package/dist/__tests__/git.test.js +38 -0
- package/dist/__tests__/graphGenerator.test.d.ts +2 -0
- package/dist/__tests__/graphGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/graphGenerator.test.js +190 -0
- package/dist/__tests__/htmlReporter.test.d.ts +2 -0
- package/dist/__tests__/htmlReporter.test.d.ts.map +1 -0
- package/dist/__tests__/htmlReporter.test.js +258 -0
- package/dist/__tests__/interactiveFixer.test.d.ts +2 -0
- package/dist/__tests__/interactiveFixer.test.d.ts.map +1 -0
- package/dist/__tests__/interactiveFixer.test.js +231 -0
- package/dist/__tests__/parser.test.d.ts +2 -0
- package/dist/__tests__/parser.test.d.ts.map +1 -0
- package/dist/__tests__/parser.test.js +56 -0
- package/dist/__tests__/performanceBudget.test.d.ts +2 -0
- package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
- package/dist/__tests__/performanceBudget.test.js +242 -0
- package/dist/__tests__/prComments.test.d.ts +2 -0
- package/dist/__tests__/prComments.test.d.ts.map +1 -0
- package/dist/__tests__/prComments.test.js +118 -0
- package/dist/__tests__/reporter.test.d.ts +2 -0
- package/dist/__tests__/reporter.test.d.ts.map +1 -0
- package/dist/__tests__/reporter.test.js +136 -0
- package/dist/__tests__/watcher.test.d.ts +2 -0
- package/dist/__tests__/watcher.test.d.ts.map +1 -0
- package/dist/__tests__/watcher.test.js +161 -0
- package/dist/__tests__/webhooks.test.d.ts +2 -0
- package/dist/__tests__/webhooks.test.d.ts.map +1 -0
- package/dist/__tests__/webhooks.test.js +209 -0
- package/dist/aiRefactoring.d.ts +29 -0
- package/dist/aiRefactoring.d.ts.map +1 -0
- package/dist/aiRefactoring.js +290 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +33 -1
- package/dist/cli.js +123 -1
- package/dist/detectors/contextApi.d.ts +11 -0
- package/dist/detectors/contextApi.d.ts.map +1 -0
- package/dist/detectors/contextApi.js +151 -0
- package/dist/detectors/errorBoundary.d.ts +11 -0
- package/dist/detectors/errorBoundary.d.ts.map +1 -0
- package/dist/detectors/errorBoundary.js +167 -0
- package/dist/detectors/formValidation.d.ts +11 -0
- package/dist/detectors/formValidation.d.ts.map +1 -0
- package/dist/detectors/formValidation.js +193 -0
- package/dist/detectors/index.d.ts +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +12 -0
- package/dist/detectors/serverComponents.d.ts +11 -0
- package/dist/detectors/serverComponents.d.ts.map +1 -0
- package/dist/detectors/serverComponents.js +222 -0
- package/dist/detectors/stateManagement.d.ts +11 -0
- package/dist/detectors/stateManagement.d.ts.map +1 -0
- package/dist/detectors/stateManagement.js +193 -0
- package/dist/detectors/testingGaps.d.ts +15 -0
- package/dist/detectors/testingGaps.d.ts.map +1 -0
- package/dist/detectors/testingGaps.js +182 -0
- package/dist/docGenerator.d.ts +37 -0
- package/dist/docGenerator.d.ts.map +1 -0
- package/dist/docGenerator.js +306 -0
- package/dist/guide.d.ts +9 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +922 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/interactiveFixer.d.ts +20 -0
- package/dist/interactiveFixer.d.ts.map +1 -0
- package/dist/interactiveFixer.js +178 -0
- package/dist/performanceBudget.d.ts +54 -0
- package/dist/performanceBudget.d.ts.map +1 -0
- package/dist/performanceBudget.js +218 -0
- package/dist/prComments.d.ts +47 -0
- package/dist/prComments.d.ts.map +1 -0
- package/dist/prComments.js +233 -0
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +18 -0
- package/package.json +10 -4
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateHTMLReport } from '../htmlReporter.js';
|
|
3
|
+
import { DEFAULT_CONFIG } from '../types/index.js';
|
|
4
|
+
describe('HTML Reporter', () => {
|
|
5
|
+
const createMockResult = (overrides = {}) => {
|
|
6
|
+
const defaultSmellsByType = Object.fromEntries(Object.keys(DEFAULT_CONFIG).filter(k => k !== 'customRulesPath').map(k => [k, 0]));
|
|
7
|
+
// Add all required smell types
|
|
8
|
+
const smellsByType = {
|
|
9
|
+
'useEffect-overuse': 0,
|
|
10
|
+
'prop-drilling': 0,
|
|
11
|
+
'large-component': 0,
|
|
12
|
+
'unmemoized-calculation': 0,
|
|
13
|
+
'missing-dependency': 0,
|
|
14
|
+
'state-in-loop': 0,
|
|
15
|
+
'inline-function-prop': 0,
|
|
16
|
+
'deep-nesting': 0,
|
|
17
|
+
'missing-key': 0,
|
|
18
|
+
'hooks-rules-violation': 0,
|
|
19
|
+
'dependency-array-issue': 0,
|
|
20
|
+
'nested-ternary': 0,
|
|
21
|
+
'dead-code': 0,
|
|
22
|
+
'magic-value': 0,
|
|
23
|
+
'nextjs-client-server-boundary': 0,
|
|
24
|
+
'nextjs-missing-metadata': 0,
|
|
25
|
+
'nextjs-image-unoptimized': 0,
|
|
26
|
+
'nextjs-router-misuse': 0,
|
|
27
|
+
'rn-inline-style': 0,
|
|
28
|
+
'rn-missing-accessibility': 0,
|
|
29
|
+
'rn-performance-issue': 0,
|
|
30
|
+
'nodejs-callback-hell': 0,
|
|
31
|
+
'nodejs-unhandled-promise': 0,
|
|
32
|
+
'nodejs-sync-io': 0,
|
|
33
|
+
'nodejs-missing-error-handling': 0,
|
|
34
|
+
'js-var-usage': 0,
|
|
35
|
+
'js-loose-equality': 0,
|
|
36
|
+
'js-implicit-coercion': 0,
|
|
37
|
+
'js-global-pollution': 0,
|
|
38
|
+
'ts-any-usage': 0,
|
|
39
|
+
'ts-missing-return-type': 0,
|
|
40
|
+
'ts-non-null-assertion': 0,
|
|
41
|
+
'ts-type-assertion': 0,
|
|
42
|
+
'debug-statement': 0,
|
|
43
|
+
'todo-comment': 0,
|
|
44
|
+
'security-xss': 0,
|
|
45
|
+
'security-eval': 0,
|
|
46
|
+
'security-secrets': 0,
|
|
47
|
+
'a11y-missing-alt': 0,
|
|
48
|
+
'a11y-missing-label': 0,
|
|
49
|
+
'a11y-interactive-role': 0,
|
|
50
|
+
'a11y-keyboard': 0,
|
|
51
|
+
'a11y-semantic': 0,
|
|
52
|
+
'high-cyclomatic-complexity': 0,
|
|
53
|
+
'high-cognitive-complexity': 0,
|
|
54
|
+
'memory-leak-event-listener': 0,
|
|
55
|
+
'memory-leak-subscription': 0,
|
|
56
|
+
'memory-leak-timer': 0,
|
|
57
|
+
'memory-leak-async': 0,
|
|
58
|
+
'circular-dependency': 0,
|
|
59
|
+
'barrel-file-import': 0,
|
|
60
|
+
'namespace-import': 0,
|
|
61
|
+
'excessive-imports': 0,
|
|
62
|
+
'unused-export': 0,
|
|
63
|
+
'dead-import': 0,
|
|
64
|
+
'server-component-hooks': 0,
|
|
65
|
+
'server-component-events': 0,
|
|
66
|
+
'server-component-browser-api': 0,
|
|
67
|
+
'async-client-component': 0,
|
|
68
|
+
'mixed-directives': 0,
|
|
69
|
+
// Context API issues
|
|
70
|
+
'context-overuse': 0,
|
|
71
|
+
'context-in-loop': 0,
|
|
72
|
+
'missing-context-memo': 0,
|
|
73
|
+
'large-context-value': 0,
|
|
74
|
+
// Error Boundary issues
|
|
75
|
+
'missing-error-boundary': 0,
|
|
76
|
+
'error-boundary-missing-fallback': 0,
|
|
77
|
+
'suspense-missing-fallback': 0,
|
|
78
|
+
// Form validation issues
|
|
79
|
+
'uncontrolled-form': 0,
|
|
80
|
+
'missing-form-validation': 0,
|
|
81
|
+
'form-without-onsubmit': 0,
|
|
82
|
+
'input-without-label': 0,
|
|
83
|
+
// State management issues
|
|
84
|
+
'redux-in-render': 0,
|
|
85
|
+
'excessive-redux-selectors': 0,
|
|
86
|
+
'state-sync-anti-pattern': 0,
|
|
87
|
+
'derived-state-in-state': 0,
|
|
88
|
+
// Testing gaps
|
|
89
|
+
'complex-untestable': 0,
|
|
90
|
+
'side-effect-heavy': 0,
|
|
91
|
+
'tightly-coupled': 0,
|
|
92
|
+
'custom-rule': 0,
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
files: [],
|
|
96
|
+
summary: {
|
|
97
|
+
totalFiles: 0,
|
|
98
|
+
totalComponents: 0,
|
|
99
|
+
totalSmells: 0,
|
|
100
|
+
smellsByType,
|
|
101
|
+
smellsBySeverity: {
|
|
102
|
+
error: 0,
|
|
103
|
+
warning: 0,
|
|
104
|
+
info: 0,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
debtScore: {
|
|
108
|
+
score: 100,
|
|
109
|
+
grade: 'A',
|
|
110
|
+
breakdown: {
|
|
111
|
+
useEffectScore: 100,
|
|
112
|
+
propDrillingScore: 100,
|
|
113
|
+
componentSizeScore: 100,
|
|
114
|
+
memoizationScore: 100,
|
|
115
|
+
},
|
|
116
|
+
estimatedRefactorTime: '< 30 minutes',
|
|
117
|
+
},
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
describe('generateHTMLReport', () => {
|
|
122
|
+
it('should generate valid HTML document', () => {
|
|
123
|
+
const result = createMockResult();
|
|
124
|
+
const html = generateHTMLReport(result, '/root');
|
|
125
|
+
expect(html).toContain('<!DOCTYPE html>');
|
|
126
|
+
expect(html).toContain('<html');
|
|
127
|
+
expect(html).toContain('</html>');
|
|
128
|
+
});
|
|
129
|
+
it('should include title', () => {
|
|
130
|
+
const result = createMockResult();
|
|
131
|
+
const html = generateHTMLReport(result, '/root');
|
|
132
|
+
expect(html).toContain('<title>');
|
|
133
|
+
expect(html).toContain('Code Smell');
|
|
134
|
+
});
|
|
135
|
+
it('should include CSS styles', () => {
|
|
136
|
+
const result = createMockResult();
|
|
137
|
+
const html = generateHTMLReport(result, '/root');
|
|
138
|
+
expect(html).toContain('<style>');
|
|
139
|
+
expect(html).toContain('</style>');
|
|
140
|
+
});
|
|
141
|
+
it('should display technical debt grade', () => {
|
|
142
|
+
const result = createMockResult();
|
|
143
|
+
const html = generateHTMLReport(result, '/root');
|
|
144
|
+
expect(html).toContain('A'); // Grade A
|
|
145
|
+
});
|
|
146
|
+
it('should display summary statistics', () => {
|
|
147
|
+
const result = createMockResult({
|
|
148
|
+
summary: {
|
|
149
|
+
...createMockResult().summary,
|
|
150
|
+
totalFiles: 10,
|
|
151
|
+
totalComponents: 25,
|
|
152
|
+
totalSmells: 5,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const html = generateHTMLReport(result, '/root');
|
|
156
|
+
expect(html).toContain('10');
|
|
157
|
+
expect(html).toContain('25');
|
|
158
|
+
});
|
|
159
|
+
it('should handle smells with various severities', () => {
|
|
160
|
+
const result = createMockResult({
|
|
161
|
+
summary: {
|
|
162
|
+
...createMockResult().summary,
|
|
163
|
+
smellsBySeverity: {
|
|
164
|
+
error: 3,
|
|
165
|
+
warning: 5,
|
|
166
|
+
info: 2,
|
|
167
|
+
},
|
|
168
|
+
totalSmells: 10,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const html = generateHTMLReport(result, '/root');
|
|
172
|
+
expect(html).toContain('error');
|
|
173
|
+
});
|
|
174
|
+
it('should include breakdown scores', () => {
|
|
175
|
+
const result = createMockResult();
|
|
176
|
+
const html = generateHTMLReport(result, '/root');
|
|
177
|
+
expect(html).toContain('100'); // Perfect scores
|
|
178
|
+
});
|
|
179
|
+
it('should handle files with smells', () => {
|
|
180
|
+
const result = createMockResult({
|
|
181
|
+
files: [
|
|
182
|
+
{
|
|
183
|
+
file: '/root/src/App.tsx',
|
|
184
|
+
components: [
|
|
185
|
+
{
|
|
186
|
+
name: 'App',
|
|
187
|
+
file: '/root/src/App.tsx',
|
|
188
|
+
startLine: 1,
|
|
189
|
+
endLine: 20,
|
|
190
|
+
lineCount: 20,
|
|
191
|
+
useEffectCount: 1,
|
|
192
|
+
useStateCount: 2,
|
|
193
|
+
useMemoCount: 0,
|
|
194
|
+
useCallbackCount: 0,
|
|
195
|
+
propsCount: 3,
|
|
196
|
+
propsDrillingDepth: 0,
|
|
197
|
+
hasExpensiveCalculation: false,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
smells: [
|
|
201
|
+
{
|
|
202
|
+
type: 'debug-statement',
|
|
203
|
+
severity: 'warning',
|
|
204
|
+
message: 'Console.log found',
|
|
205
|
+
file: '/root/src/App.tsx',
|
|
206
|
+
line: 10,
|
|
207
|
+
column: 5,
|
|
208
|
+
suggestion: 'Remove debug statement',
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
imports: [],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
const html = generateHTMLReport(result, '/root');
|
|
216
|
+
expect(html).toContain('App.tsx');
|
|
217
|
+
});
|
|
218
|
+
it('should handle all grade levels', () => {
|
|
219
|
+
const grades = ['A', 'B', 'C', 'D', 'F'];
|
|
220
|
+
for (const grade of grades) {
|
|
221
|
+
const result = createMockResult({
|
|
222
|
+
debtScore: {
|
|
223
|
+
...createMockResult().debtScore,
|
|
224
|
+
grade,
|
|
225
|
+
score: grade === 'A' ? 95 : grade === 'B' ? 85 : grade === 'C' ? 75 : grade === 'D' ? 65 : 50,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
const html = generateHTMLReport(result, '/root');
|
|
229
|
+
expect(html).toContain(grade);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
it('should escape HTML in file paths', () => {
|
|
233
|
+
const result = createMockResult({
|
|
234
|
+
files: [
|
|
235
|
+
{
|
|
236
|
+
file: '/root/src/<Component>.tsx',
|
|
237
|
+
components: [],
|
|
238
|
+
smells: [],
|
|
239
|
+
imports: [],
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
const html = generateHTMLReport(result, '/root');
|
|
244
|
+
// Should still generate valid HTML
|
|
245
|
+
expect(html).toContain('<!DOCTYPE html>');
|
|
246
|
+
});
|
|
247
|
+
it('should include estimated refactor time', () => {
|
|
248
|
+
const result = createMockResult({
|
|
249
|
+
debtScore: {
|
|
250
|
+
...createMockResult().debtScore,
|
|
251
|
+
estimatedRefactorTime: '2-4 hours',
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
const html = generateHTMLReport(result, '/root');
|
|
255
|
+
expect(html).toContain('2-4 hours');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactiveFixer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/interactiveFixer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { previewFixes } from '../interactiveFixer.js';
|
|
3
|
+
describe('Interactive Fixer', () => {
|
|
4
|
+
let consoleLogSpy;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
consoleLogSpy.mockRestore();
|
|
10
|
+
});
|
|
11
|
+
const createFixableSmell = (type, file = '/project/src/test.tsx', line = 10) => ({
|
|
12
|
+
type: type,
|
|
13
|
+
severity: 'warning',
|
|
14
|
+
message: 'Test smell',
|
|
15
|
+
file,
|
|
16
|
+
line,
|
|
17
|
+
column: 5,
|
|
18
|
+
suggestion: 'Fix this issue',
|
|
19
|
+
fix: {
|
|
20
|
+
type: 'replace',
|
|
21
|
+
oldCode: 'var x = 1',
|
|
22
|
+
newCode: 'const x = 1',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const createNonFixableSmell = (type) => ({
|
|
26
|
+
type: type,
|
|
27
|
+
severity: 'warning',
|
|
28
|
+
message: 'Test smell',
|
|
29
|
+
file: '/project/src/test.tsx',
|
|
30
|
+
line: 10,
|
|
31
|
+
column: 5,
|
|
32
|
+
suggestion: 'Fix this issue',
|
|
33
|
+
});
|
|
34
|
+
describe('Fix filtering', () => {
|
|
35
|
+
it('should identify fixable smells', () => {
|
|
36
|
+
const fixable = createFixableSmell('js-var-usage');
|
|
37
|
+
const nonFixable = createNonFixableSmell('large-component');
|
|
38
|
+
expect(fixable.fix).toBeDefined();
|
|
39
|
+
expect(nonFixable.fix).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
it('should filter fixable smells from array', () => {
|
|
42
|
+
const smells = [
|
|
43
|
+
createFixableSmell('js-var-usage'),
|
|
44
|
+
createNonFixableSmell('large-component'),
|
|
45
|
+
createFixableSmell('debug-statement'),
|
|
46
|
+
createNonFixableSmell('prop-drilling'),
|
|
47
|
+
];
|
|
48
|
+
const fixable = smells.filter(s => s.fix !== undefined);
|
|
49
|
+
expect(fixable).toHaveLength(2);
|
|
50
|
+
expect(fixable[0].type).toBe('js-var-usage');
|
|
51
|
+
expect(fixable[1].type).toBe('debug-statement');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('Fix preview generation', () => {
|
|
55
|
+
it('should generate diff preview', () => {
|
|
56
|
+
const smell = createFixableSmell('js-var-usage');
|
|
57
|
+
expect(smell.fix?.oldCode).toBe('var x = 1');
|
|
58
|
+
expect(smell.fix?.newCode).toBe('const x = 1');
|
|
59
|
+
});
|
|
60
|
+
it('should handle multi-line fixes', () => {
|
|
61
|
+
const smell = {
|
|
62
|
+
type: 'debug-statement',
|
|
63
|
+
severity: 'warning',
|
|
64
|
+
message: 'Remove debug',
|
|
65
|
+
file: '/test.tsx',
|
|
66
|
+
line: 1,
|
|
67
|
+
column: 0,
|
|
68
|
+
suggestion: 'Remove console.log',
|
|
69
|
+
fix: {
|
|
70
|
+
type: 'delete',
|
|
71
|
+
oldCode: `console.log('debug');\nconsole.log('more');`,
|
|
72
|
+
newCode: '',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
expect(smell.fix?.oldCode).toContain('\n');
|
|
76
|
+
expect(smell.fix?.newCode).toBe('');
|
|
77
|
+
});
|
|
78
|
+
it('should handle insert type fixes', () => {
|
|
79
|
+
const smell = {
|
|
80
|
+
type: 'missing-key',
|
|
81
|
+
severity: 'error',
|
|
82
|
+
message: 'Missing key prop',
|
|
83
|
+
file: '/test.tsx',
|
|
84
|
+
line: 1,
|
|
85
|
+
column: 0,
|
|
86
|
+
suggestion: 'Add key prop',
|
|
87
|
+
fix: {
|
|
88
|
+
type: 'insert',
|
|
89
|
+
oldCode: '<Item />',
|
|
90
|
+
newCode: '<Item key={id} />',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
expect(smell.fix?.type).toBe('insert');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('Fix grouping by file', () => {
|
|
97
|
+
it('should group smells by file', () => {
|
|
98
|
+
const smells = [
|
|
99
|
+
{ ...createFixableSmell('js-var-usage'), file: '/a.tsx' },
|
|
100
|
+
{ ...createFixableSmell('debug-statement'), file: '/b.tsx' },
|
|
101
|
+
{ ...createFixableSmell('console-log'), file: '/a.tsx' },
|
|
102
|
+
];
|
|
103
|
+
const grouped = smells.reduce((acc, smell) => {
|
|
104
|
+
const file = smell.file;
|
|
105
|
+
if (!acc[file])
|
|
106
|
+
acc[file] = [];
|
|
107
|
+
acc[file].push(smell);
|
|
108
|
+
return acc;
|
|
109
|
+
}, {});
|
|
110
|
+
expect(Object.keys(grouped)).toHaveLength(2);
|
|
111
|
+
expect(grouped['/a.tsx']).toHaveLength(2);
|
|
112
|
+
expect(grouped['/b.tsx']).toHaveLength(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('Fix statistics', () => {
|
|
116
|
+
it('should count applied fixes', () => {
|
|
117
|
+
const smells = [
|
|
118
|
+
createFixableSmell('js-var-usage'),
|
|
119
|
+
createFixableSmell('debug-statement'),
|
|
120
|
+
createFixableSmell('console-log'),
|
|
121
|
+
];
|
|
122
|
+
const applied = smells.filter((_, i) => i < 2);
|
|
123
|
+
const skipped = smells.filter((_, i) => i >= 2);
|
|
124
|
+
expect(applied).toHaveLength(2);
|
|
125
|
+
expect(skipped).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
it('should categorize by fix type', () => {
|
|
128
|
+
const smells = [
|
|
129
|
+
{
|
|
130
|
+
...createFixableSmell('a'),
|
|
131
|
+
fix: { type: 'replace', oldCode: 'a', newCode: 'b' },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
...createFixableSmell('b'),
|
|
135
|
+
fix: { type: 'delete', oldCode: 'c', newCode: '' },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
...createFixableSmell('c'),
|
|
139
|
+
fix: { type: 'insert', oldCode: '', newCode: 'd' },
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
const byType = smells.reduce((acc, s) => {
|
|
143
|
+
const type = s.fix.type;
|
|
144
|
+
acc[type] = (acc[type] || 0) + 1;
|
|
145
|
+
return acc;
|
|
146
|
+
}, {});
|
|
147
|
+
expect(byType.replace).toBe(1);
|
|
148
|
+
expect(byType.delete).toBe(1);
|
|
149
|
+
expect(byType.insert).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('Fix ordering', () => {
|
|
153
|
+
it('should sort fixes by line number descending for safe application', () => {
|
|
154
|
+
const smells = [
|
|
155
|
+
{ ...createFixableSmell('a'), line: 5 },
|
|
156
|
+
{ ...createFixableSmell('b'), line: 20 },
|
|
157
|
+
{ ...createFixableSmell('c'), line: 10 },
|
|
158
|
+
];
|
|
159
|
+
// Sort descending so later fixes don't affect earlier line numbers
|
|
160
|
+
const sorted = [...smells].sort((a, b) => b.line - a.line);
|
|
161
|
+
expect(sorted[0].line).toBe(20);
|
|
162
|
+
expect(sorted[1].line).toBe(10);
|
|
163
|
+
expect(sorted[2].line).toBe(5);
|
|
164
|
+
});
|
|
165
|
+
it('should handle multiple fixes on same line by column', () => {
|
|
166
|
+
const smells = [
|
|
167
|
+
{ ...createFixableSmell('a'), line: 10, column: 5 },
|
|
168
|
+
{ ...createFixableSmell('b'), line: 10, column: 20 },
|
|
169
|
+
{ ...createFixableSmell('c'), line: 10, column: 10 },
|
|
170
|
+
];
|
|
171
|
+
// Sort by column descending for same line
|
|
172
|
+
const sorted = [...smells].sort((a, b) => {
|
|
173
|
+
if (a.line !== b.line)
|
|
174
|
+
return b.line - a.line;
|
|
175
|
+
return b.column - a.column;
|
|
176
|
+
});
|
|
177
|
+
expect(sorted[0].column).toBe(20);
|
|
178
|
+
expect(sorted[1].column).toBe(10);
|
|
179
|
+
expect(sorted[2].column).toBe(5);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('previewFixes', () => {
|
|
183
|
+
it('should show message when no fixable issues', () => {
|
|
184
|
+
const nonFixableSmell = {
|
|
185
|
+
type: 'large-component',
|
|
186
|
+
severity: 'warning',
|
|
187
|
+
message: 'Too large',
|
|
188
|
+
file: '/test.tsx',
|
|
189
|
+
line: 1,
|
|
190
|
+
column: 0,
|
|
191
|
+
suggestion: 'Split component',
|
|
192
|
+
};
|
|
193
|
+
previewFixes([nonFixableSmell], '/project');
|
|
194
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
it('should display fixable issues grouped by type', () => {
|
|
197
|
+
const smells = [
|
|
198
|
+
createFixableSmell('debug-statement', '/project/a.tsx', 10),
|
|
199
|
+
createFixableSmell('debug-statement', '/project/b.tsx', 20),
|
|
200
|
+
createFixableSmell('js-var-usage', '/project/c.tsx', 30),
|
|
201
|
+
];
|
|
202
|
+
previewFixes(smells, '/project');
|
|
203
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
it('should handle empty smells array', () => {
|
|
206
|
+
previewFixes([], '/project');
|
|
207
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
it('should handle many smells of same type', () => {
|
|
210
|
+
const smells = Array.from({ length: 10 }, (_, i) => createFixableSmell('debug-statement', `/project/file${i}.tsx`, i + 1));
|
|
211
|
+
previewFixes(smells, '/project');
|
|
212
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
it('should handle mixed fixable and non-fixable smells', () => {
|
|
215
|
+
const smells = [
|
|
216
|
+
createFixableSmell('debug-statement', '/project/a.tsx', 10),
|
|
217
|
+
{
|
|
218
|
+
type: 'large-component',
|
|
219
|
+
severity: 'warning',
|
|
220
|
+
message: 'Too large',
|
|
221
|
+
file: '/project/b.tsx',
|
|
222
|
+
line: 1,
|
|
223
|
+
column: 0,
|
|
224
|
+
suggestion: 'Split component',
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
previewFixes(smells, '/project');
|
|
228
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/parser.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCode } from '../parser/index.js';
|
|
3
|
+
describe('Parser', () => {
|
|
4
|
+
it('should parse a simple React component', () => {
|
|
5
|
+
const code = `
|
|
6
|
+
function Hello({ name }) {
|
|
7
|
+
return <div>Hello {name}</div>;
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
const result = parseCode(code);
|
|
11
|
+
expect(result.components).toHaveLength(1);
|
|
12
|
+
expect(result.components[0].name).toBe('Hello');
|
|
13
|
+
});
|
|
14
|
+
it('should detect useState hook', () => {
|
|
15
|
+
const code = `
|
|
16
|
+
function Counter() {
|
|
17
|
+
const [count, setCount] = useState(0);
|
|
18
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
const result = parseCode(code);
|
|
22
|
+
expect(result.components).toHaveLength(1);
|
|
23
|
+
expect(result.components[0].hooks.useState).toHaveLength(1);
|
|
24
|
+
});
|
|
25
|
+
it('should detect useEffect hook', () => {
|
|
26
|
+
const code = `
|
|
27
|
+
function Timer() {
|
|
28
|
+
const [time, setTime] = useState(0);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const id = setInterval(() => setTime(t => t + 1), 1000);
|
|
31
|
+
return () => clearInterval(id);
|
|
32
|
+
}, []);
|
|
33
|
+
return <span>{time}</span>;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
const result = parseCode(code);
|
|
37
|
+
expect(result.components).toHaveLength(1);
|
|
38
|
+
expect(result.components[0].hooks.useEffect).toHaveLength(1);
|
|
39
|
+
});
|
|
40
|
+
it('should extract component props', () => {
|
|
41
|
+
const code = `
|
|
42
|
+
function Card({ title, description, onClick }) {
|
|
43
|
+
return (
|
|
44
|
+
<div onClick={onClick}>
|
|
45
|
+
<h1>{title}</h1>
|
|
46
|
+
<p>{description}</p>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
const result = parseCode(code);
|
|
52
|
+
expect(result.components[0].props).toContain('title');
|
|
53
|
+
expect(result.components[0].props).toContain('description');
|
|
54
|
+
expect(result.components[0].props).toContain('onClick');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performanceBudget.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/performanceBudget.test.ts"],"names":[],"mappings":""}
|