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/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A CLI tool that analyzes React projects and detects common code smells, providin
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- ๐ **Detect Code Smells**: Identifies common React anti-patterns (
|
|
7
|
+
- ๐ **Detect Code Smells**: Identifies common React anti-patterns (70+ smell types)
|
|
8
8
|
- ๐ **Technical Debt Score**: Grades your codebase from A to F
|
|
9
9
|
- ๐ก **Refactoring Suggestions**: Actionable recommendations for each issue
|
|
10
10
|
- ๐ **Multiple Output Formats**: Console (colored), JSON, Markdown, and HTML
|
|
@@ -24,6 +24,17 @@ A CLI tool that analyzes React projects and detects common code smells, providin
|
|
|
24
24
|
- ๐ **Dependency Graph Visualization**: Visual SVG/HTML of component and import relationships
|
|
25
25
|
- ๐ฆ **Bundle Size Impact**: Per-component bundle size estimates and optimization suggestions
|
|
26
26
|
- โ๏ธ **Custom Rules Engine**: Define project-specific code quality rules in configuration
|
|
27
|
+
- ๐ง **Interactive Fix Mode**: Review and apply fixes one by one with diff preview
|
|
28
|
+
- ๐ฌ **GitHub PR Comments**: Auto-comment analysis results on pull requests
|
|
29
|
+
- ๐ **Performance Budget**: Set thresholds and enforce limits in CI/CD
|
|
30
|
+
- ๐ **Component Documentation**: Auto-generate docs from component analysis
|
|
31
|
+
- โ๏ธ **React 19 Server Components**: Detect Server/Client boundary issues
|
|
32
|
+
- ๐ฎ **Context API Analysis**: Detect context overuse, missing memoization, re-render issues
|
|
33
|
+
- ๐ก๏ธ **Error Boundary Detection**: Find missing ErrorBoundary and Suspense wrappers
|
|
34
|
+
- ๐ **Form Validation Smells**: Detect uncontrolled inputs, missing validation
|
|
35
|
+
- ๐๏ธ **State Management Analysis**: Redux/Zustand anti-patterns, selector issues
|
|
36
|
+
- ๐งช **Testing Gap Detection**: Identify complex components likely needing tests
|
|
37
|
+
- ๐ค **AI Refactoring Suggestions**: LLM-powered smart refactoring recommendations
|
|
27
38
|
|
|
28
39
|
### Detected Code Smells
|
|
29
40
|
|
|
@@ -42,7 +53,13 @@ A CLI tool that analyzes React projects and detects common code smells, providin
|
|
|
42
53
|
| **Import Issues** | Circular dependencies, barrel file imports, excessive imports |
|
|
43
54
|
| **Unused Code** | Unused exports, dead imports |
|
|
44
55
|
| **Custom Rules** | User-defined code quality rules |
|
|
56
|
+
| **Server Components** | React 19 client/server boundary issues, async components |
|
|
45
57
|
| **Framework-Specific** | Next.js, React Native, Node.js, TypeScript issues |
|
|
58
|
+
| **Context API** | Context overuse, missing memoization, large context values |
|
|
59
|
+
| **Error Boundaries** | Missing ErrorBoundary, Suspense without fallback |
|
|
60
|
+
| **Form Validation** | Uncontrolled forms, missing validation, controlled inputs without onChange |
|
|
61
|
+
| **State Management** | Redux selector anti-patterns, derived state issues, state sync problems |
|
|
62
|
+
| **Testing Gaps** | Complex untestable components, side-effect heavy code, tight coupling |
|
|
46
63
|
|
|
47
64
|
## Installation
|
|
48
65
|
|
|
@@ -132,6 +149,16 @@ Or create manually:
|
|
|
132
149
|
| `--graph-format <format>` | Graph output format: svg, html | `html` |
|
|
133
150
|
| `--bundle` | Analyze bundle size impact per component | `false` |
|
|
134
151
|
| `--rules <file>` | Custom rules configuration file | - |
|
|
152
|
+
| `--fix-interactive` | Interactive fix mode: review fixes one by one | `false` |
|
|
153
|
+
| `--fix-preview` | Preview fixable issues without applying | `false` |
|
|
154
|
+
| `--pr-comment` | Generate PR comment (for GitHub Actions) | `false` |
|
|
155
|
+
| `--budget` | Check against performance budget | `false` |
|
|
156
|
+
| `--budget-config <file>` | Path to budget config file | `.smellbudget.json` |
|
|
157
|
+
| `--docs` | Generate component documentation | `false` |
|
|
158
|
+
| `--docs-format <format>` | Documentation format: markdown, html, json | `markdown` |
|
|
159
|
+
| `--ai` | Enable AI-powered refactoring suggestions | `false` |
|
|
160
|
+
| `--ai-key <key>` | API key for AI provider | - |
|
|
161
|
+
| `--ai-model <model>` | AI model to use (gpt-4, claude-3-sonnet, etc.) | `gpt-4` |
|
|
135
162
|
|
|
136
163
|
### Auto-Fix
|
|
137
164
|
|
|
@@ -392,6 +419,153 @@ react-smell ./src --rules .smellrc-rules.json --format json --ci
|
|
|
392
419
|
}
|
|
393
420
|
```
|
|
394
421
|
|
|
422
|
+
### Interactive Fix Mode
|
|
423
|
+
|
|
424
|
+
Review and apply fixes one by one with diff preview:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
# Interactive mode - review each fix
|
|
428
|
+
react-smell ./src --fix-interactive
|
|
429
|
+
|
|
430
|
+
# Preview fixable issues without applying
|
|
431
|
+
react-smell ./src --fix-preview
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Output:**
|
|
435
|
+
```
|
|
436
|
+
๐ง Interactive Fix Mode
|
|
437
|
+
Found 5 fixable issue(s). Review each one:
|
|
438
|
+
|
|
439
|
+
Commands: [y]es, [n]o, [a]ll, [q]uit
|
|
440
|
+
|
|
441
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
442
|
+
src/utils/helper.ts:15
|
|
443
|
+
debug-statement: console.log found
|
|
444
|
+
Fix: Remove console.log/debugger statements
|
|
445
|
+
|
|
446
|
+
- console.log('debug:', value);
|
|
447
|
+
+ (line removed)
|
|
448
|
+
|
|
449
|
+
Apply this fix? [y/n/a/q]: y
|
|
450
|
+
โ Applied
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### GitHub PR Comments
|
|
454
|
+
|
|
455
|
+
Auto-comment code smell analysis on pull requests:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
# Generate PR comment (outputs markdown)
|
|
459
|
+
react-smell ./src --pr-comment
|
|
460
|
+
|
|
461
|
+
# In GitHub Actions (auto-posts to PR)
|
|
462
|
+
react-smell ./src --pr-comment --ci
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**GitHub Actions workflow:**
|
|
466
|
+
```yaml
|
|
467
|
+
- name: Analyze Code
|
|
468
|
+
run: npx react-smell ./src --pr-comment --ci
|
|
469
|
+
env:
|
|
470
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Performance Budget
|
|
474
|
+
|
|
475
|
+
Set thresholds for code quality and enforce in CI/CD:
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
# Create budget config
|
|
479
|
+
react-smell init-budget
|
|
480
|
+
|
|
481
|
+
# Check against budget
|
|
482
|
+
react-smell ./src --budget
|
|
483
|
+
|
|
484
|
+
# With custom config
|
|
485
|
+
react-smell ./src --budget --budget-config my-budget.json
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Budget config (`.smellbudget.json`):**
|
|
489
|
+
```json
|
|
490
|
+
{
|
|
491
|
+
"maxErrors": 0,
|
|
492
|
+
"maxWarnings": 10,
|
|
493
|
+
"minScore": 70,
|
|
494
|
+
"minGrade": "C",
|
|
495
|
+
"maxSmellsPerFile": 5,
|
|
496
|
+
"maxByType": {
|
|
497
|
+
"useEffect-overuse": 3,
|
|
498
|
+
"prop-drilling": 5
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Output:**
|
|
504
|
+
```
|
|
505
|
+
โ Performance budget check failed
|
|
506
|
+
|
|
507
|
+
Violations:
|
|
508
|
+
โ Errors (2) exceeds budget (0)
|
|
509
|
+
โ Warnings (15) exceeds budget (10)
|
|
510
|
+
|
|
511
|
+
Checks: 3 passed, 2 failed
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Component Documentation
|
|
515
|
+
|
|
516
|
+
Auto-generate documentation from component analysis:
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
# Generate markdown docs
|
|
520
|
+
react-smell docs ./src
|
|
521
|
+
|
|
522
|
+
# Generate HTML docs
|
|
523
|
+
react-smell docs ./src -f html
|
|
524
|
+
|
|
525
|
+
# Output to specific directory
|
|
526
|
+
react-smell docs ./src -f html -o ./docs
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Output (COMPONENTS.md):**
|
|
530
|
+
```markdown
|
|
531
|
+
# Component Documentation
|
|
532
|
+
|
|
533
|
+
## Summary
|
|
534
|
+
| Metric | Value |
|
|
535
|
+
|--------|-------|
|
|
536
|
+
| Total Components | 25 |
|
|
537
|
+
| Technical Debt Grade | B |
|
|
538
|
+
|
|
539
|
+
## Components
|
|
540
|
+
|
|
541
|
+
#### Button
|
|
542
|
+
๐ `components/Button.tsx`
|
|
543
|
+
|
|
544
|
+
| Metric | Value |
|
|
545
|
+
|--------|-------|
|
|
546
|
+
| Lines | 45 |
|
|
547
|
+
| Complexity | ๐ข Low |
|
|
548
|
+
| Maintainability | ๐ข Good |
|
|
549
|
+
|
|
550
|
+
**Hooks:** useState (2), useCallback (1)
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### React 19 Server Components
|
|
554
|
+
|
|
555
|
+
Detect Server/Client component boundary issues:
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
# Enabled by default for app/ directory components
|
|
559
|
+
react-smell ./src
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Detected issues:**
|
|
563
|
+
- `server-component-hooks`: Using useState/useEffect in Server Components
|
|
564
|
+
- `server-component-events`: Using onClick/onChange in Server Components
|
|
565
|
+
- `server-component-browser-api`: Using window/document in Server Components
|
|
566
|
+
- `async-client-component`: Async function in 'use client' component
|
|
567
|
+
- `mixed-directives`: Both 'use client' and 'use server' in same file
|
|
568
|
+
|
|
395
569
|
## Example Output
|
|
396
570
|
|
|
397
571
|
```
|
|
@@ -421,9 +595,23 @@ react-smell ./src --rules .smellrc-rules.json --format json --ci
|
|
|
421
595
|
## Programmatic API
|
|
422
596
|
|
|
423
597
|
```typescript
|
|
424
|
-
import {
|
|
425
|
-
|
|
426
|
-
|
|
598
|
+
import {
|
|
599
|
+
analyzeProject,
|
|
600
|
+
reportResults,
|
|
601
|
+
// Interactive fixing
|
|
602
|
+
runInteractiveFix,
|
|
603
|
+
previewFixes,
|
|
604
|
+
// PR Comments
|
|
605
|
+
generatePRComment,
|
|
606
|
+
postPRComment,
|
|
607
|
+
// Performance Budget
|
|
608
|
+
loadBudget,
|
|
609
|
+
checkBudget,
|
|
610
|
+
formatBudgetReport,
|
|
611
|
+
// Documentation
|
|
612
|
+
generateComponentDocs,
|
|
613
|
+
writeComponentDocs,
|
|
614
|
+
} from 'react-code-smell-detector';
|
|
427
615
|
|
|
428
616
|
const result = await analyzeProject({
|
|
429
617
|
rootDir: './src',
|
|
@@ -431,32 +619,29 @@ const result = await analyzeProject({
|
|
|
431
619
|
maxUseEffectsPerComponent: 3,
|
|
432
620
|
maxComponentLines: 300,
|
|
433
621
|
checkUnusedCode: true,
|
|
434
|
-
|
|
622
|
+
checkServerComponents: true, // React 19
|
|
435
623
|
},
|
|
436
624
|
});
|
|
437
625
|
|
|
438
626
|
console.log(`Grade: ${result.debtScore.grade}`);
|
|
439
627
|
console.log(`Total issues: ${result.summary.totalSmells}`);
|
|
440
628
|
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
console.log(
|
|
446
|
-
|
|
447
|
-
// Send notification
|
|
448
|
-
const webhookConfig = getWebhookConfig(
|
|
449
|
-
process.env.REACT_SMELL_SLACK_WEBHOOK,
|
|
450
|
-
process.env.REACT_SMELL_DISCORD_WEBHOOK
|
|
451
|
-
);
|
|
452
|
-
if (webhookConfig) {
|
|
453
|
-
await sendWebhookNotification(
|
|
454
|
-
webhookConfig,
|
|
455
|
-
result.files.flatMap(f => f.smells),
|
|
456
|
-
'my-project'
|
|
457
|
-
);
|
|
629
|
+
// Check against performance budget
|
|
630
|
+
const budget = await loadBudget();
|
|
631
|
+
const budgetResult = checkBudget(result, budget);
|
|
632
|
+
if (!budgetResult.passed) {
|
|
633
|
+
console.log(formatBudgetReport(budgetResult));
|
|
458
634
|
}
|
|
459
635
|
|
|
636
|
+
// Generate documentation
|
|
637
|
+
const docsPath = await writeComponentDocs(result, './src', {
|
|
638
|
+
format: 'markdown',
|
|
639
|
+
includeSmells: true,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Generate PR comment
|
|
643
|
+
const prComment = generatePRComment(result, './src');
|
|
644
|
+
|
|
460
645
|
// Or use the reporter
|
|
461
646
|
const report = reportResults(result, {
|
|
462
647
|
format: 'markdown',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/parser.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCode } from '../parser/index.js';
|
|
3
|
+
describe('Parser', () => {
|
|
4
|
+
it('should parse a simple React component', () => {
|
|
5
|
+
const code = `
|
|
6
|
+
function Hello({ name }) {
|
|
7
|
+
return <div>Hello {name}</div>;
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
const result = parseCode(code);
|
|
11
|
+
expect(result.components).toHaveLength(1);
|
|
12
|
+
expect(result.components[0].name).toBe('Hello');
|
|
13
|
+
});
|
|
14
|
+
it('should detect useState hook', () => {
|
|
15
|
+
const code = `
|
|
16
|
+
function Counter() {
|
|
17
|
+
const [count, setCount] = useState(0);
|
|
18
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
const result = parseCode(code);
|
|
22
|
+
expect(result.components).toHaveLength(1);
|
|
23
|
+
expect(result.components[0].hooks.useState).toHaveLength(1);
|
|
24
|
+
});
|
|
25
|
+
it('should detect useEffect hook', () => {
|
|
26
|
+
const code = `
|
|
27
|
+
function Timer() {
|
|
28
|
+
const [time, setTime] = useState(0);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const id = setInterval(() => setTime(t => t + 1), 1000);
|
|
31
|
+
return () => clearInterval(id);
|
|
32
|
+
}, []);
|
|
33
|
+
return <span>{time}</span>;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
const result = parseCode(code);
|
|
37
|
+
expect(result.components).toHaveLength(1);
|
|
38
|
+
expect(result.components[0].hooks.useEffect).toHaveLength(1);
|
|
39
|
+
});
|
|
40
|
+
it('should extract component props', () => {
|
|
41
|
+
const code = `
|
|
42
|
+
function Card({ title, description, onClick }) {
|
|
43
|
+
return (
|
|
44
|
+
<div onClick={onClick}>
|
|
45
|
+
<h1>{title}</h1>
|
|
46
|
+
<p>{description}</p>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
const result = parseCode(code);
|
|
52
|
+
expect(result.components[0].props).toContain('title');
|
|
53
|
+
expect(result.components[0].props).toContain('description');
|
|
54
|
+
expect(result.components[0].props).toContain('onClick');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performanceBudget.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/performanceBudget.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { checkBudget } from '../performanceBudget.js';
|
|
3
|
+
describe('Performance Budget', () => {
|
|
4
|
+
const createMockResult = (overrides = {}) => ({
|
|
5
|
+
files: [],
|
|
6
|
+
summary: {
|
|
7
|
+
totalFiles: 5,
|
|
8
|
+
totalComponents: 10,
|
|
9
|
+
totalSmells: 3,
|
|
10
|
+
smellsByType: Object.fromEntries([
|
|
11
|
+
'useEffect-overuse', 'prop-drilling', 'large-component', 'unmemoized-calculation',
|
|
12
|
+
'missing-dependency', 'state-in-loop', 'inline-function-prop', 'deep-nesting',
|
|
13
|
+
'missing-key', 'hooks-rules-violation', 'dependency-array-issue', 'nested-ternary',
|
|
14
|
+
'dead-code', 'magic-value', 'debug-statement', 'todo-comment', 'security-xss',
|
|
15
|
+
'security-eval', 'security-secrets', 'a11y-missing-alt', 'a11y-missing-label',
|
|
16
|
+
'a11y-interactive-role', 'a11y-keyboard', 'a11y-semantic',
|
|
17
|
+
'nextjs-client-server-boundary', 'nextjs-missing-metadata', 'nextjs-image-unoptimized',
|
|
18
|
+
'nextjs-router-misuse', 'rn-inline-style', 'rn-missing-accessibility', 'rn-performance-issue',
|
|
19
|
+
'nodejs-callback-hell', 'nodejs-unhandled-promise', 'nodejs-sync-io', 'nodejs-missing-error-handling',
|
|
20
|
+
'js-var-usage', 'js-loose-equality', 'js-implicit-coercion', 'js-global-pollution',
|
|
21
|
+
'ts-any-usage', 'ts-missing-return-type', 'ts-non-null-assertion', 'ts-type-assertion',
|
|
22
|
+
'high-cyclomatic-complexity', 'high-cognitive-complexity',
|
|
23
|
+
'memory-leak-event-listener', 'memory-leak-subscription', 'memory-leak-timer', 'memory-leak-async',
|
|
24
|
+
'circular-dependency', 'barrel-file-import', 'namespace-import', 'excessive-imports',
|
|
25
|
+
'unused-export', 'dead-import',
|
|
26
|
+
'server-component-hooks', 'server-component-events', 'server-component-browser-api',
|
|
27
|
+
'async-client-component', 'mixed-directives', 'custom-rule'
|
|
28
|
+
].map(key => [key, 0])),
|
|
29
|
+
smellsBySeverity: { error: 0, warning: 2, info: 1 },
|
|
30
|
+
},
|
|
31
|
+
debtScore: {
|
|
32
|
+
score: 85,
|
|
33
|
+
grade: 'B',
|
|
34
|
+
breakdown: {
|
|
35
|
+
useEffectScore: 90,
|
|
36
|
+
propDrillingScore: 80,
|
|
37
|
+
componentSizeScore: 85,
|
|
38
|
+
memoizationScore: 85,
|
|
39
|
+
},
|
|
40
|
+
estimatedRefactorTime: '< 30 minutes',
|
|
41
|
+
},
|
|
42
|
+
...overrides,
|
|
43
|
+
});
|
|
44
|
+
it('should pass when all budgets are met', () => {
|
|
45
|
+
const budget = {
|
|
46
|
+
maxTotalSmells: 10,
|
|
47
|
+
maxErrors: 5,
|
|
48
|
+
minGrade: 'C',
|
|
49
|
+
};
|
|
50
|
+
const result = checkBudget(createMockResult(), budget);
|
|
51
|
+
expect(result.passed).toBe(true);
|
|
52
|
+
expect(result.violations).toHaveLength(0);
|
|
53
|
+
});
|
|
54
|
+
it('should fail when total smells exceed budget', () => {
|
|
55
|
+
const budget = {
|
|
56
|
+
maxTotalSmells: 2,
|
|
57
|
+
};
|
|
58
|
+
const result = checkBudget(createMockResult(), budget);
|
|
59
|
+
expect(result.passed).toBe(false);
|
|
60
|
+
expect(result.violations).toHaveLength(1);
|
|
61
|
+
expect(result.violations[0].rule).toBe('maxTotalSmells');
|
|
62
|
+
});
|
|
63
|
+
it('should fail when errors exceed budget', () => {
|
|
64
|
+
const budget = {
|
|
65
|
+
maxErrors: 0,
|
|
66
|
+
};
|
|
67
|
+
const mockResult = createMockResult();
|
|
68
|
+
mockResult.summary.smellsBySeverity.error = 2;
|
|
69
|
+
const result = checkBudget(mockResult, budget);
|
|
70
|
+
expect(result.passed).toBe(false);
|
|
71
|
+
expect(result.violations[0].rule).toBe('maxErrors');
|
|
72
|
+
});
|
|
73
|
+
it('should fail when grade is below minimum', () => {
|
|
74
|
+
const budget = {
|
|
75
|
+
minGrade: 'A',
|
|
76
|
+
};
|
|
77
|
+
const result = checkBudget(createMockResult(), budget);
|
|
78
|
+
expect(result.passed).toBe(false);
|
|
79
|
+
expect(result.violations[0].rule).toBe('minGrade');
|
|
80
|
+
});
|
|
81
|
+
it('should allow warnings as non-blocking violations', () => {
|
|
82
|
+
const budget = {
|
|
83
|
+
maxWarnings: 1,
|
|
84
|
+
};
|
|
85
|
+
const result = checkBudget(createMockResult(), budget);
|
|
86
|
+
// Should have violation but still pass (warnings are non-blocking)
|
|
87
|
+
expect(result.violations).toHaveLength(1);
|
|
88
|
+
expect(result.violations[0].severity).toBe('warning');
|
|
89
|
+
expect(result.passed).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prComments.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/prComments.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generatePRComment, generateInlineComments } from '../prComments.js';
|
|
3
|
+
describe('PR Comments', () => {
|
|
4
|
+
const createMockResult = () => ({
|
|
5
|
+
files: [
|
|
6
|
+
{
|
|
7
|
+
file: '/src/components/Button.tsx',
|
|
8
|
+
components: [
|
|
9
|
+
{
|
|
10
|
+
name: 'Button',
|
|
11
|
+
file: '/src/components/Button.tsx',
|
|
12
|
+
startLine: 1,
|
|
13
|
+
endLine: 50,
|
|
14
|
+
lineCount: 50,
|
|
15
|
+
useEffectCount: 1,
|
|
16
|
+
useStateCount: 2,
|
|
17
|
+
useMemoCount: 0,
|
|
18
|
+
useCallbackCount: 0,
|
|
19
|
+
propsCount: 3,
|
|
20
|
+
propsDrillingDepth: 0,
|
|
21
|
+
hasExpensiveCalculation: false,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
smells: [
|
|
25
|
+
{
|
|
26
|
+
type: 'useEffect-overuse',
|
|
27
|
+
severity: 'warning',
|
|
28
|
+
message: 'Component has multiple useEffect hooks',
|
|
29
|
+
file: '/src/components/Button.tsx',
|
|
30
|
+
line: 10,
|
|
31
|
+
column: 0,
|
|
32
|
+
suggestion: 'Consider consolidating effects',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
imports: [],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
summary: {
|
|
39
|
+
totalFiles: 1,
|
|
40
|
+
totalComponents: 1,
|
|
41
|
+
totalSmells: 1,
|
|
42
|
+
smellsByType: {
|
|
43
|
+
'useEffect-overuse': 1,
|
|
44
|
+
},
|
|
45
|
+
smellsBySeverity: { error: 0, warning: 1, info: 0 },
|
|
46
|
+
},
|
|
47
|
+
debtScore: {
|
|
48
|
+
score: 85,
|
|
49
|
+
grade: 'B',
|
|
50
|
+
breakdown: {
|
|
51
|
+
useEffectScore: 85,
|
|
52
|
+
propDrillingScore: 100,
|
|
53
|
+
componentSizeScore: 100,
|
|
54
|
+
memoizationScore: 100,
|
|
55
|
+
},
|
|
56
|
+
estimatedRefactorTime: '< 30 minutes',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
it('should generate a markdown PR comment', () => {
|
|
60
|
+
const result = createMockResult();
|
|
61
|
+
const comment = generatePRComment(result, '/');
|
|
62
|
+
expect(comment).toContain('## ๐ Code Smell Analysis Report');
|
|
63
|
+
expect(comment).toContain('Grade: **B**');
|
|
64
|
+
expect(comment).toContain('Total Issues | 1');
|
|
65
|
+
expect(comment).toContain('useEffect-overuse');
|
|
66
|
+
});
|
|
67
|
+
it('should include score breakdown', () => {
|
|
68
|
+
const result = createMockResult();
|
|
69
|
+
const comment = generatePRComment(result, '/');
|
|
70
|
+
expect(comment).toContain('Score Breakdown');
|
|
71
|
+
expect(comment).toContain('useEffect Usage');
|
|
72
|
+
expect(comment).toContain('85/100');
|
|
73
|
+
});
|
|
74
|
+
it('should generate inline comments for smells', () => {
|
|
75
|
+
const smells = [
|
|
76
|
+
{
|
|
77
|
+
type: 'debug-statement',
|
|
78
|
+
severity: 'warning',
|
|
79
|
+
message: 'console.log found',
|
|
80
|
+
file: '/src/utils/helper.ts',
|
|
81
|
+
line: 15,
|
|
82
|
+
column: 0,
|
|
83
|
+
suggestion: 'Remove debug statements',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
const comments = generateInlineComments(smells, '/');
|
|
87
|
+
expect(comments).toHaveLength(1);
|
|
88
|
+
expect(comments[0].path).toBe('src/utils/helper.ts');
|
|
89
|
+
expect(comments[0].line).toBe(15);
|
|
90
|
+
expect(comments[0].body).toContain('debug-statement');
|
|
91
|
+
});
|
|
92
|
+
it('should group multiple smells on same line', () => {
|
|
93
|
+
const smells = [
|
|
94
|
+
{
|
|
95
|
+
type: 'debug-statement',
|
|
96
|
+
severity: 'warning',
|
|
97
|
+
message: 'console.log found',
|
|
98
|
+
file: '/src/utils/helper.ts',
|
|
99
|
+
line: 15,
|
|
100
|
+
column: 0,
|
|
101
|
+
suggestion: 'Remove debug statements',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'js-var-usage',
|
|
105
|
+
severity: 'warning',
|
|
106
|
+
message: 'var usage detected',
|
|
107
|
+
file: '/src/utils/helper.ts',
|
|
108
|
+
line: 15,
|
|
109
|
+
column: 0,
|
|
110
|
+
suggestion: 'Use let or const',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const comments = generateInlineComments(smells, '/');
|
|
114
|
+
expect(comments).toHaveLength(1);
|
|
115
|
+
expect(comments[0].body).toContain('debug-statement');
|
|
116
|
+
expect(comments[0].body).toContain('js-var-usage');
|
|
117
|
+
});
|
|
118
|
+
});
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAiCA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA8DtF;AAmRD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/analyzer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fg from 'fast-glob';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { parseFile } from './parser/index.js';
|
|
4
|
-
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, detectComplexity, detectMemoryLeaks, detectImportIssues, detectUnusedCode, } from './detectors/index.js';
|
|
4
|
+
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, detectComplexity, detectMemoryLeaks, detectImportIssues, detectUnusedCode, detectServerComponentIssues, detectAsyncComponentIssues, } from './detectors/index.js';
|
|
5
5
|
import { parseCustomRules, detectCustomRuleViolations } from './customRules.js';
|
|
6
6
|
import { buildDependencyGraph } from './graphGenerator.js';
|
|
7
7
|
import { analyzeBundleImpact } from './bundleAnalyzer.js';
|
|
@@ -100,6 +100,9 @@ function analyzeFile(parseResult, filePath, config) {
|
|
|
100
100
|
smells.push(...detectMemoryLeaks(component, filePath, sourceCode, config));
|
|
101
101
|
smells.push(...detectImportIssues(component, filePath, sourceCode, config));
|
|
102
102
|
smells.push(...detectUnusedCode(component, filePath, sourceCode, config));
|
|
103
|
+
// Server Components (React 19)
|
|
104
|
+
smells.push(...detectServerComponentIssues(component, filePath, sourceCode, config, imports));
|
|
105
|
+
smells.push(...detectAsyncComponentIssues(component, filePath, sourceCode, config));
|
|
103
106
|
// Custom rules
|
|
104
107
|
const customRules = parseCustomRules(config);
|
|
105
108
|
if (customRules.length > 0) {
|
|
@@ -213,6 +216,12 @@ function calculateSummary(files) {
|
|
|
213
216
|
// Unused code
|
|
214
217
|
'unused-export': 0,
|
|
215
218
|
'dead-import': 0,
|
|
219
|
+
// Server Components (React 19)
|
|
220
|
+
'server-component-hooks': 0,
|
|
221
|
+
'server-component-events': 0,
|
|
222
|
+
'server-component-browser-api': 0,
|
|
223
|
+
'async-client-component': 0,
|
|
224
|
+
'mixed-directives': 0,
|
|
216
225
|
// Custom rules
|
|
217
226
|
'custom-rule': 0,
|
|
218
227
|
};
|