react-code-smell-detector 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +227 -22
- package/dist/__tests__/aiRefactoring.test.d.ts +2 -0
- package/dist/__tests__/aiRefactoring.test.d.ts.map +1 -0
- package/dist/__tests__/aiRefactoring.test.js +86 -0
- package/dist/__tests__/analyzer-real.test.d.ts +2 -0
- package/dist/__tests__/analyzer-real.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer-real.test.js +149 -0
- package/dist/__tests__/analyzer.test.d.ts +2 -0
- package/dist/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer.test.js +173 -0
- package/dist/__tests__/baseline.test.d.ts +2 -0
- package/dist/__tests__/baseline.test.d.ts.map +1 -0
- package/dist/__tests__/baseline.test.js +136 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts +2 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts.map +1 -0
- package/dist/__tests__/bundleAnalyzer.test.js +182 -0
- package/dist/__tests__/customRules.test.d.ts +2 -0
- package/dist/__tests__/customRules.test.d.ts.map +1 -0
- package/dist/__tests__/customRules.test.js +283 -0
- package/dist/__tests__/detectors/index.test.d.ts +2 -0
- package/dist/__tests__/detectors/index.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/index.test.js +1012 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts +2 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/newDetectors.test.js +333 -0
- package/dist/__tests__/docGenerator.test.d.ts +2 -0
- package/dist/__tests__/docGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/docGenerator.test.js +157 -0
- package/dist/__tests__/fixer.test.d.ts +2 -0
- package/dist/__tests__/fixer.test.d.ts.map +1 -0
- package/dist/__tests__/fixer.test.js +193 -0
- package/dist/__tests__/git.test.d.ts +2 -0
- package/dist/__tests__/git.test.d.ts.map +1 -0
- package/dist/__tests__/git.test.js +38 -0
- package/dist/__tests__/graphGenerator.test.d.ts +2 -0
- package/dist/__tests__/graphGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/graphGenerator.test.js +190 -0
- package/dist/__tests__/htmlReporter.test.d.ts +2 -0
- package/dist/__tests__/htmlReporter.test.d.ts.map +1 -0
- package/dist/__tests__/htmlReporter.test.js +258 -0
- package/dist/__tests__/interactiveFixer.test.d.ts +2 -0
- package/dist/__tests__/interactiveFixer.test.d.ts.map +1 -0
- package/dist/__tests__/interactiveFixer.test.js +231 -0
- package/dist/__tests__/parser.test.d.ts +2 -0
- package/dist/__tests__/parser.test.d.ts.map +1 -0
- package/dist/__tests__/parser.test.js +56 -0
- package/dist/__tests__/performanceBudget.test.d.ts +2 -0
- package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
- package/dist/__tests__/performanceBudget.test.js +242 -0
- package/dist/__tests__/prComments.test.d.ts +2 -0
- package/dist/__tests__/prComments.test.d.ts.map +1 -0
- package/dist/__tests__/prComments.test.js +118 -0
- package/dist/__tests__/reporter.test.d.ts +2 -0
- package/dist/__tests__/reporter.test.d.ts.map +1 -0
- package/dist/__tests__/reporter.test.js +136 -0
- package/dist/__tests__/watcher.test.d.ts +2 -0
- package/dist/__tests__/watcher.test.d.ts.map +1 -0
- package/dist/__tests__/watcher.test.js +161 -0
- package/dist/__tests__/webhooks.test.d.ts +2 -0
- package/dist/__tests__/webhooks.test.d.ts.map +1 -0
- package/dist/__tests__/webhooks.test.js +209 -0
- package/dist/aiRefactoring.d.ts +29 -0
- package/dist/aiRefactoring.d.ts.map +1 -0
- package/dist/aiRefactoring.js +290 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +33 -1
- package/dist/cli.js +123 -1
- package/dist/detectors/contextApi.d.ts +11 -0
- package/dist/detectors/contextApi.d.ts.map +1 -0
- package/dist/detectors/contextApi.js +151 -0
- package/dist/detectors/errorBoundary.d.ts +11 -0
- package/dist/detectors/errorBoundary.d.ts.map +1 -0
- package/dist/detectors/errorBoundary.js +167 -0
- package/dist/detectors/formValidation.d.ts +11 -0
- package/dist/detectors/formValidation.d.ts.map +1 -0
- package/dist/detectors/formValidation.js +193 -0
- package/dist/detectors/index.d.ts +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +12 -0
- package/dist/detectors/serverComponents.d.ts +11 -0
- package/dist/detectors/serverComponents.d.ts.map +1 -0
- package/dist/detectors/serverComponents.js +222 -0
- package/dist/detectors/stateManagement.d.ts +11 -0
- package/dist/detectors/stateManagement.d.ts.map +1 -0
- package/dist/detectors/stateManagement.js +193 -0
- package/dist/detectors/testingGaps.d.ts +15 -0
- package/dist/detectors/testingGaps.d.ts.map +1 -0
- package/dist/detectors/testingGaps.js +182 -0
- package/dist/docGenerator.d.ts +37 -0
- package/dist/docGenerator.d.ts.map +1 -0
- package/dist/docGenerator.js +306 -0
- package/dist/guide.d.ts +9 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +922 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/interactiveFixer.d.ts +20 -0
- package/dist/interactiveFixer.d.ts.map +1 -0
- package/dist/interactiveFixer.js +178 -0
- package/dist/performanceBudget.d.ts +54 -0
- package/dist/performanceBudget.d.ts.map +1 -0
- package/dist/performanceBudget.js +218 -0
- package/dist/prComments.d.ts +47 -0
- package/dist/prComments.d.ts.map +1 -0
- package/dist/prComments.js +233 -0
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +18 -0
- package/package.json +10 -4
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.
|
|
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"}
|