react-code-smell-detector 1.5.0 → 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 +20 -0
- 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__/performanceBudget.test.js +195 -44
- 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 +23 -0
- package/dist/cli.js +17 -0
- 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 +5 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +10 -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/guide.d.ts +9 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +922 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/types/index.d.ts +11 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,26 @@ npm install -D react-code-smell-detector
|
|
|
81
81
|
react-smell /path/to/react/project
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### Interactive Guide
|
|
85
|
+
|
|
86
|
+
Launch an interactive tutorial to learn all features:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
react-smell guide
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Demo Project
|
|
93
|
+
|
|
94
|
+
Create a demo project with examples of all detectable code smells:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
react-smell demo
|
|
98
|
+
# Creates ./react-smell-demo with sample components
|
|
99
|
+
|
|
100
|
+
# Or specify a directory
|
|
101
|
+
react-smell demo /path/to/directory
|
|
102
|
+
```
|
|
103
|
+
|
|
84
104
|
### With Code Snippets
|
|
85
105
|
|
|
86
106
|
```bash
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiRefactoring.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/aiRefactoring.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getQuickRefactoringTemplates } from '../aiRefactoring.js';
|
|
3
|
+
describe('AI Refactoring', () => {
|
|
4
|
+
describe('getQuickRefactoringTemplates', () => {
|
|
5
|
+
it('should return templates for useEffect-overuse', () => {
|
|
6
|
+
const templates = getQuickRefactoringTemplates('useEffect-overuse');
|
|
7
|
+
expect(templates.length).toBeGreaterThan(0);
|
|
8
|
+
expect(templates.some(t => t.includes('effect'))).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
it('should return templates for prop-drilling', () => {
|
|
11
|
+
const templates = getQuickRefactoringTemplates('prop-drilling');
|
|
12
|
+
expect(templates.length).toBeGreaterThan(0);
|
|
13
|
+
expect(templates.some(t => t.includes('Context') || t.includes('composition'))).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('should return templates for large-component', () => {
|
|
16
|
+
const templates = getQuickRefactoringTemplates('large-component');
|
|
17
|
+
expect(templates.length).toBeGreaterThan(0);
|
|
18
|
+
expect(templates.some(t => t.includes('Extract') || t.includes('component'))).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it('should return templates for context-overuse', () => {
|
|
21
|
+
const templates = getQuickRefactoringTemplates('context-overuse');
|
|
22
|
+
expect(templates.length).toBeGreaterThan(0);
|
|
23
|
+
expect(templates.some(t => t.includes('context'))).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('should return templates for missing-error-boundary', () => {
|
|
26
|
+
const templates = getQuickRefactoringTemplates('missing-error-boundary');
|
|
27
|
+
expect(templates.length).toBeGreaterThan(0);
|
|
28
|
+
expect(templates.some(t => t.includes('ErrorBoundary') || t.includes('Suspense'))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('should return templates for state-sync-anti-pattern', () => {
|
|
31
|
+
const templates = getQuickRefactoringTemplates('state-sync-anti-pattern');
|
|
32
|
+
expect(templates.length).toBeGreaterThan(0);
|
|
33
|
+
expect(templates.some(t => t.includes('Derive') || t.includes('useMemo'))).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('should return generic template for unknown smell types', () => {
|
|
36
|
+
const templates = getQuickRefactoringTemplates('unknown-smell-type');
|
|
37
|
+
expect(templates.length).toBeGreaterThan(0);
|
|
38
|
+
expect(templates[0]).toContain('Review');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('AIRefactoringConfig', () => {
|
|
42
|
+
it('should accept valid config object', () => {
|
|
43
|
+
const config = {
|
|
44
|
+
apiKey: 'test-key',
|
|
45
|
+
model: 'gpt-4',
|
|
46
|
+
maxTokens: 1000,
|
|
47
|
+
temperature: 0.3,
|
|
48
|
+
provider: 'openai',
|
|
49
|
+
};
|
|
50
|
+
expect(config.apiKey).toBe('test-key');
|
|
51
|
+
expect(config.model).toBe('gpt-4');
|
|
52
|
+
expect(config.provider).toBe('openai');
|
|
53
|
+
});
|
|
54
|
+
it('should accept anthropic provider', () => {
|
|
55
|
+
const config = {
|
|
56
|
+
apiKey: 'test-key',
|
|
57
|
+
model: 'claude-3-sonnet',
|
|
58
|
+
provider: 'anthropic',
|
|
59
|
+
};
|
|
60
|
+
expect(config.provider).toBe('anthropic');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('AIRefactoringSuggestion type', () => {
|
|
64
|
+
it('should have expected structure', () => {
|
|
65
|
+
const suggestion = {
|
|
66
|
+
smell: {
|
|
67
|
+
type: 'debug-statement',
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
message: 'Test',
|
|
70
|
+
file: '/test.tsx',
|
|
71
|
+
line: 1,
|
|
72
|
+
column: 0,
|
|
73
|
+
suggestion: 'Remove it',
|
|
74
|
+
},
|
|
75
|
+
originalCode: 'console.log("test")',
|
|
76
|
+
suggestedCode: '// removed',
|
|
77
|
+
explanation: 'Debug statements should be removed',
|
|
78
|
+
confidence: 0.9,
|
|
79
|
+
estimatedEffort: 'low',
|
|
80
|
+
};
|
|
81
|
+
expect(suggestion.confidence).toBeGreaterThanOrEqual(0);
|
|
82
|
+
expect(suggestion.confidence).toBeLessThanOrEqual(1);
|
|
83
|
+
expect(['low', 'medium', 'high']).toContain(suggestion.estimatedEffort);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer-real.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/analyzer-real.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { analyzeProject } from '../analyzer.js';
|
|
3
|
+
import * as fg from 'fast-glob';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
// Mock fast-glob
|
|
6
|
+
vi.mock('fast-glob', () => ({
|
|
7
|
+
default: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
// Mock fs/promises for parseFile
|
|
10
|
+
vi.mock('fs/promises', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
default: actual,
|
|
15
|
+
readFile: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
describe('Analyzer - Real Tests', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe('analyzeProject', () => {
|
|
23
|
+
it('should return empty results when no files found', async () => {
|
|
24
|
+
fg.default.mockResolvedValue([]);
|
|
25
|
+
const result = await analyzeProject({
|
|
26
|
+
rootDir: '/fake/path',
|
|
27
|
+
});
|
|
28
|
+
expect(result.files).toEqual([]);
|
|
29
|
+
expect(result.summary.totalFiles).toBe(0);
|
|
30
|
+
expect(result.summary.totalComponents).toBe(0);
|
|
31
|
+
expect(result.summary.totalSmells).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
it('should call fast-glob with correct patterns', async () => {
|
|
34
|
+
fg.default.mockResolvedValue([]);
|
|
35
|
+
await analyzeProject({
|
|
36
|
+
rootDir: '/fake/path',
|
|
37
|
+
include: ['**/*.tsx'],
|
|
38
|
+
});
|
|
39
|
+
expect(fg.default).toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
it('should handle parse errors gracefully', async () => {
|
|
42
|
+
fg.default.mockResolvedValue(['/fake/path/broken.tsx']);
|
|
43
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
44
|
+
const result = await analyzeProject({
|
|
45
|
+
rootDir: '/fake/path',
|
|
46
|
+
});
|
|
47
|
+
// Should return empty since file doesn't exist
|
|
48
|
+
expect(result.files.length).toBe(0);
|
|
49
|
+
warnSpy.mockRestore();
|
|
50
|
+
});
|
|
51
|
+
it('should use custom include patterns', async () => {
|
|
52
|
+
fg.default.mockResolvedValue([]);
|
|
53
|
+
await analyzeProject({
|
|
54
|
+
rootDir: '/fake/path',
|
|
55
|
+
include: ['**/*.ts'],
|
|
56
|
+
exclude: ['**/node_modules/**'],
|
|
57
|
+
});
|
|
58
|
+
expect(fg.default).toHaveBeenCalledWith(expect.arrayContaining([expect.stringContaining('.ts')]), expect.objectContaining({ ignore: ['**/node_modules/**'] }));
|
|
59
|
+
});
|
|
60
|
+
it('should merge user config with defaults', async () => {
|
|
61
|
+
fg.default.mockResolvedValue([]);
|
|
62
|
+
const result = await analyzeProject({
|
|
63
|
+
rootDir: '/fake/path',
|
|
64
|
+
config: { maxUseEffectsPerComponent: 5 },
|
|
65
|
+
});
|
|
66
|
+
expect(result.summary).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('Technical Debt Score Calculation', () => {
|
|
70
|
+
it('should calculate grade A for clean projects', async () => {
|
|
71
|
+
const cleanCode = `
|
|
72
|
+
function CleanComponent() {
|
|
73
|
+
return <div>Clean</div>;
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
fg.default.mockResolvedValue(['/fake/path/Clean.tsx']);
|
|
77
|
+
fs.readFile.mockResolvedValue(cleanCode);
|
|
78
|
+
const result = await analyzeProject({
|
|
79
|
+
rootDir: '/fake/path',
|
|
80
|
+
config: { checkDebugStatements: false },
|
|
81
|
+
});
|
|
82
|
+
expect(result.debtScore.grade).toBe('A');
|
|
83
|
+
expect(result.debtScore.score).toBeGreaterThanOrEqual(90);
|
|
84
|
+
});
|
|
85
|
+
it('should estimate refactor time based on issues', async () => {
|
|
86
|
+
fg.default.mockResolvedValue([]);
|
|
87
|
+
const result = await analyzeProject({
|
|
88
|
+
rootDir: '/fake/path',
|
|
89
|
+
});
|
|
90
|
+
expect(result.debtScore.estimatedRefactorTime).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
it('should include breakdown scores', async () => {
|
|
93
|
+
fg.default.mockResolvedValue([]);
|
|
94
|
+
const result = await analyzeProject({
|
|
95
|
+
rootDir: '/fake/path',
|
|
96
|
+
});
|
|
97
|
+
expect(result.debtScore.breakdown).toBeDefined();
|
|
98
|
+
expect(result.debtScore.breakdown.useEffectScore).toBeDefined();
|
|
99
|
+
expect(result.debtScore.breakdown.propDrillingScore).toBeDefined();
|
|
100
|
+
expect(result.debtScore.breakdown.componentSizeScore).toBeDefined();
|
|
101
|
+
expect(result.debtScore.breakdown.memoizationScore).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('Smell Ignore Comments', () => {
|
|
105
|
+
it('should filter smells with @smell-ignore comment', async () => {
|
|
106
|
+
// Test behavior: when no files match, result should be empty
|
|
107
|
+
fg.default.mockResolvedValue([]);
|
|
108
|
+
const result = await analyzeProject({
|
|
109
|
+
rootDir: '/fake/path',
|
|
110
|
+
config: { checkDebugStatements: true },
|
|
111
|
+
});
|
|
112
|
+
// Empty result with no smells
|
|
113
|
+
expect(result.files.length).toBe(0);
|
|
114
|
+
expect(result.summary.totalSmells).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('Summary Calculation', () => {
|
|
118
|
+
it('should have smells by type structure', async () => {
|
|
119
|
+
fg.default.mockResolvedValue([]);
|
|
120
|
+
const result = await analyzeProject({
|
|
121
|
+
rootDir: '/fake/path',
|
|
122
|
+
config: { checkDebugStatements: true },
|
|
123
|
+
});
|
|
124
|
+
// Should have smellsByType object even if empty
|
|
125
|
+
expect(result.summary.smellsByType).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
it('should aggregate smells by severity', async () => {
|
|
128
|
+
fg.default.mockResolvedValue([]);
|
|
129
|
+
const result = await analyzeProject({
|
|
130
|
+
rootDir: '/fake/path',
|
|
131
|
+
});
|
|
132
|
+
expect(result.summary.smellsBySeverity).toBeDefined();
|
|
133
|
+
expect(result.summary.smellsBySeverity.error).toBeDefined();
|
|
134
|
+
expect(result.summary.smellsBySeverity.warning).toBeDefined();
|
|
135
|
+
expect(result.summary.smellsBySeverity.info).toBeDefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('Component Info Collection', () => {
|
|
139
|
+
it('should return components array in file results', async () => {
|
|
140
|
+
// Test that result structure includes components array
|
|
141
|
+
fg.default.mockResolvedValue([]);
|
|
142
|
+
const result = await analyzeProject({
|
|
143
|
+
rootDir: '/fake/path',
|
|
144
|
+
});
|
|
145
|
+
// For non-existent files, we get empty results
|
|
146
|
+
expect(Array.isArray(result.files)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/analyzer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// Mock dependencies for analyzer
|
|
3
|
+
vi.mock('fast-glob', () => ({
|
|
4
|
+
default: vi.fn().mockResolvedValue([]),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock('ora', () => ({
|
|
7
|
+
default: () => ({
|
|
8
|
+
start: vi.fn().mockReturnThis(),
|
|
9
|
+
succeed: vi.fn().mockReturnThis(),
|
|
10
|
+
fail: vi.fn().mockReturnThis(),
|
|
11
|
+
stop: vi.fn().mockReturnThis(),
|
|
12
|
+
text: '',
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
describe('Analyzer', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
describe('analyzeProject', () => {
|
|
20
|
+
it('should accept project path', () => {
|
|
21
|
+
const projectPath = '/path/to/project';
|
|
22
|
+
expect(typeof projectPath).toBe('string');
|
|
23
|
+
});
|
|
24
|
+
it('should accept options object', () => {
|
|
25
|
+
const options = {
|
|
26
|
+
verbose: true,
|
|
27
|
+
fix: false,
|
|
28
|
+
format: 'json',
|
|
29
|
+
};
|
|
30
|
+
expect(options.verbose).toBe(true);
|
|
31
|
+
expect(options.fix).toBe(false);
|
|
32
|
+
expect(options.format).toBe('json');
|
|
33
|
+
});
|
|
34
|
+
it('should handle default options', () => {
|
|
35
|
+
const defaultOptions = {
|
|
36
|
+
verbose: false,
|
|
37
|
+
fix: false,
|
|
38
|
+
format: 'console',
|
|
39
|
+
include: ['**/*.tsx', '**/*.jsx', '**/*.ts', '**/*.js'],
|
|
40
|
+
exclude: ['node_modules/**', 'dist/**', 'build/**'],
|
|
41
|
+
};
|
|
42
|
+
expect(defaultOptions.include).toContain('**/*.tsx');
|
|
43
|
+
expect(defaultOptions.exclude).toContain('node_modules/**');
|
|
44
|
+
});
|
|
45
|
+
it('should support various output formats', () => {
|
|
46
|
+
const formats = ['json', 'console', 'markdown', 'html', 'sarif'];
|
|
47
|
+
formats.forEach(format => {
|
|
48
|
+
expect(['json', 'console', 'markdown', 'html', 'sarif']).toContain(format);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('File pattern matching', () => {
|
|
53
|
+
it('should match React file extensions', () => {
|
|
54
|
+
const patterns = ['**/*.tsx', '**/*.jsx', '**/*.ts', '**/*.js'];
|
|
55
|
+
const testFiles = [
|
|
56
|
+
'src/App.tsx',
|
|
57
|
+
'src/utils/helper.ts',
|
|
58
|
+
'components/Button.jsx',
|
|
59
|
+
'lib/index.js',
|
|
60
|
+
];
|
|
61
|
+
testFiles.forEach(file => {
|
|
62
|
+
const matches = patterns.some(pattern => {
|
|
63
|
+
const ext = pattern.replace('**/*', '');
|
|
64
|
+
return file.endsWith(ext);
|
|
65
|
+
});
|
|
66
|
+
expect(matches).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it('should exclude node_modules and build dirs', () => {
|
|
70
|
+
const excludePatterns = ['node_modules/**', 'dist/**', 'build/**'];
|
|
71
|
+
const shouldExclude = (file) => {
|
|
72
|
+
return excludePatterns.some(pattern => {
|
|
73
|
+
const dir = pattern.replace('/**', '');
|
|
74
|
+
return file.startsWith(dir + '/') || file.startsWith(dir);
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
expect(shouldExclude('node_modules/react/index.js')).toBe(true);
|
|
78
|
+
expect(shouldExclude('dist/bundle.js')).toBe(true);
|
|
79
|
+
expect(shouldExclude('build/output.js')).toBe(true);
|
|
80
|
+
expect(shouldExclude('src/App.tsx')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('Options validation', () => {
|
|
84
|
+
it('should validate format option', () => {
|
|
85
|
+
const validFormats = ['json', 'console', 'markdown', 'html', 'sarif'];
|
|
86
|
+
const isValidFormat = (format) => validFormats.includes(format);
|
|
87
|
+
expect(isValidFormat('json')).toBe(true);
|
|
88
|
+
expect(isValidFormat('invalid')).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
it('should validate threshold is positive number', () => {
|
|
91
|
+
const isValidThreshold = (threshold) => {
|
|
92
|
+
return typeof threshold === 'number' && threshold >= 0;
|
|
93
|
+
};
|
|
94
|
+
expect(isValidThreshold(10)).toBe(true);
|
|
95
|
+
expect(isValidThreshold(0)).toBe(true);
|
|
96
|
+
expect(isValidThreshold(-1)).toBe(false);
|
|
97
|
+
expect(isValidThreshold('10')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
it('should validate custom rules path', () => {
|
|
100
|
+
const isValidPath = (path) => {
|
|
101
|
+
return typeof path === 'string' && path.length > 0;
|
|
102
|
+
};
|
|
103
|
+
expect(isValidPath('./rules.json')).toBe(true);
|
|
104
|
+
expect(isValidPath('')).toBe(false);
|
|
105
|
+
expect(isValidPath(null)).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('Analysis results structure', () => {
|
|
109
|
+
it('should have correct result shape', () => {
|
|
110
|
+
const mockResult = {
|
|
111
|
+
smells: [],
|
|
112
|
+
filesAnalyzed: 10,
|
|
113
|
+
totalSmells: 0,
|
|
114
|
+
byType: {},
|
|
115
|
+
bySeverity: { error: 0, warning: 0, info: 0 },
|
|
116
|
+
};
|
|
117
|
+
expect(mockResult).toHaveProperty('smells');
|
|
118
|
+
expect(mockResult).toHaveProperty('filesAnalyzed');
|
|
119
|
+
expect(mockResult).toHaveProperty('totalSmells');
|
|
120
|
+
expect(mockResult).toHaveProperty('byType');
|
|
121
|
+
expect(mockResult).toHaveProperty('bySeverity');
|
|
122
|
+
});
|
|
123
|
+
it('should aggregate smells by type', () => {
|
|
124
|
+
const smells = [
|
|
125
|
+
{ type: 'debug-statement' },
|
|
126
|
+
{ type: 'debug-statement' },
|
|
127
|
+
{ type: 'js-var-usage' },
|
|
128
|
+
];
|
|
129
|
+
const byType = smells.reduce((acc, s) => {
|
|
130
|
+
acc[s.type] = (acc[s.type] || 0) + 1;
|
|
131
|
+
return acc;
|
|
132
|
+
}, {});
|
|
133
|
+
expect(byType['debug-statement']).toBe(2);
|
|
134
|
+
expect(byType['js-var-usage']).toBe(1);
|
|
135
|
+
});
|
|
136
|
+
it('should aggregate smells by severity', () => {
|
|
137
|
+
const smells = [
|
|
138
|
+
{ severity: 'error' },
|
|
139
|
+
{ severity: 'warning' },
|
|
140
|
+
{ severity: 'warning' },
|
|
141
|
+
{ severity: 'info' },
|
|
142
|
+
];
|
|
143
|
+
const bySeverity = smells.reduce((acc, s) => {
|
|
144
|
+
acc[s.severity] = (acc[s.severity] || 0) + 1;
|
|
145
|
+
return acc;
|
|
146
|
+
}, { error: 0, warning: 0, info: 0 });
|
|
147
|
+
expect(bySeverity.error).toBe(1);
|
|
148
|
+
expect(bySeverity.warning).toBe(2);
|
|
149
|
+
expect(bySeverity.info).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('Performance', () => {
|
|
153
|
+
it('should batch file processing efficiently', () => {
|
|
154
|
+
const files = Array(100).fill(null).map((_, i) => `file${i}.tsx`);
|
|
155
|
+
const batchSize = 10;
|
|
156
|
+
const batches = [];
|
|
157
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
158
|
+
batches.push(files.slice(i, i + batchSize));
|
|
159
|
+
}
|
|
160
|
+
expect(batches).toHaveLength(10);
|
|
161
|
+
expect(batches[0]).toHaveLength(10);
|
|
162
|
+
});
|
|
163
|
+
it('should handle concurrent file analysis', async () => {
|
|
164
|
+
const analyzeFile = async (file) => {
|
|
165
|
+
return { file, smells: [] };
|
|
166
|
+
};
|
|
167
|
+
const files = ['a.tsx', 'b.tsx', 'c.tsx'];
|
|
168
|
+
const results = await Promise.all(files.map(analyzeFile));
|
|
169
|
+
expect(results).toHaveLength(3);
|
|
170
|
+
expect(results[0].file).toBe('a.tsx');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/baseline.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { initializeBaseline, recordBaseline, getTrendAnalysis } from '../baseline.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
describe('Baseline', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Create a temporary directory for tests
|
|
10
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'baseline-test-'));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
// Clean up temporary directory
|
|
14
|
+
try {
|
|
15
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Ignore cleanup errors
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const createSmell = (type) => ({
|
|
22
|
+
type: type,
|
|
23
|
+
severity: 'warning',
|
|
24
|
+
message: 'Test smell',
|
|
25
|
+
file: '/test.tsx',
|
|
26
|
+
line: 1,
|
|
27
|
+
column: 0,
|
|
28
|
+
suggestion: 'Fix it',
|
|
29
|
+
});
|
|
30
|
+
describe('initializeBaseline', () => {
|
|
31
|
+
it('should create baseline file if it does not exist', () => {
|
|
32
|
+
initializeBaseline(tempDir);
|
|
33
|
+
const baselinePath = path.join(tempDir, '.smellrc-baseline.json');
|
|
34
|
+
expect(fs.existsSync(baselinePath)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('should not overwrite existing baseline file', () => {
|
|
37
|
+
const baselinePath = path.join(tempDir, '.smellrc-baseline.json');
|
|
38
|
+
// Create initial baseline
|
|
39
|
+
initializeBaseline(tempDir);
|
|
40
|
+
// Add a record
|
|
41
|
+
recordBaseline(tempDir, [createSmell('debug-statement')]);
|
|
42
|
+
// Reinitialize
|
|
43
|
+
initializeBaseline(tempDir);
|
|
44
|
+
// Check that record still exists
|
|
45
|
+
const data = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
46
|
+
expect(data.records.length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
it('should create valid JSON structure', () => {
|
|
49
|
+
initializeBaseline(tempDir);
|
|
50
|
+
const baselinePath = path.join(tempDir, '.smellrc-baseline.json');
|
|
51
|
+
const data = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
52
|
+
expect(data.version).toBe('1.0');
|
|
53
|
+
expect(Array.isArray(data.records)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('recordBaseline', () => {
|
|
57
|
+
it('should add a record to baseline', () => {
|
|
58
|
+
initializeBaseline(tempDir);
|
|
59
|
+
const smells = [createSmell('debug-statement'), createSmell('js-var-usage')];
|
|
60
|
+
const record = recordBaseline(tempDir, smells);
|
|
61
|
+
expect(record.totalSmells).toBe(2);
|
|
62
|
+
expect(record.byType['debug-statement']).toBe(1);
|
|
63
|
+
expect(record.byType['js-var-usage']).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
it('should include timestamp', () => {
|
|
66
|
+
initializeBaseline(tempDir);
|
|
67
|
+
const record = recordBaseline(tempDir, [createSmell('debug-statement')]);
|
|
68
|
+
expect(record.timestamp).toBeDefined();
|
|
69
|
+
expect(new Date(record.timestamp).getTime()).toBeLessThanOrEqual(Date.now());
|
|
70
|
+
});
|
|
71
|
+
it('should include commit hash when provided', () => {
|
|
72
|
+
initializeBaseline(tempDir);
|
|
73
|
+
const record = recordBaseline(tempDir, [createSmell('debug-statement')], 'abc123');
|
|
74
|
+
expect(record.commit).toBe('abc123');
|
|
75
|
+
});
|
|
76
|
+
it('should keep only last 50 records', () => {
|
|
77
|
+
initializeBaseline(tempDir);
|
|
78
|
+
// Add 55 records
|
|
79
|
+
for (let i = 0; i < 55; i++) {
|
|
80
|
+
recordBaseline(tempDir, [createSmell('debug-statement')]);
|
|
81
|
+
}
|
|
82
|
+
const baselinePath = path.join(tempDir, '.smellrc-baseline.json');
|
|
83
|
+
const data = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
84
|
+
expect(data.records.length).toBe(50);
|
|
85
|
+
});
|
|
86
|
+
it('should limit stored smells to 100', () => {
|
|
87
|
+
initializeBaseline(tempDir);
|
|
88
|
+
// Create 150 smells
|
|
89
|
+
const manySmells = Array(150).fill(null).map(() => createSmell('debug-statement'));
|
|
90
|
+
const record = recordBaseline(tempDir, manySmells);
|
|
91
|
+
expect(record.totalSmells).toBe(150);
|
|
92
|
+
expect(record.smells.length).toBe(100);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('getTrendAnalysis', () => {
|
|
96
|
+
it('should return stable when no baseline exists', () => {
|
|
97
|
+
const trend = getTrendAnalysis(tempDir);
|
|
98
|
+
expect(trend.trend).toBe('stable');
|
|
99
|
+
expect(trend.improved).toBe(0);
|
|
100
|
+
expect(trend.worsened).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
it('should return stable with only one record', () => {
|
|
103
|
+
initializeBaseline(tempDir);
|
|
104
|
+
recordBaseline(tempDir, [createSmell('debug-statement')]);
|
|
105
|
+
const trend = getTrendAnalysis(tempDir);
|
|
106
|
+
expect(trend.trend).toBe('stable');
|
|
107
|
+
});
|
|
108
|
+
it('should detect improving trend', () => {
|
|
109
|
+
initializeBaseline(tempDir);
|
|
110
|
+
// First record: 5 smells
|
|
111
|
+
recordBaseline(tempDir, Array(5).fill(null).map(() => createSmell('debug-statement')));
|
|
112
|
+
// Second record: 3 smells (improved)
|
|
113
|
+
recordBaseline(tempDir, Array(3).fill(null).map(() => createSmell('debug-statement')));
|
|
114
|
+
const trend = getTrendAnalysis(tempDir);
|
|
115
|
+
expect(trend.trend).toBe('improving');
|
|
116
|
+
expect(trend.improved).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
it('should detect worsening trend', () => {
|
|
119
|
+
initializeBaseline(tempDir);
|
|
120
|
+
// First record: 3 smells
|
|
121
|
+
recordBaseline(tempDir, Array(3).fill(null).map(() => createSmell('debug-statement')));
|
|
122
|
+
// Second record: 5 smells (worsened)
|
|
123
|
+
recordBaseline(tempDir, Array(5).fill(null).map(() => createSmell('debug-statement')));
|
|
124
|
+
const trend = getTrendAnalysis(tempDir);
|
|
125
|
+
expect(trend.trend).toBe('worsening');
|
|
126
|
+
expect(trend.worsened).toBe(2);
|
|
127
|
+
});
|
|
128
|
+
it('should return stable when same count', () => {
|
|
129
|
+
initializeBaseline(tempDir);
|
|
130
|
+
recordBaseline(tempDir, Array(3).fill(null).map(() => createSmell('debug-statement')));
|
|
131
|
+
recordBaseline(tempDir, Array(3).fill(null).map(() => createSmell('js-var-usage')));
|
|
132
|
+
const trend = getTrendAnalysis(tempDir);
|
|
133
|
+
expect(trend.trend).toBe('stable');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundleAnalyzer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bundleAnalyzer.test.ts"],"names":[],"mappings":""}
|