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,1012 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { detectDebugStatements } from '../../detectors/debug.js';
|
|
3
|
+
import { detectJavascriptIssues } from '../../detectors/javascript.js';
|
|
4
|
+
import { detectComplexity } from '../../detectors/complexity.js';
|
|
5
|
+
import { detectUseEffectOveruse } from '../../detectors/useEffect.js';
|
|
6
|
+
import { detectHooksRulesViolations } from '../../detectors/hooksRules.js';
|
|
7
|
+
import { detectUnmemoizedCalculations } from '../../detectors/memoization.js';
|
|
8
|
+
import { detectServerComponentIssues, detectAsyncComponentIssues } from '../../detectors/serverComponents.js';
|
|
9
|
+
import { detectAccessibilityIssues } from '../../detectors/accessibility.js';
|
|
10
|
+
import { detectSecurityIssues } from '../../detectors/security.js';
|
|
11
|
+
import { detectPropDrilling, analyzePropDrillingDepth } from '../../detectors/propDrilling.js';
|
|
12
|
+
import { detectMemoryLeaks } from '../../detectors/memoryLeak.js';
|
|
13
|
+
import { detectMissingKeys } from '../../detectors/missingKey.js';
|
|
14
|
+
import { detectNestedTernaries } from '../../detectors/nestedTernary.js';
|
|
15
|
+
import { detectLargeComponent } from '../../detectors/largeComponent.js';
|
|
16
|
+
import { detectMagicValues } from '../../detectors/magicValues.js';
|
|
17
|
+
import { detectDeadCode } from '../../detectors/deadCode.js';
|
|
18
|
+
import { detectDependencyArrayIssues } from '../../detectors/dependencyArray.js';
|
|
19
|
+
import { detectImportIssues, analyzeImports } from '../../detectors/imports.js';
|
|
20
|
+
import { detectUnusedCode } from '../../detectors/unusedCode.js';
|
|
21
|
+
import { detectNextjsIssues } from '../../detectors/nextjs.js';
|
|
22
|
+
import { detectTypescriptIssues } from '../../detectors/typescript.js';
|
|
23
|
+
import { detectNodejsIssues } from '../../detectors/nodejs.js';
|
|
24
|
+
import { detectReactNativeIssues } from '../../detectors/reactNative.js';
|
|
25
|
+
import { parseCode } from '../../parser/index.js';
|
|
26
|
+
import { DEFAULT_CONFIG } from '../../types/index.js';
|
|
27
|
+
describe('Detectors Integration', () => {
|
|
28
|
+
// Helper to get first component from code
|
|
29
|
+
const getFirstComponent = (code) => {
|
|
30
|
+
const { components } = parseCode(code, 'test.tsx');
|
|
31
|
+
return components[0];
|
|
32
|
+
};
|
|
33
|
+
describe('detectDebugStatements', () => {
|
|
34
|
+
it('should detect console.log in component', () => {
|
|
35
|
+
const code = `
|
|
36
|
+
function TestComponent() {
|
|
37
|
+
console.log('test');
|
|
38
|
+
return <div>Test</div>;
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
const component = getFirstComponent(code);
|
|
42
|
+
expect(component).toBeDefined();
|
|
43
|
+
const smells = detectDebugStatements(component, '/test.tsx', code);
|
|
44
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
45
|
+
expect(smells.length).toBeGreaterThanOrEqual(1);
|
|
46
|
+
expect(smells[0].type).toBe('debug-statement');
|
|
47
|
+
});
|
|
48
|
+
it('should not flag clean component', () => {
|
|
49
|
+
const code = `
|
|
50
|
+
function CleanComponent() {
|
|
51
|
+
const value = 1;
|
|
52
|
+
return <div>{value}</div>;
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
const component = getFirstComponent(code);
|
|
56
|
+
expect(component).toBeDefined();
|
|
57
|
+
const smells = detectDebugStatements(component, '/test.tsx', code);
|
|
58
|
+
expect(smells.length).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('detectJavascriptIssues', () => {
|
|
62
|
+
it('should detect var usage', () => {
|
|
63
|
+
const code = `
|
|
64
|
+
function VarComponent() {
|
|
65
|
+
var x = 1;
|
|
66
|
+
return <div>{x}</div>;
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
const component = getFirstComponent(code);
|
|
70
|
+
expect(component).toBeDefined();
|
|
71
|
+
const smells = detectJavascriptIssues(component, '/test.tsx', code);
|
|
72
|
+
const varSmells = smells.filter(s => s.type === 'js-var-usage');
|
|
73
|
+
expect(varSmells.length).toBe(1);
|
|
74
|
+
});
|
|
75
|
+
it('should detect loose equality', () => {
|
|
76
|
+
const code = `
|
|
77
|
+
function EqualityComponent({ a, b }) {
|
|
78
|
+
if (a == b) return <div>Equal</div>;
|
|
79
|
+
return <div>Not</div>;
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
const component = getFirstComponent(code);
|
|
83
|
+
expect(component).toBeDefined();
|
|
84
|
+
const smells = detectJavascriptIssues(component, '/test.tsx', code);
|
|
85
|
+
const eqSmells = smells.filter(s => s.type === 'js-loose-equality');
|
|
86
|
+
expect(eqSmells.length).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('detectComplexity', () => {
|
|
90
|
+
it('should return array for component', () => {
|
|
91
|
+
const code = `
|
|
92
|
+
function SimpleComponent() {
|
|
93
|
+
return <div>Simple</div>;
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
const component = getFirstComponent(code);
|
|
97
|
+
expect(component).toBeDefined();
|
|
98
|
+
const smells = detectComplexity(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
99
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('detectUseEffectOveruse', () => {
|
|
103
|
+
it('should return array for component', () => {
|
|
104
|
+
const code = `
|
|
105
|
+
function EffectComponent() {
|
|
106
|
+
return <div>Effect</div>;
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
const component = getFirstComponent(code);
|
|
110
|
+
expect(component).toBeDefined();
|
|
111
|
+
const smells = detectUseEffectOveruse(component, '/test.tsx', code);
|
|
112
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('detectHooksRulesViolations', () => {
|
|
116
|
+
it('should return array for component', () => {
|
|
117
|
+
const code = `
|
|
118
|
+
function HooksComponent() {
|
|
119
|
+
return <div>Hooks</div>;
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
const component = getFirstComponent(code);
|
|
123
|
+
expect(component).toBeDefined();
|
|
124
|
+
const smells = detectHooksRulesViolations(component, '/test.tsx', code);
|
|
125
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('detectUnmemoizedCalculations', () => {
|
|
129
|
+
it('should return array for component', () => {
|
|
130
|
+
const code = `
|
|
131
|
+
function MemoComponent() {
|
|
132
|
+
return <div>Memo</div>;
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
const component = getFirstComponent(code);
|
|
136
|
+
expect(component).toBeDefined();
|
|
137
|
+
const smells = detectUnmemoizedCalculations(component, '/test.tsx', code);
|
|
138
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe('detectServerComponentIssues', () => {
|
|
142
|
+
it('should return array for component', () => {
|
|
143
|
+
const code = `
|
|
144
|
+
function ServerComponent() {
|
|
145
|
+
return <div>Server</div>;
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
const component = getFirstComponent(code);
|
|
149
|
+
expect(component).toBeDefined();
|
|
150
|
+
const smells = detectServerComponentIssues(component, '/app/page.tsx', code, DEFAULT_CONFIG);
|
|
151
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
it('should not flag client component', () => {
|
|
154
|
+
const code = `
|
|
155
|
+
"use client";
|
|
156
|
+
function ClientComponent() {
|
|
157
|
+
return <div>Client</div>;
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
160
|
+
const component = getFirstComponent(code);
|
|
161
|
+
expect(component).toBeDefined();
|
|
162
|
+
const smells = detectServerComponentIssues(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
163
|
+
// Client components should not have server component issues
|
|
164
|
+
const serverSmells = smells.filter(s => s.type === 'server-component-hooks' ||
|
|
165
|
+
s.type === 'server-component-events');
|
|
166
|
+
expect(serverSmells.length).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('detectAsyncComponentIssues', () => {
|
|
170
|
+
it('should return array for component', () => {
|
|
171
|
+
const code = `
|
|
172
|
+
function SyncComponent() {
|
|
173
|
+
return <div>Sync</div>;
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
const component = getFirstComponent(code);
|
|
177
|
+
expect(component).toBeDefined();
|
|
178
|
+
const smells = detectAsyncComponentIssues(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
179
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
// ============ ACCESSIBILITY TESTS ============
|
|
183
|
+
describe('detectAccessibilityIssues', () => {
|
|
184
|
+
const config = { ...DEFAULT_CONFIG, checkAccessibility: true };
|
|
185
|
+
it('should detect img without alt', () => {
|
|
186
|
+
const code = `
|
|
187
|
+
function ImgComponent() {
|
|
188
|
+
return <img src="photo.jpg" />;
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
const component = getFirstComponent(code);
|
|
192
|
+
const smells = detectAccessibilityIssues(component, '/test.tsx', code, config);
|
|
193
|
+
expect(smells.some(s => s.type === 'a11y-missing-alt')).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
it('should not flag img with alt', () => {
|
|
196
|
+
const code = `
|
|
197
|
+
function ImgComponent() {
|
|
198
|
+
return <img src="photo.jpg" alt="A photo" />;
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
const component = getFirstComponent(code);
|
|
202
|
+
const smells = detectAccessibilityIssues(component, '/test.tsx', code, config);
|
|
203
|
+
const altSmells = smells.filter(s => s.type === 'a11y-missing-alt' && s.severity === 'error');
|
|
204
|
+
expect(altSmells.length).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
it('should detect input without label', () => {
|
|
207
|
+
const code = `
|
|
208
|
+
function FormComponent() {
|
|
209
|
+
return <input type="text" />;
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
const component = getFirstComponent(code);
|
|
213
|
+
const smells = detectAccessibilityIssues(component, '/test.tsx', code, config);
|
|
214
|
+
expect(smells.some(s => s.type === 'a11y-missing-label')).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
it('should detect clickable div without role', () => {
|
|
217
|
+
const code = `
|
|
218
|
+
function ClickDiv() {
|
|
219
|
+
return <div onClick={() => {}}>Click me</div>;
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
const component = getFirstComponent(code);
|
|
223
|
+
const smells = detectAccessibilityIssues(component, '/test.tsx', code, config);
|
|
224
|
+
expect(smells.some(s => s.type === 'a11y-interactive-role')).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
it('should return empty when disabled', () => {
|
|
227
|
+
const code = `
|
|
228
|
+
function ImgComponent() {
|
|
229
|
+
return <img src="photo.jpg" />;
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
const component = getFirstComponent(code);
|
|
233
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkAccessibility: false };
|
|
234
|
+
const smells = detectAccessibilityIssues(component, '/test.tsx', code, disabledConfig);
|
|
235
|
+
expect(smells.length).toBe(0);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
// ============ SECURITY TESTS ============
|
|
239
|
+
describe('detectSecurityIssues', () => {
|
|
240
|
+
const config = { ...DEFAULT_CONFIG, checkSecurity: true };
|
|
241
|
+
it('should detect dangerouslySetInnerHTML', () => {
|
|
242
|
+
const code = `
|
|
243
|
+
function XSSComponent() {
|
|
244
|
+
return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
const component = getFirstComponent(code);
|
|
248
|
+
const smells = detectSecurityIssues(component, '/test.tsx', code, config);
|
|
249
|
+
expect(smells.some(s => s.type === 'security-xss')).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
it('should detect eval usage', () => {
|
|
252
|
+
const code = `
|
|
253
|
+
function EvalComponent() {
|
|
254
|
+
const result = eval('1 + 1');
|
|
255
|
+
return <div>{result}</div>;
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
const component = getFirstComponent(code);
|
|
259
|
+
const smells = detectSecurityIssues(component, '/test.tsx', code, config);
|
|
260
|
+
expect(smells.some(s => s.type === 'security-eval')).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
it('should detect new Function', () => {
|
|
263
|
+
const code = `
|
|
264
|
+
function FunctionComponent() {
|
|
265
|
+
const fn = new Function('return 1');
|
|
266
|
+
return <div>{fn()}</div>;
|
|
267
|
+
}
|
|
268
|
+
`;
|
|
269
|
+
const component = getFirstComponent(code);
|
|
270
|
+
const smells = detectSecurityIssues(component, '/test.tsx', code, config);
|
|
271
|
+
expect(smells.some(s => s.type === 'security-eval')).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
it('should detect innerHTML assignment', () => {
|
|
274
|
+
const code = `
|
|
275
|
+
function InnerHTMLComponent() {
|
|
276
|
+
const ref = useRef();
|
|
277
|
+
ref.current.innerHTML = userContent;
|
|
278
|
+
return <div ref={ref} />;
|
|
279
|
+
}
|
|
280
|
+
`;
|
|
281
|
+
const component = getFirstComponent(code);
|
|
282
|
+
const smells = detectSecurityIssues(component, '/test.tsx', code, config);
|
|
283
|
+
expect(smells.some(s => s.type === 'security-xss')).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
it('should detect javascript: URL', () => {
|
|
286
|
+
const code = `
|
|
287
|
+
function LinkComponent() {
|
|
288
|
+
return <a href="javascript:alert('xss')">Click</a>;
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
const component = getFirstComponent(code);
|
|
292
|
+
const smells = detectSecurityIssues(component, '/test.tsx', code, config);
|
|
293
|
+
expect(smells.some(s => s.type === 'security-xss')).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
it('should return empty when disabled', () => {
|
|
296
|
+
const code = `
|
|
297
|
+
function EvalComponent() {
|
|
298
|
+
const result = eval('1 + 1');
|
|
299
|
+
return <div>{result}</div>;
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
const component = getFirstComponent(code);
|
|
303
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkSecurity: false };
|
|
304
|
+
const smells = detectSecurityIssues(component, '/test.tsx', code, disabledConfig);
|
|
305
|
+
expect(smells.length).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
// ============ PROP DRILLING TESTS ============
|
|
309
|
+
describe('detectPropDrilling', () => {
|
|
310
|
+
it('should detect too many props', () => {
|
|
311
|
+
const code = `
|
|
312
|
+
function ManyPropsComponent({ a, b, c, d, e, f, g, h, i }) {
|
|
313
|
+
return <div>{a}{b}{c}{d}{e}{f}{g}{h}{i}</div>;
|
|
314
|
+
}
|
|
315
|
+
`;
|
|
316
|
+
const component = getFirstComponent(code);
|
|
317
|
+
const config = { ...DEFAULT_CONFIG, maxPropsCount: 5 };
|
|
318
|
+
const smells = detectPropDrilling(component, '/test.tsx', code, config);
|
|
319
|
+
expect(smells.some(s => s.type === 'prop-drilling')).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
it('should detect drilling pattern props', () => {
|
|
322
|
+
const code = `
|
|
323
|
+
function DrillComponent({ userData, configData, settingsData }) {
|
|
324
|
+
return <div>{userData}</div>;
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
const component = getFirstComponent(code);
|
|
328
|
+
const smells = detectPropDrilling(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
329
|
+
expect(smells.some(s => s.type === 'prop-drilling')).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
it('should not flag few props', () => {
|
|
332
|
+
const code = `
|
|
333
|
+
function FewPropsComponent({ name, age }) {
|
|
334
|
+
return <div>{name}: {age}</div>;
|
|
335
|
+
}
|
|
336
|
+
`;
|
|
337
|
+
const component = getFirstComponent(code);
|
|
338
|
+
const smells = detectPropDrilling(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
339
|
+
expect(smells.length).toBe(0);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
describe('analyzePropDrillingDepth', () => {
|
|
343
|
+
it('should detect props passed through multiple components', () => {
|
|
344
|
+
const code = `
|
|
345
|
+
function Parent({ userId }) { return <Child userId={userId} />; }
|
|
346
|
+
function Child({ userId }) { return <GrandChild userId={userId} />; }
|
|
347
|
+
function GrandChild({ userId }) { return <div>{userId}</div>; }
|
|
348
|
+
`;
|
|
349
|
+
const { components } = parseCode(code, 'test.tsx');
|
|
350
|
+
const config = { ...DEFAULT_CONFIG, maxPropDrillingDepth: 2 };
|
|
351
|
+
const smells = analyzePropDrillingDepth(components, '/test.tsx', code, config);
|
|
352
|
+
expect(smells.some(s => s.type === 'prop-drilling')).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
// ============ MEMORY LEAK TESTS ============
|
|
356
|
+
describe('detectMemoryLeaks', () => {
|
|
357
|
+
const config = { ...DEFAULT_CONFIG, checkMemoryLeaks: true };
|
|
358
|
+
it('should detect setInterval without cleanup', () => {
|
|
359
|
+
const code = `
|
|
360
|
+
function TimerComponent() {
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
setInterval(() => console.log('tick'), 1000);
|
|
363
|
+
}, []);
|
|
364
|
+
return <div>Timer</div>;
|
|
365
|
+
}
|
|
366
|
+
`;
|
|
367
|
+
const component = getFirstComponent(code);
|
|
368
|
+
const smells = detectMemoryLeaks(component, '/test.tsx', code, config);
|
|
369
|
+
expect(smells.some(s => s.type === 'memory-leak-timer')).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
it('should return empty when disabled', () => {
|
|
372
|
+
const code = `
|
|
373
|
+
function TimerComponent() {
|
|
374
|
+
setInterval(() => console.log('tick'), 1000);
|
|
375
|
+
return <div>Timer</div>;
|
|
376
|
+
}
|
|
377
|
+
`;
|
|
378
|
+
const component = getFirstComponent(code);
|
|
379
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkMemoryLeaks: false };
|
|
380
|
+
const smells = detectMemoryLeaks(component, '/test.tsx', code, disabledConfig);
|
|
381
|
+
expect(smells.length).toBe(0);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
// ============ MISSING KEY TESTS ============
|
|
385
|
+
describe('detectMissingKeys', () => {
|
|
386
|
+
const config = { ...DEFAULT_CONFIG, checkMissingKeys: true };
|
|
387
|
+
it('should detect map without key', () => {
|
|
388
|
+
const code = `
|
|
389
|
+
function ListComponent({ items }) {
|
|
390
|
+
return <ul>{items.map(item => <li>{item}</li>)}</ul>;
|
|
391
|
+
}
|
|
392
|
+
`;
|
|
393
|
+
const component = getFirstComponent(code);
|
|
394
|
+
const smells = detectMissingKeys(component, '/test.tsx', code, config);
|
|
395
|
+
expect(smells.some(s => s.type === 'missing-key')).toBe(true);
|
|
396
|
+
});
|
|
397
|
+
it('should not flag map with key', () => {
|
|
398
|
+
const code = `
|
|
399
|
+
function ListComponent({ items }) {
|
|
400
|
+
return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
|
|
401
|
+
}
|
|
402
|
+
`;
|
|
403
|
+
const component = getFirstComponent(code);
|
|
404
|
+
const smells = detectMissingKeys(component, '/test.tsx', code, config);
|
|
405
|
+
expect(smells.filter(s => s.type === 'missing-key').length).toBe(0);
|
|
406
|
+
});
|
|
407
|
+
it('should return empty when disabled', () => {
|
|
408
|
+
const code = `
|
|
409
|
+
function ListComponent({ items }) {
|
|
410
|
+
return <ul>{items.map(item => <li>{item}</li>)}</ul>;
|
|
411
|
+
}
|
|
412
|
+
`;
|
|
413
|
+
const component = getFirstComponent(code);
|
|
414
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkMissingKeys: false };
|
|
415
|
+
const smells = detectMissingKeys(component, '/test.tsx', code, disabledConfig);
|
|
416
|
+
expect(smells.length).toBe(0);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
// ============ NESTED TERNARY TESTS ============
|
|
420
|
+
describe('detectNestedTernaries', () => {
|
|
421
|
+
it('should detect deeply nested ternaries', () => {
|
|
422
|
+
const code = `
|
|
423
|
+
function TernaryComponent({ a, b, c }) {
|
|
424
|
+
return <div>{a ? 'A' : b ? 'B' : c ? 'C' : 'D'}</div>;
|
|
425
|
+
}
|
|
426
|
+
`;
|
|
427
|
+
const component = getFirstComponent(code);
|
|
428
|
+
const config = { ...DEFAULT_CONFIG, maxTernaryDepth: 1 };
|
|
429
|
+
const smells = detectNestedTernaries(component, '/test.tsx', code, config);
|
|
430
|
+
expect(smells.some(s => s.type === 'nested-ternary')).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
it('should not flag simple ternary', () => {
|
|
433
|
+
const code = `
|
|
434
|
+
function SimpleTernaryComponent({ show }) {
|
|
435
|
+
return <div>{show ? 'Yes' : 'No'}</div>;
|
|
436
|
+
}
|
|
437
|
+
`;
|
|
438
|
+
const component = getFirstComponent(code);
|
|
439
|
+
const config = { ...DEFAULT_CONFIG, maxTernaryDepth: 2 };
|
|
440
|
+
const smells = detectNestedTernaries(component, '/test.tsx', code, config);
|
|
441
|
+
expect(smells.length).toBe(0);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
// ============ LARGE COMPONENT TESTS ============
|
|
445
|
+
describe('detectLargeComponent', () => {
|
|
446
|
+
it('should detect large component', () => {
|
|
447
|
+
// Generate a component with many lines using unique variable names
|
|
448
|
+
const lines = Array(150).fill(null).map((_, i) => `const x${i} = ${i};`).join('\n');
|
|
449
|
+
const code = `
|
|
450
|
+
function LargeComponent() {
|
|
451
|
+
${lines}
|
|
452
|
+
return <div>Large</div>;
|
|
453
|
+
}
|
|
454
|
+
`;
|
|
455
|
+
const component = getFirstComponent(code);
|
|
456
|
+
const config = { ...DEFAULT_CONFIG, maxComponentLines: 100 };
|
|
457
|
+
const smells = detectLargeComponent(component, '/test.tsx', code, config);
|
|
458
|
+
expect(smells.some(s => s.type === 'large-component')).toBe(true);
|
|
459
|
+
});
|
|
460
|
+
it('should not flag small component', () => {
|
|
461
|
+
const code = `
|
|
462
|
+
function SmallComponent() {
|
|
463
|
+
return <div>Small</div>;
|
|
464
|
+
}
|
|
465
|
+
`;
|
|
466
|
+
const component = getFirstComponent(code);
|
|
467
|
+
const smells = detectLargeComponent(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
468
|
+
expect(smells.length).toBe(0);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
// ============ MAGIC VALUES TESTS ============
|
|
472
|
+
describe('detectMagicValues', () => {
|
|
473
|
+
const config = { ...DEFAULT_CONFIG, checkMagicValues: true, magicNumberThreshold: 10 };
|
|
474
|
+
it('should detect magic numbers', () => {
|
|
475
|
+
const code = `
|
|
476
|
+
function MagicComponent() {
|
|
477
|
+
const width = 800;
|
|
478
|
+
const height = 600;
|
|
479
|
+
return <div>{width + height}</div>;
|
|
480
|
+
}
|
|
481
|
+
`;
|
|
482
|
+
const component = getFirstComponent(code);
|
|
483
|
+
const smells = detectMagicValues(component, '/test.tsx', code, config);
|
|
484
|
+
expect(smells.some(s => s.type === 'magic-value')).toBe(true);
|
|
485
|
+
});
|
|
486
|
+
it('should not flag common numbers', () => {
|
|
487
|
+
const code = `
|
|
488
|
+
function CommonComponent() {
|
|
489
|
+
const x = 0;
|
|
490
|
+
const y = 1;
|
|
491
|
+
const z = -1;
|
|
492
|
+
return <div>{x}{y}{z}</div>;
|
|
493
|
+
}
|
|
494
|
+
`;
|
|
495
|
+
const component = getFirstComponent(code);
|
|
496
|
+
const smells = detectMagicValues(component, '/test.tsx', code, config);
|
|
497
|
+
expect(smells.length).toBe(0);
|
|
498
|
+
});
|
|
499
|
+
it('should return empty when disabled', () => {
|
|
500
|
+
const code = `
|
|
501
|
+
function MagicComponent() {
|
|
502
|
+
const x = 42;
|
|
503
|
+
return <div>{x}</div>;
|
|
504
|
+
}
|
|
505
|
+
`;
|
|
506
|
+
const component = getFirstComponent(code);
|
|
507
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkMagicValues: false };
|
|
508
|
+
const smells = detectMagicValues(component, '/test.tsx', code, disabledConfig);
|
|
509
|
+
expect(smells.length).toBe(0);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
// ============ DEAD CODE TESTS ============
|
|
513
|
+
describe('detectDeadCode', () => {
|
|
514
|
+
const config = { ...DEFAULT_CONFIG, checkDeadCode: true };
|
|
515
|
+
it('should detect unused variables', () => {
|
|
516
|
+
const code = `
|
|
517
|
+
function DeadCodeComponent() {
|
|
518
|
+
const unusedVar = 42;
|
|
519
|
+
const usedVar = 10;
|
|
520
|
+
return <div>{usedVar}</div>;
|
|
521
|
+
}
|
|
522
|
+
`;
|
|
523
|
+
const component = getFirstComponent(code);
|
|
524
|
+
const smells = detectDeadCode(component, '/test.tsx', code, config);
|
|
525
|
+
expect(smells.some(s => s.type === 'dead-code' && s.message.includes('unusedVar'))).toBe(true);
|
|
526
|
+
});
|
|
527
|
+
it('should return empty when disabled', () => {
|
|
528
|
+
const code = `
|
|
529
|
+
function DeadCodeComponent() {
|
|
530
|
+
const unused = 42;
|
|
531
|
+
return <div>Done</div>;
|
|
532
|
+
}
|
|
533
|
+
`;
|
|
534
|
+
const component = getFirstComponent(code);
|
|
535
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkDeadCode: false };
|
|
536
|
+
const smells = detectDeadCode(component, '/test.tsx', code, disabledConfig);
|
|
537
|
+
expect(smells.length).toBe(0);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
// ============ DEPENDENCY ARRAY TESTS ============
|
|
541
|
+
describe('detectDependencyArrayIssues', () => {
|
|
542
|
+
const config = { ...DEFAULT_CONFIG, checkDependencyArrays: true };
|
|
543
|
+
it('should detect empty dependency array with external deps', () => {
|
|
544
|
+
const code = `
|
|
545
|
+
function DepComponent({ value }) {
|
|
546
|
+
useEffect(() => {
|
|
547
|
+
console.log(value);
|
|
548
|
+
}, []);
|
|
549
|
+
return <div>{value}</div>;
|
|
550
|
+
}
|
|
551
|
+
`;
|
|
552
|
+
const component = getFirstComponent(code);
|
|
553
|
+
const smells = detectDependencyArrayIssues(component, '/test.tsx', code, config);
|
|
554
|
+
// Should detect missing dependency
|
|
555
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
556
|
+
});
|
|
557
|
+
it('should return empty when disabled', () => {
|
|
558
|
+
const code = `
|
|
559
|
+
function DepComponent({ value }) {
|
|
560
|
+
useEffect(() => {
|
|
561
|
+
console.log(value);
|
|
562
|
+
}, []);
|
|
563
|
+
return <div>{value}</div>;
|
|
564
|
+
}
|
|
565
|
+
`;
|
|
566
|
+
const component = getFirstComponent(code);
|
|
567
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkDependencyArrays: false };
|
|
568
|
+
const smells = detectDependencyArrayIssues(component, '/test.tsx', code, disabledConfig);
|
|
569
|
+
expect(smells.length).toBe(0);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
// ============ IMPORT ISSUES TESTS ============
|
|
573
|
+
describe('detectImportIssues', () => {
|
|
574
|
+
it('should analyze imports', () => {
|
|
575
|
+
const code = `
|
|
576
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
577
|
+
import * as lodash from 'lodash';
|
|
578
|
+
|
|
579
|
+
function TestComponent() {
|
|
580
|
+
const [x, setX] = useState(0);
|
|
581
|
+
return <div>{x}</div>;
|
|
582
|
+
}
|
|
583
|
+
`;
|
|
584
|
+
const { components } = parseCode(code, 'test.tsx');
|
|
585
|
+
const component = components[0];
|
|
586
|
+
const smells = detectImportIssues(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
587
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
588
|
+
});
|
|
589
|
+
it('should detect namespace imports', () => {
|
|
590
|
+
const code = `
|
|
591
|
+
import * as everything from 'some-lib';
|
|
592
|
+
|
|
593
|
+
function TestComponent() {
|
|
594
|
+
return <div>{everything.value}</div>;
|
|
595
|
+
}
|
|
596
|
+
`;
|
|
597
|
+
const { components } = parseCode(code, 'test.tsx');
|
|
598
|
+
const component = components[0];
|
|
599
|
+
const config = { ...DEFAULT_CONFIG, checkImports: true };
|
|
600
|
+
const smells = detectImportIssues(component, '/test.tsx', code, config);
|
|
601
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
describe('analyzeImports', () => {
|
|
605
|
+
it('should be an async function', () => {
|
|
606
|
+
// analyzeImports is async and takes (files[], rootDir, config)
|
|
607
|
+
expect(typeof analyzeImports).toBe('function');
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
// ============ UNUSED CODE TESTS ============
|
|
611
|
+
describe('detectUnusedCode', () => {
|
|
612
|
+
const config = { ...DEFAULT_CONFIG, checkUnusedCode: true };
|
|
613
|
+
it('should return array', () => {
|
|
614
|
+
const code = `
|
|
615
|
+
function TestComponent() {
|
|
616
|
+
const unused = 42;
|
|
617
|
+
return <div>Test</div>;
|
|
618
|
+
}
|
|
619
|
+
`;
|
|
620
|
+
const component = getFirstComponent(code);
|
|
621
|
+
const smells = detectUnusedCode(component, '/test.tsx', code, config);
|
|
622
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
623
|
+
});
|
|
624
|
+
it('should return empty when disabled', () => {
|
|
625
|
+
const code = `
|
|
626
|
+
function TestComponent() {
|
|
627
|
+
const unused = 42;
|
|
628
|
+
return <div>Test</div>;
|
|
629
|
+
}
|
|
630
|
+
`;
|
|
631
|
+
const component = getFirstComponent(code);
|
|
632
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkUnusedCode: false };
|
|
633
|
+
const smells = detectUnusedCode(component, '/test.tsx', code, disabledConfig);
|
|
634
|
+
expect(smells.length).toBe(0);
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
// ============ MORE COMPLEXITY TESTS ============
|
|
638
|
+
describe('detectComplexity (extended)', () => {
|
|
639
|
+
it('should detect high cyclomatic complexity', () => {
|
|
640
|
+
const code = `
|
|
641
|
+
function ComplexComponent({ a, b, c, d, e }) {
|
|
642
|
+
if (a) { return <div>A</div>; }
|
|
643
|
+
if (b) { return <div>B</div>; }
|
|
644
|
+
if (c) { return <div>C</div>; }
|
|
645
|
+
if (d) { return <div>D</div>; }
|
|
646
|
+
if (e) { return <div>E</div>; }
|
|
647
|
+
switch(a) {
|
|
648
|
+
case 1: return <div>1</div>;
|
|
649
|
+
case 2: return <div>2</div>;
|
|
650
|
+
case 3: return <div>3</div>;
|
|
651
|
+
case 4: return <div>4</div>;
|
|
652
|
+
default: return <div>Default</div>;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
`;
|
|
656
|
+
const component = getFirstComponent(code);
|
|
657
|
+
const config = { ...DEFAULT_CONFIG, maxComplexity: 5 };
|
|
658
|
+
const smells = detectComplexity(component, '/test.tsx', code, config);
|
|
659
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
// ============ MORE USEEFFECT TESTS ============
|
|
663
|
+
describe('detectUseEffectOveruse (extended)', () => {
|
|
664
|
+
it('should detect multiple useEffects', () => {
|
|
665
|
+
const code = `
|
|
666
|
+
function EffectsComponent() {
|
|
667
|
+
useEffect(() => {}, []);
|
|
668
|
+
useEffect(() => {}, []);
|
|
669
|
+
useEffect(() => {}, []);
|
|
670
|
+
useEffect(() => {}, []);
|
|
671
|
+
useEffect(() => {}, []);
|
|
672
|
+
return <div>Effects</div>;
|
|
673
|
+
}
|
|
674
|
+
`;
|
|
675
|
+
const component = getFirstComponent(code);
|
|
676
|
+
const smells = detectUseEffectOveruse(component, '/test.tsx', code);
|
|
677
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
// ============ MORE HOOKS RULES TESTS ============
|
|
681
|
+
describe('detectHooksRulesViolations (extended)', () => {
|
|
682
|
+
const config = { ...DEFAULT_CONFIG, checkHooksRules: true };
|
|
683
|
+
it('should detect hooks in conditional', () => {
|
|
684
|
+
const code = `
|
|
685
|
+
function ConditionalHook({ show }) {
|
|
686
|
+
if (show) {
|
|
687
|
+
const [x, setX] = useState(0);
|
|
688
|
+
}
|
|
689
|
+
return <div>Test</div>;
|
|
690
|
+
}
|
|
691
|
+
`;
|
|
692
|
+
const component = getFirstComponent(code);
|
|
693
|
+
const smells = detectHooksRulesViolations(component, '/test.tsx', code, config);
|
|
694
|
+
expect(smells.some(s => s.type === 'hooks-rules-violation')).toBe(true);
|
|
695
|
+
});
|
|
696
|
+
it('should detect hooks in loop', () => {
|
|
697
|
+
const code = `
|
|
698
|
+
function LoopHook({ items }) {
|
|
699
|
+
for (const item of items) {
|
|
700
|
+
const [x, setX] = useState(0);
|
|
701
|
+
}
|
|
702
|
+
return <div>Test</div>;
|
|
703
|
+
}
|
|
704
|
+
`;
|
|
705
|
+
const component = getFirstComponent(code);
|
|
706
|
+
const smells = detectHooksRulesViolations(component, '/test.tsx', code, config);
|
|
707
|
+
expect(smells.some(s => s.type === 'hooks-rules-violation')).toBe(true);
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
// ============ MORE MEMOIZATION TESTS ============
|
|
711
|
+
describe('detectUnmemoizedCalculations (extended)', () => {
|
|
712
|
+
it('should detect expensive calculations', () => {
|
|
713
|
+
const code = `
|
|
714
|
+
function ExpensiveComponent({ items }) {
|
|
715
|
+
const sorted = items.sort((a, b) => a - b);
|
|
716
|
+
const filtered = items.filter(x => x > 5);
|
|
717
|
+
const mapped = items.map(x => x * 2);
|
|
718
|
+
return <div>{sorted.length}</div>;
|
|
719
|
+
}
|
|
720
|
+
`;
|
|
721
|
+
const component = getFirstComponent(code);
|
|
722
|
+
const smells = detectUnmemoizedCalculations(component, '/test.tsx', code);
|
|
723
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
// ============ NEXTJS TESTS ============
|
|
727
|
+
describe('detectNextjsIssues', () => {
|
|
728
|
+
const config = { ...DEFAULT_CONFIG, checkNextjs: true };
|
|
729
|
+
it('should detect native img usage', () => {
|
|
730
|
+
const code = `
|
|
731
|
+
function PageComponent() {
|
|
732
|
+
return <img src="/photo.jpg" alt="Photo" />;
|
|
733
|
+
}
|
|
734
|
+
`;
|
|
735
|
+
const component = getFirstComponent(code);
|
|
736
|
+
const smells = detectNextjsIssues(component, '/app/page.tsx', code, config, ['next/image']);
|
|
737
|
+
expect(smells.some(s => s.type === 'nextjs-image-unoptimized')).toBe(true);
|
|
738
|
+
});
|
|
739
|
+
it('should detect client hooks without use client', () => {
|
|
740
|
+
const code = `
|
|
741
|
+
function PageComponent() {
|
|
742
|
+
const [count, setCount] = useState(0);
|
|
743
|
+
return <div>{count}</div>;
|
|
744
|
+
}
|
|
745
|
+
`;
|
|
746
|
+
const component = getFirstComponent(code);
|
|
747
|
+
const smells = detectNextjsIssues(component, '/app/page.tsx', code, config, []);
|
|
748
|
+
expect(smells.some(s => s.type === 'nextjs-client-server-boundary')).toBe(true);
|
|
749
|
+
});
|
|
750
|
+
it('should return empty when disabled', () => {
|
|
751
|
+
const code = `
|
|
752
|
+
function PageComponent() {
|
|
753
|
+
return <img src="/photo.jpg" alt="Photo" />;
|
|
754
|
+
}
|
|
755
|
+
`;
|
|
756
|
+
const component = getFirstComponent(code);
|
|
757
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkNextjs: false };
|
|
758
|
+
const smells = detectNextjsIssues(component, '/app/page.tsx', code, disabledConfig, []);
|
|
759
|
+
expect(smells.length).toBe(0);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
// ============ TYPESCRIPT TESTS ============
|
|
763
|
+
describe('detectTypescriptIssues', () => {
|
|
764
|
+
const config = { ...DEFAULT_CONFIG, checkTypescript: true };
|
|
765
|
+
it('should detect any type usage', () => {
|
|
766
|
+
const code = `
|
|
767
|
+
function TypedComponent(props: any) {
|
|
768
|
+
return <div>{props.value}</div>;
|
|
769
|
+
}
|
|
770
|
+
`;
|
|
771
|
+
const component = getFirstComponent(code);
|
|
772
|
+
const smells = detectTypescriptIssues(component, '/test.tsx', code, config);
|
|
773
|
+
expect(smells.some(s => s.type === 'ts-any-usage')).toBe(true);
|
|
774
|
+
});
|
|
775
|
+
it('should not run on non-TypeScript files', () => {
|
|
776
|
+
const code = `
|
|
777
|
+
function Component(props) {
|
|
778
|
+
return <div>{props.value}</div>;
|
|
779
|
+
}
|
|
780
|
+
`;
|
|
781
|
+
const component = getFirstComponent(code);
|
|
782
|
+
const smells = detectTypescriptIssues(component, '/test.jsx', code, config);
|
|
783
|
+
expect(smells.length).toBe(0);
|
|
784
|
+
});
|
|
785
|
+
it('should return empty when disabled', () => {
|
|
786
|
+
const code = `
|
|
787
|
+
function TypedComponent(props: any) {
|
|
788
|
+
return <div>{props.value}</div>;
|
|
789
|
+
}
|
|
790
|
+
`;
|
|
791
|
+
const component = getFirstComponent(code);
|
|
792
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkTypescript: false };
|
|
793
|
+
const smells = detectTypescriptIssues(component, '/test.tsx', code, disabledConfig);
|
|
794
|
+
expect(smells.length).toBe(0);
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
// ============ NODEJS TESTS ============
|
|
798
|
+
describe('detectNodejsIssues', () => {
|
|
799
|
+
const config = { ...DEFAULT_CONFIG, checkNodejs: true };
|
|
800
|
+
it('should detect sync fs operations', () => {
|
|
801
|
+
const code = `
|
|
802
|
+
function ServerComponent() {
|
|
803
|
+
const data = fs.readFileSync('file.txt');
|
|
804
|
+
return <div>{data}</div>;
|
|
805
|
+
}
|
|
806
|
+
`;
|
|
807
|
+
const component = getFirstComponent(code);
|
|
808
|
+
const smells = detectNodejsIssues(component, '/api/route.ts', code, config, ['fs']);
|
|
809
|
+
expect(smells.some(s => s.type === 'nodejs-sync-io')).toBe(true);
|
|
810
|
+
});
|
|
811
|
+
it('should return empty when disabled', () => {
|
|
812
|
+
const code = `
|
|
813
|
+
function ServerComponent() {
|
|
814
|
+
const data = fs.readFileSync('file.txt');
|
|
815
|
+
return <div>{data}</div>;
|
|
816
|
+
}
|
|
817
|
+
`;
|
|
818
|
+
const component = getFirstComponent(code);
|
|
819
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkNodejs: false };
|
|
820
|
+
const smells = detectNodejsIssues(component, '/api/route.ts', code, disabledConfig, ['fs']);
|
|
821
|
+
expect(smells.length).toBe(0);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
// ============ REACT NATIVE TESTS ============
|
|
825
|
+
describe('detectReactNativeIssues', () => {
|
|
826
|
+
const config = { ...DEFAULT_CONFIG, checkReactNative: true };
|
|
827
|
+
it('should detect inline styles', () => {
|
|
828
|
+
const code = `
|
|
829
|
+
function NativeComponent() {
|
|
830
|
+
return <View style={{ backgroundColor: 'red', padding: 10 }}>Hello</View>;
|
|
831
|
+
}
|
|
832
|
+
`;
|
|
833
|
+
const component = getFirstComponent(code);
|
|
834
|
+
const smells = detectReactNativeIssues(component, '/App.tsx', code, config, ['react-native']);
|
|
835
|
+
expect(smells.some(s => s.type === 'rn-inline-style')).toBe(true);
|
|
836
|
+
});
|
|
837
|
+
it('should return empty when disabled', () => {
|
|
838
|
+
const code = `
|
|
839
|
+
function NativeComponent() {
|
|
840
|
+
return <View style={{ backgroundColor: 'red' }}>Hello</View>;
|
|
841
|
+
}
|
|
842
|
+
`;
|
|
843
|
+
const component = getFirstComponent(code);
|
|
844
|
+
const disabledConfig = { ...DEFAULT_CONFIG, checkReactNative: false };
|
|
845
|
+
const smells = detectReactNativeIssues(component, '/App.tsx', code, disabledConfig, ['react-native']);
|
|
846
|
+
expect(smells.length).toBe(0);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
// ============ ADDITIONAL EDGE CASE TESTS ============
|
|
850
|
+
describe('Additional import/memoization/complexity tests', () => {
|
|
851
|
+
it('detectImportIssues should detect barrel imports', () => {
|
|
852
|
+
const code = `
|
|
853
|
+
import { Button, Card, Modal } from './components/index';
|
|
854
|
+
function ImportComponent() {
|
|
855
|
+
return <Button />;
|
|
856
|
+
}
|
|
857
|
+
`;
|
|
858
|
+
const component = getFirstComponent(code);
|
|
859
|
+
const smells = detectImportIssues(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
860
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
861
|
+
});
|
|
862
|
+
it('detectImportIssues should detect namespace imports', () => {
|
|
863
|
+
const code = `
|
|
864
|
+
import * as lodash from 'lodash';
|
|
865
|
+
function LodashComponent() {
|
|
866
|
+
return <div>{lodash.get({}, 'a')}</div>;
|
|
867
|
+
}
|
|
868
|
+
`;
|
|
869
|
+
const component = getFirstComponent(code);
|
|
870
|
+
const smells = detectImportIssues(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
871
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
872
|
+
});
|
|
873
|
+
it('detectUnmemoizedCalculations should detect expensive map operations', () => {
|
|
874
|
+
const code = `
|
|
875
|
+
function MapComponent({ items }) {
|
|
876
|
+
const mapped = items.map(i => i * 2).filter(x => x > 5).reduce((a, b) => a + b, 0);
|
|
877
|
+
return <div>{mapped}</div>;
|
|
878
|
+
}
|
|
879
|
+
`;
|
|
880
|
+
const component = getFirstComponent(code);
|
|
881
|
+
const smells = detectUnmemoizedCalculations(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
882
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
883
|
+
});
|
|
884
|
+
it('detectComplexity should detect complex conditionals', () => {
|
|
885
|
+
const code = `
|
|
886
|
+
function ComplexComponent({ a, b, c, d }) {
|
|
887
|
+
if (a && b || c && !d) {
|
|
888
|
+
if (a > b && c < d) {
|
|
889
|
+
return <div>Complex</div>;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return <div>Simple</div>;
|
|
893
|
+
}
|
|
894
|
+
`;
|
|
895
|
+
const component = getFirstComponent(code);
|
|
896
|
+
const smells = detectComplexity(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
897
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
898
|
+
});
|
|
899
|
+
it('detectDeadCode should detect unreachable code after return', () => {
|
|
900
|
+
const code = `
|
|
901
|
+
function UnreachableComponent() {
|
|
902
|
+
return <div>Done</div>;
|
|
903
|
+
const unreachable = 1;
|
|
904
|
+
}
|
|
905
|
+
`;
|
|
906
|
+
const component = getFirstComponent(code);
|
|
907
|
+
const smells = detectDeadCode(component, '/test.tsx', code);
|
|
908
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
909
|
+
});
|
|
910
|
+
it('detectDependencyArrayIssues should detect missing dependency', () => {
|
|
911
|
+
const code = `
|
|
912
|
+
function DepComponent({ count }) {
|
|
913
|
+
useEffect(() => {
|
|
914
|
+
console.log(count);
|
|
915
|
+
}, []);
|
|
916
|
+
return <div>{count}</div>;
|
|
917
|
+
}
|
|
918
|
+
`;
|
|
919
|
+
const component = getFirstComponent(code);
|
|
920
|
+
const smells = detectDependencyArrayIssues(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
921
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
922
|
+
});
|
|
923
|
+
it('detectUnusedCode should detect unused imports', () => {
|
|
924
|
+
const code = `
|
|
925
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
926
|
+
function SimpleComponent() {
|
|
927
|
+
const [val] = useState(0);
|
|
928
|
+
return <div>{val}</div>;
|
|
929
|
+
}
|
|
930
|
+
`;
|
|
931
|
+
const component = getFirstComponent(code);
|
|
932
|
+
const smells = detectUnusedCode(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
933
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
934
|
+
});
|
|
935
|
+
it('detectHooksRulesViolations should detect hook in condition', () => {
|
|
936
|
+
const code = `
|
|
937
|
+
function ConditionalHookComponent({ show }) {
|
|
938
|
+
if (show) {
|
|
939
|
+
const [val] = useState(0);
|
|
940
|
+
}
|
|
941
|
+
return <div>Conditional</div>;
|
|
942
|
+
}
|
|
943
|
+
`;
|
|
944
|
+
const component = getFirstComponent(code);
|
|
945
|
+
const smells = detectHooksRulesViolations(component, '/test.tsx', code);
|
|
946
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
947
|
+
});
|
|
948
|
+
it('detectUseEffectOveruse should detect many effects', () => {
|
|
949
|
+
const code = `
|
|
950
|
+
function ManyEffectsComponent() {
|
|
951
|
+
useEffect(() => {}, []);
|
|
952
|
+
useEffect(() => {}, []);
|
|
953
|
+
useEffect(() => {}, []);
|
|
954
|
+
useEffect(() => {}, []);
|
|
955
|
+
useEffect(() => {}, []);
|
|
956
|
+
return <div>Effects</div>;
|
|
957
|
+
}
|
|
958
|
+
`;
|
|
959
|
+
const component = getFirstComponent(code);
|
|
960
|
+
const smells = detectUseEffectOveruse(component, '/test.tsx', code);
|
|
961
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
962
|
+
});
|
|
963
|
+
it('detectMagicValues should detect magic strings', () => {
|
|
964
|
+
const code = `
|
|
965
|
+
function MagicStringComponent() {
|
|
966
|
+
const status = 'PROCESSING_STATE_ACTIVE';
|
|
967
|
+
return <div className="super-long-class-name-here">{status}</div>;
|
|
968
|
+
}
|
|
969
|
+
`;
|
|
970
|
+
const component = getFirstComponent(code);
|
|
971
|
+
const smells = detectMagicValues(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
972
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
973
|
+
});
|
|
974
|
+
it('detectMemoryLeaks should detect setInterval without cleanup', () => {
|
|
975
|
+
const code = `
|
|
976
|
+
function IntervalComponent() {
|
|
977
|
+
useEffect(() => {
|
|
978
|
+
setInterval(() => console.log('tick'), 1000);
|
|
979
|
+
}, []);
|
|
980
|
+
return <div>Interval</div>;
|
|
981
|
+
}
|
|
982
|
+
`;
|
|
983
|
+
const component = getFirstComponent(code);
|
|
984
|
+
const config = { ...DEFAULT_CONFIG, checkMemoryLeaks: true };
|
|
985
|
+
const smells = detectMemoryLeaks(component, '/test.tsx', code, config);
|
|
986
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
987
|
+
expect(smells.some(s => s.type === 'memory-leak-timer')).toBe(true);
|
|
988
|
+
});
|
|
989
|
+
it('detectSecurityIssues should detect eval usage', () => {
|
|
990
|
+
const code = `
|
|
991
|
+
function EvalComponent({ code }) {
|
|
992
|
+
const result = eval(code);
|
|
993
|
+
return <div>{result}</div>;
|
|
994
|
+
}
|
|
995
|
+
`;
|
|
996
|
+
const component = getFirstComponent(code);
|
|
997
|
+
const config = { ...DEFAULT_CONFIG, checkSecurity: true };
|
|
998
|
+
const smells = detectSecurityIssues(component, '/test.tsx', code, config);
|
|
999
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
1000
|
+
});
|
|
1001
|
+
it('detectAccessibilityIssues should detect missing button text', () => {
|
|
1002
|
+
const code = `
|
|
1003
|
+
function ButtonComponent() {
|
|
1004
|
+
return <button aria-label=""></button>;
|
|
1005
|
+
}
|
|
1006
|
+
`;
|
|
1007
|
+
const component = getFirstComponent(code);
|
|
1008
|
+
const smells = detectAccessibilityIssues(component, '/test.tsx', code, DEFAULT_CONFIG);
|
|
1009
|
+
expect(Array.isArray(smells)).toBe(true);
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
});
|