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.
Files changed (87) hide show
  1. package/README.md +241 -4
  2. package/dist/__tests__/aiRefactoring.test.d.ts +2 -0
  3. package/dist/__tests__/aiRefactoring.test.d.ts.map +1 -0
  4. package/dist/__tests__/aiRefactoring.test.js +86 -0
  5. package/dist/__tests__/analyzer-real.test.d.ts +2 -0
  6. package/dist/__tests__/analyzer-real.test.d.ts.map +1 -0
  7. package/dist/__tests__/analyzer-real.test.js +149 -0
  8. package/dist/__tests__/analyzer.test.d.ts +2 -0
  9. package/dist/__tests__/analyzer.test.d.ts.map +1 -0
  10. package/dist/__tests__/analyzer.test.js +173 -0
  11. package/dist/__tests__/baseline.test.d.ts +2 -0
  12. package/dist/__tests__/baseline.test.d.ts.map +1 -0
  13. package/dist/__tests__/baseline.test.js +136 -0
  14. package/dist/__tests__/bundleAnalyzer.test.d.ts +2 -0
  15. package/dist/__tests__/bundleAnalyzer.test.d.ts.map +1 -0
  16. package/dist/__tests__/bundleAnalyzer.test.js +182 -0
  17. package/dist/__tests__/customRules.test.d.ts +2 -0
  18. package/dist/__tests__/customRules.test.d.ts.map +1 -0
  19. package/dist/__tests__/customRules.test.js +283 -0
  20. package/dist/__tests__/detectors/index.test.d.ts +2 -0
  21. package/dist/__tests__/detectors/index.test.d.ts.map +1 -0
  22. package/dist/__tests__/detectors/index.test.js +1012 -0
  23. package/dist/__tests__/detectors/newDetectors.test.d.ts +2 -0
  24. package/dist/__tests__/detectors/newDetectors.test.d.ts.map +1 -0
  25. package/dist/__tests__/detectors/newDetectors.test.js +333 -0
  26. package/dist/__tests__/docGenerator.test.d.ts +2 -0
  27. package/dist/__tests__/docGenerator.test.d.ts.map +1 -0
  28. package/dist/__tests__/docGenerator.test.js +157 -0
  29. package/dist/__tests__/fixer.test.d.ts +2 -0
  30. package/dist/__tests__/fixer.test.d.ts.map +1 -0
  31. package/dist/__tests__/fixer.test.js +193 -0
  32. package/dist/__tests__/git.test.d.ts +2 -0
  33. package/dist/__tests__/git.test.d.ts.map +1 -0
  34. package/dist/__tests__/git.test.js +38 -0
  35. package/dist/__tests__/graphGenerator.test.d.ts +2 -0
  36. package/dist/__tests__/graphGenerator.test.d.ts.map +1 -0
  37. package/dist/__tests__/graphGenerator.test.js +190 -0
  38. package/dist/__tests__/htmlReporter.test.d.ts +2 -0
  39. package/dist/__tests__/htmlReporter.test.d.ts.map +1 -0
  40. package/dist/__tests__/htmlReporter.test.js +258 -0
  41. package/dist/__tests__/interactiveFixer.test.d.ts +2 -0
  42. package/dist/__tests__/interactiveFixer.test.d.ts.map +1 -0
  43. package/dist/__tests__/interactiveFixer.test.js +231 -0
  44. package/dist/__tests__/performanceBudget.test.js +195 -44
  45. package/dist/__tests__/reporter.test.d.ts +2 -0
  46. package/dist/__tests__/reporter.test.d.ts.map +1 -0
  47. package/dist/__tests__/reporter.test.js +136 -0
  48. package/dist/__tests__/watcher.test.d.ts +2 -0
  49. package/dist/__tests__/watcher.test.d.ts.map +1 -0
  50. package/dist/__tests__/watcher.test.js +161 -0
  51. package/dist/__tests__/webhooks.test.d.ts +2 -0
  52. package/dist/__tests__/webhooks.test.d.ts.map +1 -0
  53. package/dist/__tests__/webhooks.test.js +209 -0
  54. package/dist/aiRefactoring.d.ts +29 -0
  55. package/dist/aiRefactoring.d.ts.map +1 -0
  56. package/dist/aiRefactoring.js +290 -0
  57. package/dist/analyzer.d.ts.map +1 -1
  58. package/dist/analyzer.js +23 -0
  59. package/dist/cli.js +17 -0
  60. package/dist/detectors/contextApi.d.ts +11 -0
  61. package/dist/detectors/contextApi.d.ts.map +1 -0
  62. package/dist/detectors/contextApi.js +151 -0
  63. package/dist/detectors/errorBoundary.d.ts +11 -0
  64. package/dist/detectors/errorBoundary.d.ts.map +1 -0
  65. package/dist/detectors/errorBoundary.js +167 -0
  66. package/dist/detectors/formValidation.d.ts +11 -0
  67. package/dist/detectors/formValidation.d.ts.map +1 -0
  68. package/dist/detectors/formValidation.js +193 -0
  69. package/dist/detectors/index.d.ts +5 -0
  70. package/dist/detectors/index.d.ts.map +1 -1
  71. package/dist/detectors/index.js +10 -0
  72. package/dist/detectors/stateManagement.d.ts +11 -0
  73. package/dist/detectors/stateManagement.d.ts.map +1 -0
  74. package/dist/detectors/stateManagement.js +193 -0
  75. package/dist/detectors/testingGaps.d.ts +15 -0
  76. package/dist/detectors/testingGaps.d.ts.map +1 -0
  77. package/dist/detectors/testingGaps.js +182 -0
  78. package/dist/guide.d.ts +9 -0
  79. package/dist/guide.d.ts.map +1 -0
  80. package/dist/guide.js +922 -0
  81. package/dist/index.d.ts +1 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1 -0
  84. package/dist/types/index.d.ts +11 -1
  85. package/dist/types/index.d.ts.map +1 -1
  86. package/dist/types/index.js +16 -0
  87. 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"}
@@ -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"}