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,193 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* Detect state management anti-patterns (Redux, Zustand, MobX, etc.)
|
|
4
|
+
*/
|
|
5
|
+
export function detectStateManagementIssues(component, filePath, sourceCode, config) {
|
|
6
|
+
if (!config.checkStateManagement)
|
|
7
|
+
return [];
|
|
8
|
+
const smells = [];
|
|
9
|
+
let useSelectorCount = 0;
|
|
10
|
+
let useStoreCount = 0;
|
|
11
|
+
const selectorLocations = [];
|
|
12
|
+
component.path.traverse({
|
|
13
|
+
CallExpression(path) {
|
|
14
|
+
const node = path.node;
|
|
15
|
+
const { callee } = node;
|
|
16
|
+
// Detect Redux useSelector
|
|
17
|
+
if (t.isIdentifier(callee) && callee.name === 'useSelector') {
|
|
18
|
+
useSelectorCount++;
|
|
19
|
+
const loc = node.loc;
|
|
20
|
+
selectorLocations.push({ line: loc?.start.line || 0, column: loc?.start.column || 0 });
|
|
21
|
+
// Check if selector is defined inline (anti-pattern)
|
|
22
|
+
if (node.arguments.length > 0 && t.isArrowFunctionExpression(node.arguments[0])) {
|
|
23
|
+
smells.push({
|
|
24
|
+
type: 'redux-in-render',
|
|
25
|
+
severity: 'warning',
|
|
26
|
+
message: `Inline selector in useSelector in "${component.name}" - will cause re-renders`,
|
|
27
|
+
file: filePath,
|
|
28
|
+
line: loc?.start.line || 0,
|
|
29
|
+
column: loc?.start.column || 0,
|
|
30
|
+
suggestion: 'Define selectors outside the component or use createSelector for memoization.',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Detect Zustand useStore
|
|
35
|
+
if (t.isIdentifier(callee) && (callee.name === 'useStore' || callee.name.endsWith('Store'))) {
|
|
36
|
+
useStoreCount++;
|
|
37
|
+
const loc = node.loc;
|
|
38
|
+
// Check if selecting entire store (anti-pattern)
|
|
39
|
+
if (node.arguments.length === 0) {
|
|
40
|
+
smells.push({
|
|
41
|
+
type: 'excessive-redux-selectors',
|
|
42
|
+
severity: 'warning',
|
|
43
|
+
message: `Using entire store without selector in "${component.name}"`,
|
|
44
|
+
file: filePath,
|
|
45
|
+
line: loc?.start.line || 0,
|
|
46
|
+
column: loc?.start.column || 0,
|
|
47
|
+
suggestion: 'Use a selector function to subscribe only to needed state slices.',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Detect useDispatch in render
|
|
52
|
+
if (t.isIdentifier(callee) && callee.name === 'useDispatch') {
|
|
53
|
+
// Check if dispatch is being called directly in render body
|
|
54
|
+
let currentPath = path.parentPath;
|
|
55
|
+
let inCallback = false;
|
|
56
|
+
while (currentPath) {
|
|
57
|
+
if (t.isArrowFunctionExpression(currentPath.node) ||
|
|
58
|
+
t.isFunctionExpression(currentPath.node)) {
|
|
59
|
+
inCallback = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (t.isJSXAttribute(currentPath.node)) {
|
|
63
|
+
inCallback = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
currentPath = currentPath.parentPath;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
// Detect derived state stored in state (anti-pattern)
|
|
71
|
+
VariableDeclarator(path) {
|
|
72
|
+
const node = path.node;
|
|
73
|
+
// Check for useState with computed value from props or other state
|
|
74
|
+
if (t.isCallExpression(node.init) &&
|
|
75
|
+
t.isIdentifier(node.init.callee) &&
|
|
76
|
+
node.init.callee.name === 'useState') {
|
|
77
|
+
const args = node.init.arguments;
|
|
78
|
+
if (args.length > 0) {
|
|
79
|
+
// Check if initial value is derived from props
|
|
80
|
+
const initValue = args[0];
|
|
81
|
+
if (t.isIdentifier(initValue) && initValue.name.startsWith('props')) {
|
|
82
|
+
const loc = node.loc;
|
|
83
|
+
smells.push({
|
|
84
|
+
type: 'derived-state-in-state',
|
|
85
|
+
severity: 'warning',
|
|
86
|
+
message: `State initialized from props in "${component.name}" - may cause sync issues`,
|
|
87
|
+
file: filePath,
|
|
88
|
+
line: loc?.start.line || 0,
|
|
89
|
+
column: loc?.start.column || 0,
|
|
90
|
+
suggestion: 'Derive values directly from props instead of copying to state, or use a key to reset.',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
// Check for too many selectors (component doing too much)
|
|
98
|
+
if (useSelectorCount > 3) {
|
|
99
|
+
const firstLoc = selectorLocations[0] || { line: component.path.node.loc?.start.line || 0, column: 0 };
|
|
100
|
+
smells.push({
|
|
101
|
+
type: 'excessive-redux-selectors',
|
|
102
|
+
severity: 'warning',
|
|
103
|
+
message: `Component "${component.name}" has ${useSelectorCount} useSelector calls - consider splitting`,
|
|
104
|
+
file: filePath,
|
|
105
|
+
line: firstLoc.line,
|
|
106
|
+
column: firstLoc.column,
|
|
107
|
+
suggestion: 'Create a combined selector or split into smaller connected components.',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Detect state sync anti-patterns
|
|
111
|
+
detectStateSyncAntiPatterns(component, filePath, smells);
|
|
112
|
+
return smells;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Detect state synchronization anti-patterns
|
|
116
|
+
*/
|
|
117
|
+
function detectStateSyncAntiPatterns(component, filePath, smells) {
|
|
118
|
+
// Check useEffect that sets state based on other state (state sync)
|
|
119
|
+
for (const effect of component.hooks.useEffect) {
|
|
120
|
+
if (!t.isArrowFunctionExpression(effect.arguments[0]) &&
|
|
121
|
+
!t.isFunctionExpression(effect.arguments[0])) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const callback = effect.arguments[0];
|
|
125
|
+
let setsState = false;
|
|
126
|
+
let dependsOnState = false;
|
|
127
|
+
// Simple check: look for setState calls in the effect
|
|
128
|
+
if (t.isBlockStatement(callback.body)) {
|
|
129
|
+
const bodyStr = JSON.stringify(callback.body);
|
|
130
|
+
setsState = bodyStr.includes('"callee":{"name":"set') ||
|
|
131
|
+
bodyStr.includes('setState');
|
|
132
|
+
}
|
|
133
|
+
// Check dependency array for state variables
|
|
134
|
+
if (effect.arguments.length > 1 && t.isArrayExpression(effect.arguments[1])) {
|
|
135
|
+
const deps = effect.arguments[1].elements;
|
|
136
|
+
dependsOnState = deps.length > 0;
|
|
137
|
+
}
|
|
138
|
+
if (setsState && dependsOnState) {
|
|
139
|
+
const loc = effect.loc;
|
|
140
|
+
smells.push({
|
|
141
|
+
type: 'state-sync-anti-pattern',
|
|
142
|
+
severity: 'warning',
|
|
143
|
+
message: `useEffect syncs state from dependencies in "${component.name}" - potential infinite loop or unnecessary re-renders`,
|
|
144
|
+
file: filePath,
|
|
145
|
+
line: loc?.start.line || 0,
|
|
146
|
+
column: 0,
|
|
147
|
+
suggestion: 'Consider deriving the value during render instead of syncing in useEffect.',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Detect file-level state management patterns
|
|
154
|
+
*/
|
|
155
|
+
export function detectStateManagementPatterns(sourceCode, filePath, config) {
|
|
156
|
+
if (!config.checkStateManagement)
|
|
157
|
+
return [];
|
|
158
|
+
const smells = [];
|
|
159
|
+
const lines = sourceCode.split('\n');
|
|
160
|
+
lines.forEach((line, index) => {
|
|
161
|
+
// Check for dispatch in JSX (anti-pattern - should be in handler)
|
|
162
|
+
if (line.includes('onClick={') && line.includes('dispatch(')) {
|
|
163
|
+
smells.push({
|
|
164
|
+
type: 'redux-in-render',
|
|
165
|
+
severity: 'info',
|
|
166
|
+
message: 'Inline dispatch in JSX - consider extracting to handler function',
|
|
167
|
+
file: filePath,
|
|
168
|
+
line: index + 1,
|
|
169
|
+
column: 0,
|
|
170
|
+
suggestion: 'Extract dispatch calls to named handler functions for better readability and debugging.',
|
|
171
|
+
codeSnippet: line.trim(),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Check for createSelector without memoization
|
|
175
|
+
if (line.includes('useSelector(state =>') && !line.includes('createSelector')) {
|
|
176
|
+
// Check if it's a complex selector (has multiple property accesses)
|
|
177
|
+
const accessCount = (line.match(/\./g) || []).length;
|
|
178
|
+
if (accessCount > 2) {
|
|
179
|
+
smells.push({
|
|
180
|
+
type: 'redux-in-render',
|
|
181
|
+
severity: 'info',
|
|
182
|
+
message: 'Complex inline selector without createSelector',
|
|
183
|
+
file: filePath,
|
|
184
|
+
line: index + 1,
|
|
185
|
+
column: 0,
|
|
186
|
+
suggestion: 'Use createSelector from reselect for complex derived data to enable memoization.',
|
|
187
|
+
codeSnippet: line.trim(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return smells;
|
|
193
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect components that are likely hard to test or should have tests
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectTestingGaps(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
7
|
+
/**
|
|
8
|
+
* Generate test file suggestions for components
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateTestSuggestions(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Check if a test file exists for a component
|
|
13
|
+
*/
|
|
14
|
+
export declare function hasTestFile(filePath: string): boolean;
|
|
15
|
+
//# sourceMappingURL=testingGaps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingGaps.d.ts","sourceRoot":"","sources":["../../src/detectors/testingGaps.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAgDb;AAsHD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,MAAM,EAAE,CAiCV;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAUrD"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* Detect components that are likely hard to test or should have tests
|
|
4
|
+
*/
|
|
5
|
+
export function detectTestingGaps(component, filePath, sourceCode, config) {
|
|
6
|
+
if (!config.checkTestingGaps)
|
|
7
|
+
return [];
|
|
8
|
+
const smells = [];
|
|
9
|
+
// Calculate testability metrics
|
|
10
|
+
const metrics = calculateTestabilityMetrics(component, sourceCode);
|
|
11
|
+
// Flag complex components without apparent tests
|
|
12
|
+
if (metrics.complexityScore > config.testComplexityThreshold) {
|
|
13
|
+
smells.push({
|
|
14
|
+
type: 'complex-untestable',
|
|
15
|
+
severity: 'info',
|
|
16
|
+
message: `Component "${component.name}" has high complexity (${metrics.complexityScore}) - ensure adequate test coverage`,
|
|
17
|
+
file: filePath,
|
|
18
|
+
line: component.path.node.loc?.start.line || 0,
|
|
19
|
+
column: 0,
|
|
20
|
+
suggestion: `This component has ${metrics.hooks} hooks, ${metrics.effects} effects, and ${metrics.conditionals} conditionals. Consider splitting or ensuring thorough test coverage.`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Flag components with many side effects
|
|
24
|
+
if (metrics.sideEffects > 3) {
|
|
25
|
+
smells.push({
|
|
26
|
+
type: 'side-effect-heavy',
|
|
27
|
+
severity: 'warning',
|
|
28
|
+
message: `Component "${component.name}" has ${metrics.sideEffects} side effects - difficult to test`,
|
|
29
|
+
file: filePath,
|
|
30
|
+
line: component.path.node.loc?.start.line || 0,
|
|
31
|
+
column: 0,
|
|
32
|
+
suggestion: 'Extract side effects into custom hooks or separate functions for easier testing and mocking.',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Flag tightly coupled components
|
|
36
|
+
if (metrics.externalDependencies > 5) {
|
|
37
|
+
smells.push({
|
|
38
|
+
type: 'tightly-coupled',
|
|
39
|
+
severity: 'info',
|
|
40
|
+
message: `Component "${component.name}" has ${metrics.externalDependencies} external dependencies`,
|
|
41
|
+
file: filePath,
|
|
42
|
+
line: component.path.node.loc?.start.line || 0,
|
|
43
|
+
column: 0,
|
|
44
|
+
suggestion: 'Consider dependency injection or using a container pattern to make testing easier.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return smells;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculate testability metrics for a component
|
|
51
|
+
*/
|
|
52
|
+
function calculateTestabilityMetrics(component, sourceCode) {
|
|
53
|
+
let conditionals = 0;
|
|
54
|
+
let sideEffects = 0;
|
|
55
|
+
let externalDependencies = 0;
|
|
56
|
+
let callbacks = 0;
|
|
57
|
+
component.path.traverse({
|
|
58
|
+
// Count conditionals
|
|
59
|
+
IfStatement() {
|
|
60
|
+
conditionals++;
|
|
61
|
+
},
|
|
62
|
+
ConditionalExpression() {
|
|
63
|
+
conditionals++;
|
|
64
|
+
},
|
|
65
|
+
SwitchStatement() {
|
|
66
|
+
conditionals++;
|
|
67
|
+
},
|
|
68
|
+
LogicalExpression(path) {
|
|
69
|
+
const node = path.node;
|
|
70
|
+
if (node.operator === '&&' || node.operator === '||') {
|
|
71
|
+
conditionals++;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
// Count callbacks/event handlers
|
|
75
|
+
ArrowFunctionExpression() {
|
|
76
|
+
callbacks++;
|
|
77
|
+
},
|
|
78
|
+
FunctionExpression() {
|
|
79
|
+
callbacks++;
|
|
80
|
+
},
|
|
81
|
+
// Count side effects
|
|
82
|
+
CallExpression(path) {
|
|
83
|
+
const node = path.node;
|
|
84
|
+
const { callee } = node;
|
|
85
|
+
if (t.isIdentifier(callee)) {
|
|
86
|
+
// Side effect indicators
|
|
87
|
+
const sideEffectFns = [
|
|
88
|
+
'fetch', 'axios', 'console', 'localStorage', 'sessionStorage',
|
|
89
|
+
'setInterval', 'setTimeout', 'addEventListener', 'removeEventListener',
|
|
90
|
+
'dispatch', 'navigate', 'push', 'replace',
|
|
91
|
+
];
|
|
92
|
+
if (sideEffectFns.some(fn => callee.name.includes(fn))) {
|
|
93
|
+
sideEffects++;
|
|
94
|
+
}
|
|
95
|
+
// External dependencies (hooks that likely need mocking)
|
|
96
|
+
const externalHooks = [
|
|
97
|
+
'useQuery', 'useMutation', 'useSWR', 'useRouter', 'useNavigate',
|
|
98
|
+
'useLocation', 'useParams', 'useSelector', 'useDispatch',
|
|
99
|
+
'useAuth', 'useUser', 'useApi',
|
|
100
|
+
];
|
|
101
|
+
if (externalHooks.some(hook => callee.name === hook || callee.name.includes(hook))) {
|
|
102
|
+
externalDependencies++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Member expression side effects (e.g., router.push, api.get)
|
|
106
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
107
|
+
const methodName = callee.property.name;
|
|
108
|
+
const sideEffectMethods = ['push', 'replace', 'get', 'post', 'put', 'delete', 'fetch'];
|
|
109
|
+
if (sideEffectMethods.includes(methodName)) {
|
|
110
|
+
sideEffects++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const hooks = component.hooks.useState.length +
|
|
116
|
+
component.hooks.useEffect.length +
|
|
117
|
+
component.hooks.useMemo.length +
|
|
118
|
+
component.hooks.useCallback.length +
|
|
119
|
+
component.hooks.useRef.length;
|
|
120
|
+
const effects = component.hooks.useEffect.length;
|
|
121
|
+
// Calculate overall complexity score
|
|
122
|
+
const complexityScore = hooks * 1 +
|
|
123
|
+
effects * 2 +
|
|
124
|
+
conditionals * 1.5 +
|
|
125
|
+
sideEffects * 2 +
|
|
126
|
+
externalDependencies * 1.5 +
|
|
127
|
+
callbacks * 0.5;
|
|
128
|
+
return {
|
|
129
|
+
complexityScore: Math.round(complexityScore),
|
|
130
|
+
hooks,
|
|
131
|
+
effects,
|
|
132
|
+
conditionals,
|
|
133
|
+
sideEffects,
|
|
134
|
+
externalDependencies,
|
|
135
|
+
callbacks,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Generate test file suggestions for components
|
|
140
|
+
*/
|
|
141
|
+
export function generateTestSuggestions(component, filePath, sourceCode, config) {
|
|
142
|
+
const suggestions = [];
|
|
143
|
+
const metrics = calculateTestabilityMetrics(component, sourceCode);
|
|
144
|
+
// Basic render test
|
|
145
|
+
suggestions.push(`it('should render ${component.name} without crashing', () => {
|
|
146
|
+
render(<${component.name} />);
|
|
147
|
+
});`);
|
|
148
|
+
// Props tests if component has props
|
|
149
|
+
if (component.path.node.type === 'FunctionDeclaration' ||
|
|
150
|
+
(component.path.node.type === 'VariableDeclarator' &&
|
|
151
|
+
t.isArrowFunctionExpression(component.path.node.init))) {
|
|
152
|
+
suggestions.push(`it('should pass props correctly', () => {
|
|
153
|
+
// TODO: Add prop tests for ${component.name}
|
|
154
|
+
});`);
|
|
155
|
+
}
|
|
156
|
+
// Effect tests
|
|
157
|
+
if (metrics.effects > 0) {
|
|
158
|
+
suggestions.push(`it('should handle side effects in ${component.name}', async () => {
|
|
159
|
+
// TODO: Test useEffect behavior
|
|
160
|
+
});`);
|
|
161
|
+
}
|
|
162
|
+
// Interaction tests
|
|
163
|
+
if (metrics.callbacks > 0) {
|
|
164
|
+
suggestions.push(`it('should handle user interactions', async () => {
|
|
165
|
+
// TODO: Add interaction tests for ${component.name}
|
|
166
|
+
});`);
|
|
167
|
+
}
|
|
168
|
+
return suggestions;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Check if a test file exists for a component
|
|
172
|
+
*/
|
|
173
|
+
export function hasTestFile(filePath) {
|
|
174
|
+
const testPatterns = [
|
|
175
|
+
filePath.replace(/\.(tsx?|jsx?)$/, '.test.$1'),
|
|
176
|
+
filePath.replace(/\.(tsx?|jsx?)$/, '.spec.$1'),
|
|
177
|
+
filePath.replace(/\/([^/]+)\.(tsx?|jsx?)$/, '/__tests__/$1.$2'),
|
|
178
|
+
];
|
|
179
|
+
// Note: This is a simple pattern check. In real usage,
|
|
180
|
+
// you'd want to actually check the file system
|
|
181
|
+
return false; // Placeholder - actual implementation would check fs
|
|
182
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AnalysisResult, CodeSmell } from './types/index.js';
|
|
2
|
+
export interface DocGeneratorOptions {
|
|
3
|
+
format: 'markdown' | 'html' | 'json';
|
|
4
|
+
outputDir?: string;
|
|
5
|
+
includeSmells?: boolean;
|
|
6
|
+
includeMetrics?: boolean;
|
|
7
|
+
groupByFolder?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ComponentDoc {
|
|
10
|
+
name: string;
|
|
11
|
+
file: string;
|
|
12
|
+
relativePath: string;
|
|
13
|
+
lineCount: number;
|
|
14
|
+
props: string[];
|
|
15
|
+
hooks: {
|
|
16
|
+
useState: number;
|
|
17
|
+
useEffect: number;
|
|
18
|
+
useMemo: number;
|
|
19
|
+
useCallback: number;
|
|
20
|
+
useRef: number;
|
|
21
|
+
custom: string[];
|
|
22
|
+
};
|
|
23
|
+
smells: CodeSmell[];
|
|
24
|
+
metrics: {
|
|
25
|
+
complexity: string;
|
|
26
|
+
maintainability: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate documentation from component analysis
|
|
31
|
+
*/
|
|
32
|
+
export declare function generateComponentDocs(result: AnalysisResult, rootDir: string, options?: DocGeneratorOptions): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Write documentation to file
|
|
35
|
+
*/
|
|
36
|
+
export declare function writeComponentDocs(result: AnalysisResult, rootDir: string, options: DocGeneratorOptions): Promise<string>;
|
|
37
|
+
//# sourceMappingURL=docGenerator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docGenerator.d.ts","sourceRoot":"","sources":["../src/docGenerator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAA+B,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE1F,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAA4C,GACpD,OAAO,CAAC,MAAM,CAAC,CAYjB;AAqTD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAYjB"}
|