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.
Files changed (110) hide show
  1. package/README.md +227 -22
  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__/parser.test.d.ts +2 -0
  45. package/dist/__tests__/parser.test.d.ts.map +1 -0
  46. package/dist/__tests__/parser.test.js +56 -0
  47. package/dist/__tests__/performanceBudget.test.d.ts +2 -0
  48. package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
  49. package/dist/__tests__/performanceBudget.test.js +242 -0
  50. package/dist/__tests__/prComments.test.d.ts +2 -0
  51. package/dist/__tests__/prComments.test.d.ts.map +1 -0
  52. package/dist/__tests__/prComments.test.js +118 -0
  53. package/dist/__tests__/reporter.test.d.ts +2 -0
  54. package/dist/__tests__/reporter.test.d.ts.map +1 -0
  55. package/dist/__tests__/reporter.test.js +136 -0
  56. package/dist/__tests__/watcher.test.d.ts +2 -0
  57. package/dist/__tests__/watcher.test.d.ts.map +1 -0
  58. package/dist/__tests__/watcher.test.js +161 -0
  59. package/dist/__tests__/webhooks.test.d.ts +2 -0
  60. package/dist/__tests__/webhooks.test.d.ts.map +1 -0
  61. package/dist/__tests__/webhooks.test.js +209 -0
  62. package/dist/aiRefactoring.d.ts +29 -0
  63. package/dist/aiRefactoring.d.ts.map +1 -0
  64. package/dist/aiRefactoring.js +290 -0
  65. package/dist/analyzer.d.ts.map +1 -1
  66. package/dist/analyzer.js +33 -1
  67. package/dist/cli.js +123 -1
  68. package/dist/detectors/contextApi.d.ts +11 -0
  69. package/dist/detectors/contextApi.d.ts.map +1 -0
  70. package/dist/detectors/contextApi.js +151 -0
  71. package/dist/detectors/errorBoundary.d.ts +11 -0
  72. package/dist/detectors/errorBoundary.d.ts.map +1 -0
  73. package/dist/detectors/errorBoundary.js +167 -0
  74. package/dist/detectors/formValidation.d.ts +11 -0
  75. package/dist/detectors/formValidation.d.ts.map +1 -0
  76. package/dist/detectors/formValidation.js +193 -0
  77. package/dist/detectors/index.d.ts +6 -0
  78. package/dist/detectors/index.d.ts.map +1 -1
  79. package/dist/detectors/index.js +12 -0
  80. package/dist/detectors/serverComponents.d.ts +11 -0
  81. package/dist/detectors/serverComponents.d.ts.map +1 -0
  82. package/dist/detectors/serverComponents.js +222 -0
  83. package/dist/detectors/stateManagement.d.ts +11 -0
  84. package/dist/detectors/stateManagement.d.ts.map +1 -0
  85. package/dist/detectors/stateManagement.js +193 -0
  86. package/dist/detectors/testingGaps.d.ts +15 -0
  87. package/dist/detectors/testingGaps.d.ts.map +1 -0
  88. package/dist/detectors/testingGaps.js +182 -0
  89. package/dist/docGenerator.d.ts +37 -0
  90. package/dist/docGenerator.d.ts.map +1 -0
  91. package/dist/docGenerator.js +306 -0
  92. package/dist/guide.d.ts +9 -0
  93. package/dist/guide.d.ts.map +1 -0
  94. package/dist/guide.js +922 -0
  95. package/dist/index.d.ts +5 -0
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +5 -0
  98. package/dist/interactiveFixer.d.ts +20 -0
  99. package/dist/interactiveFixer.d.ts.map +1 -0
  100. package/dist/interactiveFixer.js +178 -0
  101. package/dist/performanceBudget.d.ts +54 -0
  102. package/dist/performanceBudget.d.ts.map +1 -0
  103. package/dist/performanceBudget.js +218 -0
  104. package/dist/prComments.d.ts +47 -0
  105. package/dist/prComments.d.ts.map +1 -0
  106. package/dist/prComments.js +233 -0
  107. package/dist/types/index.d.ts +12 -1
  108. package/dist/types/index.d.ts.map +1 -1
  109. package/dist/types/index.js +18 -0
  110. package/package.json +10 -4
package/dist/cli.js CHANGED
@@ -13,12 +13,17 @@ import { initializeBaseline, recordBaseline, formatTrendReport } from './baselin
13
13
  import { sendWebhookNotification, getWebhookConfig } from './webhooks.js';
14
14
  import { generateDependencyGraphHTML } from './graphGenerator.js';
15
15
  import { generateBundleReport } from './bundleAnalyzer.js';
16
+ import { runInteractiveFix, previewFixes } from './interactiveFixer.js';
17
+ import { generatePRComment, postPRComment, parseGitHubInfo, getPRNumber } from './prComments.js';
18
+ import { loadBudget, checkBudget, formatBudgetReport, createBudgetConfig } from './performanceBudget.js';
19
+ import { writeComponentDocs } from './docGenerator.js';
20
+ import { runGuide, createDemoProject } from './guide.js';
16
21
  import fs from 'fs/promises';
17
22
  const program = new Command();
18
23
  program
19
24
  .name('react-smell')
20
25
  .description('Detect code smells in React projects')
21
- .version('1.3.0')
26
+ .version('1.5.0')
22
27
  .argument('[directory]', 'Directory to analyze', '.')
23
28
  .option('-f, --format <format>', 'Output format: console, json, markdown, html', 'console')
24
29
  .option('-s, --snippets', 'Show code snippets in output', false)
@@ -43,6 +48,13 @@ program
43
48
  .option('--graph-format <format>', 'Graph output format: svg, html', 'html')
44
49
  .option('--bundle', 'Analyze bundle size impact per component', false)
45
50
  .option('--rules <file>', 'Custom rules configuration file')
51
+ .option('--fix-interactive', 'Interactive fix mode: review and apply fixes one by one')
52
+ .option('--fix-preview', 'Preview fixable issues without applying')
53
+ .option('--pr-comment', 'Generate PR comment (for GitHub Actions)')
54
+ .option('--budget', 'Check against performance budget')
55
+ .option('--budget-config <file>', 'Path to budget config file')
56
+ .option('--docs', 'Generate component documentation')
57
+ .option('--docs-format <format>', 'Documentation format: markdown, html, json', 'markdown')
46
58
  .action(async (directory, options) => {
47
59
  const rootDir = path.resolve(process.cwd(), directory);
48
60
  // Check if directory exists
@@ -166,6 +178,59 @@ program
166
178
  console.log(chalk.yellow('\nNo auto-fixable issues found\n'));
167
179
  }
168
180
  }
181
+ // Interactive fix mode
182
+ if (options.fixInteractive) {
183
+ const allSmells = result.files.flatMap(f => f.smells);
184
+ await runInteractiveFix({ smells: allSmells, rootDir, showDiff: true });
185
+ }
186
+ // Fix preview mode
187
+ if (options.fixPreview) {
188
+ const allSmells = result.files.flatMap(f => f.smells);
189
+ previewFixes(allSmells, rootDir);
190
+ }
191
+ // Performance budget check
192
+ if (options.budget) {
193
+ const budget = await loadBudget(options.budgetConfig);
194
+ const budgetResult = checkBudget(result, budget);
195
+ console.log(formatBudgetReport(budgetResult));
196
+ if (!budgetResult.passed && options.ci) {
197
+ process.exit(1);
198
+ }
199
+ }
200
+ // Generate documentation
201
+ if (options.docs) {
202
+ const docsPath = await writeComponentDocs(result, rootDir, {
203
+ format: options.docsFormat || 'markdown',
204
+ includeSmells: true,
205
+ includeMetrics: true,
206
+ groupByFolder: true,
207
+ });
208
+ console.log(chalk.green(`✓ Component documentation written to ${docsPath}`));
209
+ }
210
+ // PR comment generation (for GitHub Actions)
211
+ if (options.prComment) {
212
+ const comment = generatePRComment(result, rootDir);
213
+ // Try to post to GitHub if in Actions environment
214
+ const ghToken = process.env.GITHUB_TOKEN;
215
+ const ghInfo = parseGitHubInfo();
216
+ const prNumber = getPRNumber();
217
+ if (ghToken && ghInfo && prNumber) {
218
+ const posted = await postPRComment({
219
+ token: ghToken,
220
+ owner: ghInfo.owner,
221
+ repo: ghInfo.repo,
222
+ prNumber,
223
+ }, comment);
224
+ if (posted) {
225
+ console.log(chalk.green('✓ PR comment posted successfully'));
226
+ }
227
+ }
228
+ else {
229
+ // Output comment to console/file for manual use
230
+ console.log(chalk.cyan('\n📝 PR Comment (copy to GitHub):\n'));
231
+ console.log(comment);
232
+ }
233
+ }
169
234
  let output;
170
235
  if (options.format === 'html') {
171
236
  output = generateHTMLReport(result, rootDir);
@@ -285,4 +350,61 @@ program
285
350
  console.log(chalk.green('✓ Created .smellrc.json'));
286
351
  }
287
352
  });
353
+ // Init budget command
354
+ program
355
+ .command('init-budget')
356
+ .description('Create a performance budget configuration file')
357
+ .action(async () => {
358
+ try {
359
+ const budgetPath = await createBudgetConfig();
360
+ console.log(chalk.green(`✓ Created performance budget config at ${budgetPath}`));
361
+ }
362
+ catch (error) {
363
+ console.error(chalk.red(`Error: ${error.message}`));
364
+ process.exit(1);
365
+ }
366
+ });
367
+ // Generate docs command
368
+ program
369
+ .command('docs')
370
+ .description('Generate component documentation')
371
+ .argument('[directory]', 'Directory to analyze', '.')
372
+ .option('-f, --format <format>', 'Output format: markdown, html, json', 'markdown')
373
+ .option('-o, --output <dir>', 'Output directory')
374
+ .action(async (directory, options) => {
375
+ const rootDir = path.resolve(process.cwd(), directory);
376
+ const spinner = ora('Generating documentation...').start();
377
+ try {
378
+ const result = await analyzeProject({ rootDir });
379
+ spinner.stop();
380
+ const docsPath = await writeComponentDocs(result, rootDir, {
381
+ format: options.format,
382
+ outputDir: options.output,
383
+ includeSmells: true,
384
+ includeMetrics: true,
385
+ groupByFolder: true,
386
+ });
387
+ console.log(chalk.green(`✓ Documentation written to ${docsPath}`));
388
+ }
389
+ catch (error) {
390
+ spinner.fail(`Documentation generation failed: ${error.message}`);
391
+ process.exit(1);
392
+ }
393
+ });
394
+ // Interactive guide command
395
+ program
396
+ .command('guide')
397
+ .description('Interactive guide to using react-smell with examples')
398
+ .action(async () => {
399
+ await runGuide();
400
+ });
401
+ // Demo project command
402
+ program
403
+ .command('demo')
404
+ .description('Create a demo project with example code smells')
405
+ .argument('[directory]', 'Directory to create demo in', '.')
406
+ .action(async (directory) => {
407
+ const targetDir = path.resolve(process.cwd(), directory);
408
+ await createDemoProject(targetDir);
409
+ });
288
410
  program.parse();
@@ -0,0 +1,11 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detect Context API anti-patterns and performance issues
5
+ */
6
+ export declare function detectContextIssues(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
7
+ /**
8
+ * Detect Context Provider issues
9
+ */
10
+ export declare function detectContextProviderIssues(sourceCode: string, filePath: string, config: DetectorConfig): CodeSmell[];
11
+ //# sourceMappingURL=contextApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contextApi.d.ts","sourceRoot":"","sources":["../../src/detectors/contextApi.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,mBAAmB,CACjC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAkGb;AAuBD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAwCb"}
@@ -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"}