react-code-smell-detector 1.5.0 → 1.5.2
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 +241 -4
- 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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* Detect Context API anti-patterns and performance issues
|
|
4
|
+
*/
|
|
5
|
+
export function detectContextIssues(component, filePath, sourceCode, config) {
|
|
6
|
+
if (!config.checkContextApi)
|
|
7
|
+
return [];
|
|
8
|
+
const smells = [];
|
|
9
|
+
let useContextCount = 0;
|
|
10
|
+
const contextLocations = [];
|
|
11
|
+
// Check for useContext calls
|
|
12
|
+
component.path.traverse({
|
|
13
|
+
CallExpression(path) {
|
|
14
|
+
const node = path.node;
|
|
15
|
+
const { callee } = node;
|
|
16
|
+
// Detect useContext calls
|
|
17
|
+
if (t.isIdentifier(callee) && callee.name === 'useContext') {
|
|
18
|
+
useContextCount++;
|
|
19
|
+
const loc = node.loc;
|
|
20
|
+
if (loc) {
|
|
21
|
+
contextLocations.push({ line: loc.start.line, column: loc.start.column });
|
|
22
|
+
}
|
|
23
|
+
// Check if context value is destructured with many properties
|
|
24
|
+
const parent = path.parentPath;
|
|
25
|
+
if (parent && t.isVariableDeclarator(parent.node)) {
|
|
26
|
+
const id = parent.node.id;
|
|
27
|
+
if (t.isObjectPattern(id) && id.properties.length > 5) {
|
|
28
|
+
smells.push({
|
|
29
|
+
type: 'large-context-value',
|
|
30
|
+
severity: 'warning',
|
|
31
|
+
message: `Context destructures ${id.properties.length} properties in "${component.name}" - consider splitting context`,
|
|
32
|
+
file: filePath,
|
|
33
|
+
line: loc?.start.line || 0,
|
|
34
|
+
column: loc?.start.column || 0,
|
|
35
|
+
suggestion: 'Split large contexts into smaller, focused contexts to prevent unnecessary re-renders.',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Detect useContext inside loops or conditionals (anti-pattern)
|
|
41
|
+
if (t.isIdentifier(callee) && callee.name === 'useContext') {
|
|
42
|
+
let currentPath = path.parentPath;
|
|
43
|
+
while (currentPath) {
|
|
44
|
+
if (t.isForStatement(currentPath.node) ||
|
|
45
|
+
t.isWhileStatement(currentPath.node) ||
|
|
46
|
+
t.isForInStatement(currentPath.node) ||
|
|
47
|
+
t.isForOfStatement(currentPath.node)) {
|
|
48
|
+
const loc = node.loc;
|
|
49
|
+
smells.push({
|
|
50
|
+
type: 'context-in-loop',
|
|
51
|
+
severity: 'error',
|
|
52
|
+
message: `useContext called inside a loop in "${component.name}"`,
|
|
53
|
+
file: filePath,
|
|
54
|
+
line: loc?.start.line || 0,
|
|
55
|
+
column: loc?.start.column || 0,
|
|
56
|
+
suggestion: 'Move useContext outside of loops. Hooks must be called at the top level.',
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
currentPath = currentPath.parentPath;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
// Check for too many useContext calls (context overuse)
|
|
66
|
+
if (useContextCount > config.maxContextConsumers) {
|
|
67
|
+
const firstLoc = contextLocations[0] || { line: component.path.node.loc?.start.line || 0, column: 0 };
|
|
68
|
+
smells.push({
|
|
69
|
+
type: 'context-overuse',
|
|
70
|
+
severity: 'warning',
|
|
71
|
+
message: `Component "${component.name}" uses ${useContextCount} contexts (max: ${config.maxContextConsumers})`,
|
|
72
|
+
file: filePath,
|
|
73
|
+
line: firstLoc.line,
|
|
74
|
+
column: firstLoc.column,
|
|
75
|
+
suggestion: 'Consider using composition or combining related contexts. Too many contexts can make components hard to test and maintain.',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Check if component using context is not memoized
|
|
79
|
+
if (useContextCount > 0) {
|
|
80
|
+
const isMemoized = checkIfMemoized(component, sourceCode);
|
|
81
|
+
if (!isMemoized && useContextCount > 1) {
|
|
82
|
+
smells.push({
|
|
83
|
+
type: 'missing-context-memo',
|
|
84
|
+
severity: 'info',
|
|
85
|
+
message: `Component "${component.name}" uses multiple contexts but is not memoized`,
|
|
86
|
+
file: filePath,
|
|
87
|
+
line: component.path.node.loc?.start.line || 0,
|
|
88
|
+
column: 0,
|
|
89
|
+
suggestion: 'Consider wrapping with React.memo() to prevent unnecessary re-renders when unrelated context values change.',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return smells;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Check if a component is wrapped with React.memo
|
|
97
|
+
*/
|
|
98
|
+
function checkIfMemoized(component, sourceCode) {
|
|
99
|
+
const lines = sourceCode.split('\n');
|
|
100
|
+
const componentName = component.name;
|
|
101
|
+
// Check for React.memo or memo wrapper patterns
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (line.includes(`memo(${componentName})`) ||
|
|
104
|
+
line.includes(`React.memo(${componentName})`) ||
|
|
105
|
+
line.includes(`memo(function ${componentName}`)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Detect Context Provider issues
|
|
113
|
+
*/
|
|
114
|
+
export function detectContextProviderIssues(sourceCode, filePath, config) {
|
|
115
|
+
if (!config.checkContextApi)
|
|
116
|
+
return [];
|
|
117
|
+
const smells = [];
|
|
118
|
+
const lines = sourceCode.split('\n');
|
|
119
|
+
// Check for context provider with object literal value (causes re-renders)
|
|
120
|
+
lines.forEach((line, index) => {
|
|
121
|
+
// Pattern: <SomeContext.Provider value={{ ... }}>
|
|
122
|
+
const providerMatch = line.match(/\.Provider\s+value=\{\{/);
|
|
123
|
+
if (providerMatch) {
|
|
124
|
+
smells.push({
|
|
125
|
+
type: 'large-context-value',
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
message: 'Context Provider has inline object value - causes re-renders on every render',
|
|
128
|
+
file: filePath,
|
|
129
|
+
line: index + 1,
|
|
130
|
+
column: providerMatch.index || 0,
|
|
131
|
+
suggestion: 'Memoize the context value with useMemo() or extract to a stable reference.',
|
|
132
|
+
codeSnippet: line.trim(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Pattern: <SomeContext.Provider value={[ ... ]}>
|
|
136
|
+
const arrayMatch = line.match(/\.Provider\s+value=\{\[/);
|
|
137
|
+
if (arrayMatch) {
|
|
138
|
+
smells.push({
|
|
139
|
+
type: 'large-context-value',
|
|
140
|
+
severity: 'warning',
|
|
141
|
+
message: 'Context Provider has inline array value - causes re-renders on every render',
|
|
142
|
+
file: filePath,
|
|
143
|
+
line: index + 1,
|
|
144
|
+
column: arrayMatch.index || 0,
|
|
145
|
+
suggestion: 'Memoize the context value with useMemo() or extract to a stable reference.',
|
|
146
|
+
codeSnippet: line.trim(),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return smells;
|
|
151
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect missing Error Boundaries and Suspense issues
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectErrorBoundaryIssues(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
7
|
+
/**
|
|
8
|
+
* Detect file-level error boundary patterns
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectMissingErrorBoundaries(sourceCode: string, filePath: string, config: DetectorConfig): CodeSmell[];
|
|
11
|
+
//# sourceMappingURL=errorBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errorBoundary.d.ts","sourceRoot":"","sources":["../../src/detectors/errorBoundary.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,yBAAyB,CACvC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAoIb;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAyDb"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* Detect missing Error Boundaries and Suspense issues
|
|
4
|
+
*/
|
|
5
|
+
export function detectErrorBoundaryIssues(component, filePath, sourceCode, config) {
|
|
6
|
+
if (!config.checkErrorBoundaries)
|
|
7
|
+
return [];
|
|
8
|
+
const smells = [];
|
|
9
|
+
// Track JSX elements and their wrappers
|
|
10
|
+
let hasSuspense = false;
|
|
11
|
+
let hasErrorBoundary = false;
|
|
12
|
+
let hasAsyncComponent = false;
|
|
13
|
+
let hasLazyLoad = false;
|
|
14
|
+
const suspenseLocations = [];
|
|
15
|
+
component.path.traverse({
|
|
16
|
+
JSXElement(path) {
|
|
17
|
+
const openingElement = path.node.openingElement;
|
|
18
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
19
|
+
const name = openingElement.name.name;
|
|
20
|
+
// Check for Suspense
|
|
21
|
+
if (name === 'Suspense') {
|
|
22
|
+
hasSuspense = true;
|
|
23
|
+
const loc = openingElement.loc;
|
|
24
|
+
const hasFallback = openingElement.attributes.some(attr => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === 'fallback');
|
|
25
|
+
suspenseLocations.push({
|
|
26
|
+
line: loc?.start.line || 0,
|
|
27
|
+
hasFallback,
|
|
28
|
+
});
|
|
29
|
+
if (!hasFallback) {
|
|
30
|
+
smells.push({
|
|
31
|
+
type: 'suspense-missing-fallback',
|
|
32
|
+
severity: 'warning',
|
|
33
|
+
message: `Suspense without fallback prop in "${component.name}"`,
|
|
34
|
+
file: filePath,
|
|
35
|
+
line: loc?.start.line || 0,
|
|
36
|
+
column: loc?.start.column || 0,
|
|
37
|
+
suggestion: 'Add a fallback prop to Suspense: <Suspense fallback={<Loading />}>',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Check for ErrorBoundary (custom or library)
|
|
42
|
+
if (name === 'ErrorBoundary' ||
|
|
43
|
+
name.includes('ErrorBoundary') ||
|
|
44
|
+
name === 'ErrorBoundaryWrapper') {
|
|
45
|
+
hasErrorBoundary = true;
|
|
46
|
+
const hasFallback = openingElement.attributes.some(attr => t.isJSXAttribute(attr) &&
|
|
47
|
+
t.isJSXIdentifier(attr.name) &&
|
|
48
|
+
(attr.name.name === 'fallback' || attr.name.name === 'FallbackComponent' || attr.name.name === 'fallbackRender'));
|
|
49
|
+
if (!hasFallback) {
|
|
50
|
+
const loc = openingElement.loc;
|
|
51
|
+
smells.push({
|
|
52
|
+
type: 'error-boundary-missing-fallback',
|
|
53
|
+
severity: 'warning',
|
|
54
|
+
message: `ErrorBoundary without fallback in "${component.name}"`,
|
|
55
|
+
file: filePath,
|
|
56
|
+
line: loc?.start.line || 0,
|
|
57
|
+
column: loc?.start.column || 0,
|
|
58
|
+
suggestion: 'Add a fallback prop or FallbackComponent to ErrorBoundary.',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
CallExpression(path) {
|
|
65
|
+
const node = path.node;
|
|
66
|
+
const { callee } = node;
|
|
67
|
+
// Check for React.lazy or lazy imports
|
|
68
|
+
if ((t.isIdentifier(callee) && callee.name === 'lazy') ||
|
|
69
|
+
(t.isMemberExpression(callee) &&
|
|
70
|
+
t.isIdentifier(callee.object) &&
|
|
71
|
+
callee.object.name === 'React' &&
|
|
72
|
+
t.isIdentifier(callee.property) &&
|
|
73
|
+
callee.property.name === 'lazy')) {
|
|
74
|
+
hasLazyLoad = true;
|
|
75
|
+
}
|
|
76
|
+
// Check for async data fetching patterns
|
|
77
|
+
if (t.isIdentifier(callee)) {
|
|
78
|
+
const asyncPatterns = ['useQuery', 'useSWR', 'useAsync', 'useFetch', 'useData'];
|
|
79
|
+
if (asyncPatterns.includes(callee.name)) {
|
|
80
|
+
hasAsyncComponent = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
// Detect async components (often need Suspense)
|
|
85
|
+
AwaitExpression() {
|
|
86
|
+
hasAsyncComponent = true;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
// Check for lazy-loaded components without Suspense
|
|
90
|
+
if (hasLazyLoad && !hasSuspense) {
|
|
91
|
+
smells.push({
|
|
92
|
+
type: 'missing-error-boundary',
|
|
93
|
+
severity: 'error',
|
|
94
|
+
message: `Component "${component.name}" uses lazy loading but has no Suspense wrapper`,
|
|
95
|
+
file: filePath,
|
|
96
|
+
line: component.path.node.loc?.start.line || 0,
|
|
97
|
+
column: 0,
|
|
98
|
+
suggestion: 'Wrap lazy-loaded components with <Suspense fallback={<Loading />}>',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Suggest ErrorBoundary for complex components
|
|
102
|
+
if (!hasErrorBoundary && hasAsyncComponent) {
|
|
103
|
+
smells.push({
|
|
104
|
+
type: 'missing-error-boundary',
|
|
105
|
+
severity: 'info',
|
|
106
|
+
message: `Component "${component.name}" has async operations but no ErrorBoundary`,
|
|
107
|
+
file: filePath,
|
|
108
|
+
line: component.path.node.loc?.start.line || 0,
|
|
109
|
+
column: 0,
|
|
110
|
+
suggestion: 'Consider wrapping async components with an ErrorBoundary to handle failures gracefully.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return smells;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Detect file-level error boundary patterns
|
|
117
|
+
*/
|
|
118
|
+
export function detectMissingErrorBoundaries(sourceCode, filePath, config) {
|
|
119
|
+
if (!config.checkErrorBoundaries)
|
|
120
|
+
return [];
|
|
121
|
+
const smells = [];
|
|
122
|
+
const lines = sourceCode.split('\n');
|
|
123
|
+
// Check for lazy imports without corresponding Suspense
|
|
124
|
+
let hasLazyImport = false;
|
|
125
|
+
let hasSuspenseImport = false;
|
|
126
|
+
let hasErrorBoundaryImport = false;
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
if (line.includes('lazy(') || line.includes('React.lazy(')) {
|
|
129
|
+
hasLazyImport = true;
|
|
130
|
+
}
|
|
131
|
+
if (line.includes('Suspense') && (line.includes('import') || line.includes('React.Suspense'))) {
|
|
132
|
+
hasSuspenseImport = true;
|
|
133
|
+
}
|
|
134
|
+
if (line.includes('ErrorBoundary') && line.includes('import')) {
|
|
135
|
+
hasErrorBoundaryImport = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// If file has lazy imports but no Suspense import, flag it
|
|
139
|
+
if (hasLazyImport && !hasSuspenseImport) {
|
|
140
|
+
smells.push({
|
|
141
|
+
type: 'missing-error-boundary',
|
|
142
|
+
severity: 'warning',
|
|
143
|
+
message: 'File uses React.lazy but does not import Suspense',
|
|
144
|
+
file: filePath,
|
|
145
|
+
line: 1,
|
|
146
|
+
column: 0,
|
|
147
|
+
suggestion: 'Import Suspense from React and wrap lazy components.',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// Check for fetch/axios without error handling patterns
|
|
151
|
+
const hasDataFetching = lines.some(line => line.includes('fetch(') ||
|
|
152
|
+
line.includes('axios.') ||
|
|
153
|
+
line.includes('useQuery') ||
|
|
154
|
+
line.includes('useSWR'));
|
|
155
|
+
if (hasDataFetching && !hasErrorBoundaryImport) {
|
|
156
|
+
smells.push({
|
|
157
|
+
type: 'missing-error-boundary',
|
|
158
|
+
severity: 'info',
|
|
159
|
+
message: 'File has data fetching but no ErrorBoundary import',
|
|
160
|
+
file: filePath,
|
|
161
|
+
line: 1,
|
|
162
|
+
column: 0,
|
|
163
|
+
suggestion: 'Consider using an ErrorBoundary to handle data fetching failures gracefully.',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return smells;
|
|
167
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect form-related anti-patterns and validation issues
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectFormIssues(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
7
|
+
/**
|
|
8
|
+
* Detect form-related patterns at file level
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectFormPatterns(sourceCode: string, filePath: string, config: DetectorConfig): CodeSmell[];
|
|
11
|
+
//# sourceMappingURL=formValidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formValidation.d.ts","sourceRoot":"","sources":["../../src/detectors/formValidation.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,gBAAgB,CAC9B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CA4Kb;AAwBD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CA4Bb"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* Detect form-related anti-patterns and validation issues
|
|
4
|
+
*/
|
|
5
|
+
export function detectFormIssues(component, filePath, sourceCode, config) {
|
|
6
|
+
if (!config.checkForms)
|
|
7
|
+
return [];
|
|
8
|
+
const smells = [];
|
|
9
|
+
// Track form elements and their attributes
|
|
10
|
+
const formElements = [];
|
|
11
|
+
const inputElements = [];
|
|
12
|
+
const labeledInputIds = new Set();
|
|
13
|
+
const inputIds = new Set();
|
|
14
|
+
component.path.traverse({
|
|
15
|
+
JSXElement(path) {
|
|
16
|
+
const openingElement = path.node.openingElement;
|
|
17
|
+
if (!t.isJSXIdentifier(openingElement.name))
|
|
18
|
+
return;
|
|
19
|
+
const elementName = openingElement.name.name.toLowerCase();
|
|
20
|
+
const loc = openingElement.loc;
|
|
21
|
+
const attributes = openingElement.attributes;
|
|
22
|
+
// Check form elements
|
|
23
|
+
if (elementName === 'form') {
|
|
24
|
+
const hasOnSubmit = attributes.some(attr => t.isJSXAttribute(attr) &&
|
|
25
|
+
t.isJSXIdentifier(attr.name) &&
|
|
26
|
+
attr.name.name === 'onSubmit');
|
|
27
|
+
const hasAction = attributes.some(attr => t.isJSXAttribute(attr) &&
|
|
28
|
+
t.isJSXIdentifier(attr.name) &&
|
|
29
|
+
attr.name.name === 'action');
|
|
30
|
+
formElements.push({
|
|
31
|
+
line: loc?.start.line || 0,
|
|
32
|
+
hasOnSubmit,
|
|
33
|
+
hasAction,
|
|
34
|
+
});
|
|
35
|
+
if (!hasOnSubmit && !hasAction) {
|
|
36
|
+
smells.push({
|
|
37
|
+
type: 'form-without-onsubmit',
|
|
38
|
+
severity: 'warning',
|
|
39
|
+
message: `Form without onSubmit handler in "${component.name}"`,
|
|
40
|
+
file: filePath,
|
|
41
|
+
line: loc?.start.line || 0,
|
|
42
|
+
column: loc?.start.column || 0,
|
|
43
|
+
suggestion: 'Add an onSubmit handler to handle form submission and prevent default behavior.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Check input elements
|
|
48
|
+
if (elementName === 'input' || elementName === 'textarea' || elementName === 'select') {
|
|
49
|
+
const hasValue = attributes.some(attr => t.isJSXAttribute(attr) &&
|
|
50
|
+
t.isJSXIdentifier(attr.name) &&
|
|
51
|
+
(attr.name.name === 'value' || attr.name.name === 'checked'));
|
|
52
|
+
const hasDefaultValue = attributes.some(attr => t.isJSXAttribute(attr) &&
|
|
53
|
+
t.isJSXIdentifier(attr.name) &&
|
|
54
|
+
(attr.name.name === 'defaultValue' || attr.name.name === 'defaultChecked'));
|
|
55
|
+
const hasOnChange = attributes.some(attr => t.isJSXAttribute(attr) &&
|
|
56
|
+
t.isJSXIdentifier(attr.name) &&
|
|
57
|
+
attr.name.name === 'onChange');
|
|
58
|
+
const hasName = attributes.some(attr => t.isJSXAttribute(attr) &&
|
|
59
|
+
t.isJSXIdentifier(attr.name) &&
|
|
60
|
+
attr.name.name === 'name');
|
|
61
|
+
// Get input type
|
|
62
|
+
let inputType = 'text';
|
|
63
|
+
const typeAttr = attributes.find(attr => t.isJSXAttribute(attr) &&
|
|
64
|
+
t.isJSXIdentifier(attr.name) &&
|
|
65
|
+
attr.name.name === 'type');
|
|
66
|
+
if (typeAttr && t.isJSXAttribute(typeAttr) && t.isStringLiteral(typeAttr.value)) {
|
|
67
|
+
inputType = typeAttr.value.value;
|
|
68
|
+
}
|
|
69
|
+
// Get input id for label checking
|
|
70
|
+
const idAttr = attributes.find(attr => t.isJSXAttribute(attr) &&
|
|
71
|
+
t.isJSXIdentifier(attr.name) &&
|
|
72
|
+
attr.name.name === 'id');
|
|
73
|
+
if (idAttr && t.isJSXAttribute(idAttr) && t.isStringLiteral(idAttr.value)) {
|
|
74
|
+
inputIds.add(idAttr.value.value);
|
|
75
|
+
}
|
|
76
|
+
inputElements.push({
|
|
77
|
+
line: loc?.start.line || 0,
|
|
78
|
+
hasValue,
|
|
79
|
+
hasOnChange,
|
|
80
|
+
hasName,
|
|
81
|
+
type: inputType,
|
|
82
|
+
});
|
|
83
|
+
// Check for uncontrolled inputs (no value or defaultValue)
|
|
84
|
+
if (!hasValue && !hasDefaultValue && !hasOnChange) {
|
|
85
|
+
// Skip hidden, submit, button, reset types
|
|
86
|
+
const skipTypes = ['hidden', 'submit', 'button', 'reset', 'image'];
|
|
87
|
+
if (!skipTypes.includes(inputType)) {
|
|
88
|
+
smells.push({
|
|
89
|
+
type: 'uncontrolled-form',
|
|
90
|
+
severity: 'info',
|
|
91
|
+
message: `Uncontrolled ${elementName} element in "${component.name}" - consider using controlled components`,
|
|
92
|
+
file: filePath,
|
|
93
|
+
line: loc?.start.line || 0,
|
|
94
|
+
column: loc?.start.column || 0,
|
|
95
|
+
suggestion: 'Use value and onChange for controlled components, or ref for uncontrolled access.',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Check for controlled input without onChange (will be read-only)
|
|
100
|
+
if (hasValue && !hasOnChange && !hasDefaultValue) {
|
|
101
|
+
smells.push({
|
|
102
|
+
type: 'uncontrolled-form',
|
|
103
|
+
severity: 'error',
|
|
104
|
+
message: `Input has value prop but no onChange handler in "${component.name}" - input will be read-only`,
|
|
105
|
+
file: filePath,
|
|
106
|
+
line: loc?.start.line || 0,
|
|
107
|
+
column: loc?.start.column || 0,
|
|
108
|
+
suggestion: 'Add an onChange handler or use defaultValue instead of value.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Check for label elements and collect their "for/htmlFor" attributes
|
|
113
|
+
if (elementName === 'label') {
|
|
114
|
+
const forAttr = attributes.find(attr => t.isJSXAttribute(attr) &&
|
|
115
|
+
t.isJSXIdentifier(attr.name) &&
|
|
116
|
+
(attr.name.name === 'htmlFor' || attr.name.name === 'for'));
|
|
117
|
+
if (forAttr && t.isJSXAttribute(forAttr) && t.isStringLiteral(forAttr.value)) {
|
|
118
|
+
labeledInputIds.add(forAttr.value.value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
// Check for inputs without associated labels
|
|
124
|
+
inputIds.forEach(id => {
|
|
125
|
+
if (!labeledInputIds.has(id)) {
|
|
126
|
+
// This is already handled by accessibility detector, but we note it for forms
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
// Check for missing form validation patterns
|
|
130
|
+
const hasValidationLibrary = hasFormValidation(sourceCode);
|
|
131
|
+
if (formElements.length > 0 && !hasValidationLibrary) {
|
|
132
|
+
smells.push({
|
|
133
|
+
type: 'missing-form-validation',
|
|
134
|
+
severity: 'info',
|
|
135
|
+
message: `Form in "${component.name}" without apparent validation library`,
|
|
136
|
+
file: filePath,
|
|
137
|
+
line: formElements[0].line,
|
|
138
|
+
column: 0,
|
|
139
|
+
suggestion: 'Consider using a form validation library like react-hook-form, formik, or yup for robust validation.',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return smells;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if code uses common form validation patterns/libraries
|
|
146
|
+
*/
|
|
147
|
+
function hasFormValidation(sourceCode) {
|
|
148
|
+
const validationPatterns = [
|
|
149
|
+
'useForm', // react-hook-form
|
|
150
|
+
'useFormik', // formik
|
|
151
|
+
'Formik', // formik component
|
|
152
|
+
'yup.', // yup validation
|
|
153
|
+
'zod.', // zod validation
|
|
154
|
+
'.validate(', // custom validation
|
|
155
|
+
'validator.', // validator.js
|
|
156
|
+
'Joi.', // joi validation
|
|
157
|
+
'required:', // validation rules
|
|
158
|
+
'pattern:', // regex validation
|
|
159
|
+
'minLength:', // length validation
|
|
160
|
+
'schema.validate', // schema validation
|
|
161
|
+
];
|
|
162
|
+
return validationPatterns.some(pattern => sourceCode.includes(pattern));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Detect form-related patterns at file level
|
|
166
|
+
*/
|
|
167
|
+
export function detectFormPatterns(sourceCode, filePath, config) {
|
|
168
|
+
if (!config.checkForms)
|
|
169
|
+
return [];
|
|
170
|
+
const smells = [];
|
|
171
|
+
const lines = sourceCode.split('\n');
|
|
172
|
+
// Check for form submission without preventDefault
|
|
173
|
+
lines.forEach((line, index) => {
|
|
174
|
+
// Pattern: onSubmit={(e) => { ... }} without e.preventDefault()
|
|
175
|
+
if (line.includes('onSubmit') && line.includes('=>')) {
|
|
176
|
+
// Look ahead for preventDefault in nearby lines
|
|
177
|
+
const nextLines = lines.slice(index, index + 5).join('\n');
|
|
178
|
+
if (!nextLines.includes('preventDefault')) {
|
|
179
|
+
smells.push({
|
|
180
|
+
type: 'form-without-onsubmit',
|
|
181
|
+
severity: 'warning',
|
|
182
|
+
message: 'Form onSubmit handler may be missing e.preventDefault()',
|
|
183
|
+
file: filePath,
|
|
184
|
+
line: index + 1,
|
|
185
|
+
column: 0,
|
|
186
|
+
suggestion: 'Call e.preventDefault() in form submit handlers to prevent default page reload.',
|
|
187
|
+
codeSnippet: line.trim(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return smells;
|
|
193
|
+
}
|
|
@@ -21,4 +21,9 @@ export { detectMemoryLeaks } from './memoryLeak.js';
|
|
|
21
21
|
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
22
22
|
export { detectUnusedCode } from './unusedCode.js';
|
|
23
23
|
export { detectServerComponentIssues, detectAsyncComponentIssues } from './serverComponents.js';
|
|
24
|
+
export { detectContextIssues, detectContextProviderIssues } from './contextApi.js';
|
|
25
|
+
export { detectErrorBoundaryIssues, detectMissingErrorBoundaries } from './errorBoundary.js';
|
|
26
|
+
export { detectFormIssues, detectFormPatterns } from './formValidation.js';
|
|
27
|
+
export { detectStateManagementIssues, detectStateManagementPatterns } from './stateManagement.js';
|
|
28
|
+
export { detectTestingGaps, generateTestSuggestions } from './testingGaps.js';
|
|
24
29
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAEhG,OAAO,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,EAAE,yBAAyB,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAE7F,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE3E,OAAO,EAAE,2BAA2B,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAElG,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/detectors/index.js
CHANGED
|
@@ -25,3 +25,13 @@ export { detectImportIssues, analyzeImports } from './imports.js';
|
|
|
25
25
|
export { detectUnusedCode } from './unusedCode.js';
|
|
26
26
|
// Server Components (React 19)
|
|
27
27
|
export { detectServerComponentIssues, detectAsyncComponentIssues } from './serverComponents.js';
|
|
28
|
+
// Context API Analysis
|
|
29
|
+
export { detectContextIssues, detectContextProviderIssues } from './contextApi.js';
|
|
30
|
+
// Error Boundary Detection
|
|
31
|
+
export { detectErrorBoundaryIssues, detectMissingErrorBoundaries } from './errorBoundary.js';
|
|
32
|
+
// Form Validation
|
|
33
|
+
export { detectFormIssues, detectFormPatterns } from './formValidation.js';
|
|
34
|
+
// State Management
|
|
35
|
+
export { detectStateManagementIssues, detectStateManagementPatterns } from './stateManagement.js';
|
|
36
|
+
// Testing Gaps
|
|
37
|
+
export { detectTestingGaps, generateTestSuggestions } from './testingGaps.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect state management anti-patterns (Redux, Zustand, MobX, etc.)
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectStateManagementIssues(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
7
|
+
/**
|
|
8
|
+
* Detect file-level state management patterns
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectStateManagementPatterns(sourceCode: string, filePath: string, config: DetectorConfig): CodeSmell[];
|
|
11
|
+
//# sourceMappingURL=stateManagement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stateManagement.d.ts","sourceRoot":"","sources":["../../src/detectors/stateManagement.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,2BAA2B,CACzC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CA6Hb;AAiDD;;GAEG;AACH,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAyCb"}
|