react-code-smell-detector 1.4.2 → 1.5.0
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 +207 -22
- 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 +91 -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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +10 -1
- package/dist/cli.js +106 -1
- package/dist/detectors/index.d.ts +1 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +2 -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/docGenerator.d.ts +37 -0
- package/dist/docGenerator.d.ts.map +1 -0
- package/dist/docGenerator.js +306 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/package.json +10 -4
package/dist/cli.js
CHANGED
|
@@ -13,12 +13,16 @@ 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';
|
|
16
20
|
import fs from 'fs/promises';
|
|
17
21
|
const program = new Command();
|
|
18
22
|
program
|
|
19
23
|
.name('react-smell')
|
|
20
24
|
.description('Detect code smells in React projects')
|
|
21
|
-
.version('1.
|
|
25
|
+
.version('1.5.0')
|
|
22
26
|
.argument('[directory]', 'Directory to analyze', '.')
|
|
23
27
|
.option('-f, --format <format>', 'Output format: console, json, markdown, html', 'console')
|
|
24
28
|
.option('-s, --snippets', 'Show code snippets in output', false)
|
|
@@ -43,6 +47,13 @@ program
|
|
|
43
47
|
.option('--graph-format <format>', 'Graph output format: svg, html', 'html')
|
|
44
48
|
.option('--bundle', 'Analyze bundle size impact per component', false)
|
|
45
49
|
.option('--rules <file>', 'Custom rules configuration file')
|
|
50
|
+
.option('--fix-interactive', 'Interactive fix mode: review and apply fixes one by one')
|
|
51
|
+
.option('--fix-preview', 'Preview fixable issues without applying')
|
|
52
|
+
.option('--pr-comment', 'Generate PR comment (for GitHub Actions)')
|
|
53
|
+
.option('--budget', 'Check against performance budget')
|
|
54
|
+
.option('--budget-config <file>', 'Path to budget config file')
|
|
55
|
+
.option('--docs', 'Generate component documentation')
|
|
56
|
+
.option('--docs-format <format>', 'Documentation format: markdown, html, json', 'markdown')
|
|
46
57
|
.action(async (directory, options) => {
|
|
47
58
|
const rootDir = path.resolve(process.cwd(), directory);
|
|
48
59
|
// Check if directory exists
|
|
@@ -166,6 +177,59 @@ program
|
|
|
166
177
|
console.log(chalk.yellow('\nNo auto-fixable issues found\n'));
|
|
167
178
|
}
|
|
168
179
|
}
|
|
180
|
+
// Interactive fix mode
|
|
181
|
+
if (options.fixInteractive) {
|
|
182
|
+
const allSmells = result.files.flatMap(f => f.smells);
|
|
183
|
+
await runInteractiveFix({ smells: allSmells, rootDir, showDiff: true });
|
|
184
|
+
}
|
|
185
|
+
// Fix preview mode
|
|
186
|
+
if (options.fixPreview) {
|
|
187
|
+
const allSmells = result.files.flatMap(f => f.smells);
|
|
188
|
+
previewFixes(allSmells, rootDir);
|
|
189
|
+
}
|
|
190
|
+
// Performance budget check
|
|
191
|
+
if (options.budget) {
|
|
192
|
+
const budget = await loadBudget(options.budgetConfig);
|
|
193
|
+
const budgetResult = checkBudget(result, budget);
|
|
194
|
+
console.log(formatBudgetReport(budgetResult));
|
|
195
|
+
if (!budgetResult.passed && options.ci) {
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Generate documentation
|
|
200
|
+
if (options.docs) {
|
|
201
|
+
const docsPath = await writeComponentDocs(result, rootDir, {
|
|
202
|
+
format: options.docsFormat || 'markdown',
|
|
203
|
+
includeSmells: true,
|
|
204
|
+
includeMetrics: true,
|
|
205
|
+
groupByFolder: true,
|
|
206
|
+
});
|
|
207
|
+
console.log(chalk.green(`✓ Component documentation written to ${docsPath}`));
|
|
208
|
+
}
|
|
209
|
+
// PR comment generation (for GitHub Actions)
|
|
210
|
+
if (options.prComment) {
|
|
211
|
+
const comment = generatePRComment(result, rootDir);
|
|
212
|
+
// Try to post to GitHub if in Actions environment
|
|
213
|
+
const ghToken = process.env.GITHUB_TOKEN;
|
|
214
|
+
const ghInfo = parseGitHubInfo();
|
|
215
|
+
const prNumber = getPRNumber();
|
|
216
|
+
if (ghToken && ghInfo && prNumber) {
|
|
217
|
+
const posted = await postPRComment({
|
|
218
|
+
token: ghToken,
|
|
219
|
+
owner: ghInfo.owner,
|
|
220
|
+
repo: ghInfo.repo,
|
|
221
|
+
prNumber,
|
|
222
|
+
}, comment);
|
|
223
|
+
if (posted) {
|
|
224
|
+
console.log(chalk.green('✓ PR comment posted successfully'));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Output comment to console/file for manual use
|
|
229
|
+
console.log(chalk.cyan('\n📝 PR Comment (copy to GitHub):\n'));
|
|
230
|
+
console.log(comment);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
169
233
|
let output;
|
|
170
234
|
if (options.format === 'html') {
|
|
171
235
|
output = generateHTMLReport(result, rootDir);
|
|
@@ -285,4 +349,45 @@ program
|
|
|
285
349
|
console.log(chalk.green('✓ Created .smellrc.json'));
|
|
286
350
|
}
|
|
287
351
|
});
|
|
352
|
+
// Init budget command
|
|
353
|
+
program
|
|
354
|
+
.command('init-budget')
|
|
355
|
+
.description('Create a performance budget configuration file')
|
|
356
|
+
.action(async () => {
|
|
357
|
+
try {
|
|
358
|
+
const budgetPath = await createBudgetConfig();
|
|
359
|
+
console.log(chalk.green(`✓ Created performance budget config at ${budgetPath}`));
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
// Generate docs command
|
|
367
|
+
program
|
|
368
|
+
.command('docs')
|
|
369
|
+
.description('Generate component documentation')
|
|
370
|
+
.argument('[directory]', 'Directory to analyze', '.')
|
|
371
|
+
.option('-f, --format <format>', 'Output format: markdown, html, json', 'markdown')
|
|
372
|
+
.option('-o, --output <dir>', 'Output directory')
|
|
373
|
+
.action(async (directory, options) => {
|
|
374
|
+
const rootDir = path.resolve(process.cwd(), directory);
|
|
375
|
+
const spinner = ora('Generating documentation...').start();
|
|
376
|
+
try {
|
|
377
|
+
const result = await analyzeProject({ rootDir });
|
|
378
|
+
spinner.stop();
|
|
379
|
+
const docsPath = await writeComponentDocs(result, rootDir, {
|
|
380
|
+
format: options.format,
|
|
381
|
+
outputDir: options.output,
|
|
382
|
+
includeSmells: true,
|
|
383
|
+
includeMetrics: true,
|
|
384
|
+
groupByFolder: true,
|
|
385
|
+
});
|
|
386
|
+
console.log(chalk.green(`✓ Documentation written to ${docsPath}`));
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
spinner.fail(`Documentation generation failed: ${error.message}`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
288
393
|
program.parse();
|
|
@@ -20,4 +20,5 @@ export { detectComplexity } from './complexity.js';
|
|
|
20
20
|
export { detectMemoryLeaks } from './memoryLeak.js';
|
|
21
21
|
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
22
22
|
export { detectUnusedCode } from './unusedCode.js';
|
|
23
|
+
export { detectServerComponentIssues, detectAsyncComponentIssues } from './serverComponents.js';
|
|
23
24
|
//# 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"}
|
|
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"}
|
package/dist/detectors/index.js
CHANGED
|
@@ -23,3 +23,5 @@ export { detectComplexity } from './complexity.js';
|
|
|
23
23
|
export { detectMemoryLeaks } from './memoryLeak.js';
|
|
24
24
|
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
25
25
|
export { detectUnusedCode } from './unusedCode.js';
|
|
26
|
+
// Server Components (React 19)
|
|
27
|
+
export { detectServerComponentIssues, detectAsyncComponentIssues } from './serverComponents.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect React 19 Server/Client component boundary issues
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectServerComponentIssues(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig, imports?: string[]): CodeSmell[];
|
|
7
|
+
/**
|
|
8
|
+
* Detect proper async component patterns in React 19
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectAsyncComponentIssues(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
11
|
+
//# sourceMappingURL=serverComponents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverComponents.d.ts","sourceRoot":"","sources":["../../src/detectors/serverComponents.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAqC9D;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,MAAM,EAAO,GACrB,SAAS,EAAE,CA+Jb;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAmCb"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
// Hooks that are not allowed in Server Components
|
|
3
|
+
const CLIENT_ONLY_HOOKS = [
|
|
4
|
+
'useState',
|
|
5
|
+
'useReducer',
|
|
6
|
+
'useEffect',
|
|
7
|
+
'useLayoutEffect',
|
|
8
|
+
'useInsertionEffect',
|
|
9
|
+
'useRef',
|
|
10
|
+
'useImperativeHandle',
|
|
11
|
+
'useSyncExternalStore',
|
|
12
|
+
'useTransition',
|
|
13
|
+
'useDeferredValue',
|
|
14
|
+
'useOptimistic',
|
|
15
|
+
'useFormStatus',
|
|
16
|
+
'useActionState',
|
|
17
|
+
];
|
|
18
|
+
// Browser-only APIs
|
|
19
|
+
const BROWSER_APIS = [
|
|
20
|
+
'window',
|
|
21
|
+
'document',
|
|
22
|
+
'localStorage',
|
|
23
|
+
'sessionStorage',
|
|
24
|
+
'navigator',
|
|
25
|
+
'location',
|
|
26
|
+
'history',
|
|
27
|
+
'alert',
|
|
28
|
+
'confirm',
|
|
29
|
+
'prompt',
|
|
30
|
+
'fetch', // Can be used in Server Components but behaves differently
|
|
31
|
+
];
|
|
32
|
+
// Event handler props (indicate client interactivity)
|
|
33
|
+
const EVENT_HANDLER_PATTERN = /^on[A-Z]/;
|
|
34
|
+
/**
|
|
35
|
+
* Detect React 19 Server/Client component boundary issues
|
|
36
|
+
*/
|
|
37
|
+
export function detectServerComponentIssues(component, filePath, sourceCode, config, imports = []) {
|
|
38
|
+
if (!config.checkServerComponents)
|
|
39
|
+
return [];
|
|
40
|
+
const smells = [];
|
|
41
|
+
const lines = sourceCode.split('\n');
|
|
42
|
+
// Check if file has 'use client' or 'use server' directive
|
|
43
|
+
const hasUseClient = lines.some(line => /^['"]use client['"];?\s*$/.test(line.trim()));
|
|
44
|
+
const hasUseServer = lines.some(line => /^['"]use server['"];?\s*$/.test(line.trim()));
|
|
45
|
+
// Detect if this is a Server Component (no 'use client' directive in app/ directory)
|
|
46
|
+
const isInAppDir = filePath.includes('/app/') || filePath.includes('\\app\\');
|
|
47
|
+
const isServerComponent = isInAppDir && !hasUseClient;
|
|
48
|
+
if (isServerComponent) {
|
|
49
|
+
// Check for client-only hooks in Server Components
|
|
50
|
+
const clientHooksUsed = [];
|
|
51
|
+
for (const hookName of CLIENT_ONLY_HOOKS) {
|
|
52
|
+
const hooks = component.hooks[hookName];
|
|
53
|
+
if (hooks && hooks.length > 0) {
|
|
54
|
+
clientHooksUsed.push(hookName);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Also traverse for any hook calls
|
|
58
|
+
component.path.traverse({
|
|
59
|
+
CallExpression(path) {
|
|
60
|
+
const callee = path.node.callee;
|
|
61
|
+
if (t.isIdentifier(callee) && CLIENT_ONLY_HOOKS.includes(callee.name)) {
|
|
62
|
+
if (!clientHooksUsed.includes(callee.name)) {
|
|
63
|
+
clientHooksUsed.push(callee.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
if (clientHooksUsed.length > 0) {
|
|
69
|
+
smells.push({
|
|
70
|
+
type: 'server-component-hooks',
|
|
71
|
+
severity: 'error',
|
|
72
|
+
message: `Server Component "${component.name}" uses client-only hooks: ${clientHooksUsed.join(', ')}`,
|
|
73
|
+
file: filePath,
|
|
74
|
+
line: component.startLine,
|
|
75
|
+
column: 0,
|
|
76
|
+
suggestion: `Add 'use client' directive at the top of the file, or move stateful logic to a Client Component.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Check for event handlers in JSX (onClick, onChange, etc.)
|
|
80
|
+
const eventHandlers = [];
|
|
81
|
+
component.path.traverse({
|
|
82
|
+
JSXAttribute(path) {
|
|
83
|
+
if (t.isJSXIdentifier(path.node.name)) {
|
|
84
|
+
const name = path.node.name.name;
|
|
85
|
+
if (EVENT_HANDLER_PATTERN.test(name)) {
|
|
86
|
+
if (!eventHandlers.includes(name)) {
|
|
87
|
+
eventHandlers.push(name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (eventHandlers.length > 0) {
|
|
94
|
+
smells.push({
|
|
95
|
+
type: 'server-component-events',
|
|
96
|
+
severity: 'error',
|
|
97
|
+
message: `Server Component "${component.name}" uses event handlers: ${eventHandlers.join(', ')}`,
|
|
98
|
+
file: filePath,
|
|
99
|
+
line: component.startLine,
|
|
100
|
+
column: 0,
|
|
101
|
+
suggestion: `Add 'use client' directive or extract interactive elements to a Client Component.`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Check for browser APIs usage
|
|
105
|
+
const browserApisUsed = [];
|
|
106
|
+
component.path.traverse({
|
|
107
|
+
Identifier(path) {
|
|
108
|
+
const name = path.node.name;
|
|
109
|
+
if (BROWSER_APIS.includes(name)) {
|
|
110
|
+
// Make sure it's not a property access like obj.window
|
|
111
|
+
if (!t.isMemberExpression(path.parent) || !t.isIdentifier(path.parent.property) || path.parent.object === path.node) {
|
|
112
|
+
if (!browserApisUsed.includes(name)) {
|
|
113
|
+
browserApisUsed.push(name);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (browserApisUsed.length > 0) {
|
|
120
|
+
smells.push({
|
|
121
|
+
type: 'server-component-browser-api',
|
|
122
|
+
severity: 'warning',
|
|
123
|
+
message: `Server Component "${component.name}" may use browser APIs: ${browserApisUsed.join(', ')}`,
|
|
124
|
+
file: filePath,
|
|
125
|
+
line: component.startLine,
|
|
126
|
+
column: 0,
|
|
127
|
+
suggestion: `Browser APIs are not available in Server Components. Move to a Client Component or use conditional checks.`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Check for 'use server' in client components (async actions)
|
|
132
|
+
if (hasUseClient && hasUseServer) {
|
|
133
|
+
smells.push({
|
|
134
|
+
type: 'mixed-directives',
|
|
135
|
+
severity: 'error',
|
|
136
|
+
message: `File has both 'use client' and 'use server' directives`,
|
|
137
|
+
file: filePath,
|
|
138
|
+
line: 1,
|
|
139
|
+
column: 0,
|
|
140
|
+
suggestion: `A file can only be either a Client Component or contain Server Actions, not both. Separate them into different files.`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Check for async component without 'use server' actions (they should be in Server Components)
|
|
144
|
+
if (!isServerComponent && !hasUseServer) {
|
|
145
|
+
let hasAsyncServerAction = false;
|
|
146
|
+
component.path.traverse({
|
|
147
|
+
FunctionDeclaration(path) {
|
|
148
|
+
if (path.node.async) {
|
|
149
|
+
// Check if body starts with 'use server'
|
|
150
|
+
const body = path.node.body;
|
|
151
|
+
if (body.directives?.some(d => d.value.value === 'use server')) {
|
|
152
|
+
hasAsyncServerAction = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
ArrowFunctionExpression(path) {
|
|
157
|
+
if (path.node.async && t.isBlockStatement(path.node.body)) {
|
|
158
|
+
const directives = path.node.body.directives;
|
|
159
|
+
if (directives?.some(d => d.value.value === 'use server')) {
|
|
160
|
+
hasAsyncServerAction = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
// This is fine - Server Actions can be defined inline with 'use server' inside
|
|
166
|
+
}
|
|
167
|
+
// Check for passing server actions incorrectly to client components
|
|
168
|
+
// (This is complex to detect statically, so we provide guidance)
|
|
169
|
+
// Check for importing Server Component into Client Component (potential issue)
|
|
170
|
+
if (hasUseClient) {
|
|
171
|
+
// Look for imports that might be Server Components
|
|
172
|
+
const serverComponentImports = imports.filter(imp => {
|
|
173
|
+
// Heuristic: imports from app/ directory without 'use client'
|
|
174
|
+
return imp.includes('/app/') && !imp.includes('.client');
|
|
175
|
+
});
|
|
176
|
+
// Can't definitively detect, but can warn about patterns
|
|
177
|
+
}
|
|
178
|
+
return smells;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Detect proper async component patterns in React 19
|
|
182
|
+
*/
|
|
183
|
+
export function detectAsyncComponentIssues(component, filePath, sourceCode, config) {
|
|
184
|
+
if (!config.checkServerComponents)
|
|
185
|
+
return [];
|
|
186
|
+
const smells = [];
|
|
187
|
+
const lines = sourceCode.split('\n');
|
|
188
|
+
const hasUseClient = lines.some(line => /^['"]use client['"];?\s*$/.test(line.trim()));
|
|
189
|
+
// Check if component is async
|
|
190
|
+
let isAsync = false;
|
|
191
|
+
const node = component.path.node;
|
|
192
|
+
if (t.isFunctionDeclaration(node) || t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
|
|
193
|
+
isAsync = node.async || false;
|
|
194
|
+
}
|
|
195
|
+
// Async components in Client Components are not allowed
|
|
196
|
+
if (isAsync && hasUseClient) {
|
|
197
|
+
smells.push({
|
|
198
|
+
type: 'async-client-component',
|
|
199
|
+
severity: 'error',
|
|
200
|
+
message: `Client Component "${component.name}" is async, which is not supported`,
|
|
201
|
+
file: filePath,
|
|
202
|
+
line: component.startLine,
|
|
203
|
+
column: 0,
|
|
204
|
+
suggestion: `Remove async from the component. Use useEffect or React Query for data fetching in Client Components.`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Check for proper Suspense usage with async Server Components
|
|
208
|
+
if (isAsync && !hasUseClient) {
|
|
209
|
+
// Good pattern - async Server Component
|
|
210
|
+
// Could check if parent has Suspense boundary, but that requires cross-file analysis
|
|
211
|
+
}
|
|
212
|
+
return smells;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get code snippet for context
|
|
216
|
+
*/
|
|
217
|
+
function getCodeSnippet(sourceCode, line) {
|
|
218
|
+
const lines = sourceCode.split('\n');
|
|
219
|
+
const startLine = Math.max(0, line - 2);
|
|
220
|
+
const endLine = Math.min(lines.length, line + 1);
|
|
221
|
+
return lines.slice(startLine, endLine).join('\n');
|
|
222
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AnalysisResult, CodeSmell } from './types/index.js';
|
|
2
|
+
export interface DocGeneratorOptions {
|
|
3
|
+
format: 'markdown' | 'html' | 'json';
|
|
4
|
+
outputDir?: string;
|
|
5
|
+
includeSmells?: boolean;
|
|
6
|
+
includeMetrics?: boolean;
|
|
7
|
+
groupByFolder?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ComponentDoc {
|
|
10
|
+
name: string;
|
|
11
|
+
file: string;
|
|
12
|
+
relativePath: string;
|
|
13
|
+
lineCount: number;
|
|
14
|
+
props: string[];
|
|
15
|
+
hooks: {
|
|
16
|
+
useState: number;
|
|
17
|
+
useEffect: number;
|
|
18
|
+
useMemo: number;
|
|
19
|
+
useCallback: number;
|
|
20
|
+
useRef: number;
|
|
21
|
+
custom: string[];
|
|
22
|
+
};
|
|
23
|
+
smells: CodeSmell[];
|
|
24
|
+
metrics: {
|
|
25
|
+
complexity: string;
|
|
26
|
+
maintainability: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate documentation from component analysis
|
|
31
|
+
*/
|
|
32
|
+
export declare function generateComponentDocs(result: AnalysisResult, rootDir: string, options?: DocGeneratorOptions): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Write documentation to file
|
|
35
|
+
*/
|
|
36
|
+
export declare function writeComponentDocs(result: AnalysisResult, rootDir: string, options: DocGeneratorOptions): Promise<string>;
|
|
37
|
+
//# sourceMappingURL=docGenerator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docGenerator.d.ts","sourceRoot":"","sources":["../src/docGenerator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAA+B,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE1F,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAA4C,GACpD,OAAO,CAAC,MAAM,CAAC,CAYjB;AAqTD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAYjB"}
|