react-code-smell-detector 1.0.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 +179 -0
- package/dist/analyzer.d.ts +10 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +169 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/detectors/index.d.ts +5 -0
- package/dist/detectors/index.d.ts.map +1 -0
- package/dist/detectors/index.js +4 -0
- package/dist/detectors/largeComponent.d.ts +4 -0
- package/dist/detectors/largeComponent.d.ts.map +1 -0
- package/dist/detectors/largeComponent.js +51 -0
- package/dist/detectors/memoization.d.ts +4 -0
- package/dist/detectors/memoization.d.ts.map +1 -0
- package/dist/detectors/memoization.js +150 -0
- package/dist/detectors/propDrilling.d.ts +5 -0
- package/dist/detectors/propDrilling.d.ts.map +1 -0
- package/dist/detectors/propDrilling.js +82 -0
- package/dist/detectors/useEffect.d.ts +4 -0
- package/dist/detectors/useEffect.d.ts.map +1 -0
- package/dist/detectors/useEffect.js +101 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/parser/index.d.ts +29 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +151 -0
- package/dist/reporter.d.ts +8 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +217 -0
- package/dist/types/index.d.ts +64 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/package.json +45 -0
- package/src/analyzer.ts +216 -0
- package/src/cli.ts +125 -0
- package/src/detectors/index.ts +4 -0
- package/src/detectors/largeComponent.ts +63 -0
- package/src/detectors/memoization.ts +177 -0
- package/src/detectors/propDrilling.ts +103 -0
- package/src/detectors/useEffect.ts +117 -0
- package/src/index.ts +4 -0
- package/src/parser/index.ts +195 -0
- package/src/reporter.ts +248 -0
- package/src/types/index.ts +86 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# React Code Smell Detector
|
|
2
|
+
|
|
3
|
+
A CLI tool that analyzes React projects and detects common code smells, providing refactoring suggestions and a technical debt score.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔍 **Detect Code Smells**: Identifies common React anti-patterns
|
|
8
|
+
- 📊 **Technical Debt Score**: Grades your codebase from A to F
|
|
9
|
+
- 💡 **Refactoring Suggestions**: Actionable recommendations for each issue
|
|
10
|
+
- 📝 **Multiple Output Formats**: Console (colored), JSON, and Markdown
|
|
11
|
+
|
|
12
|
+
### Detected Code Smells
|
|
13
|
+
|
|
14
|
+
| Category | Detection |
|
|
15
|
+
|----------|-----------|
|
|
16
|
+
| **useEffect Overuse** | Multiple useEffects per component, missing cleanup, async patterns |
|
|
17
|
+
| **Prop Drilling** | Too many props, drilling depth analysis |
|
|
18
|
+
| **Large Components** | Components over 300 lines, deep JSX nesting |
|
|
19
|
+
| **Unmemoized Calculations** | `.map()`, `.filter()`, `.reduce()`, `JSON.parse()` without `useMemo` |
|
|
20
|
+
| **Inline Functions** | Callbacks defined inline in JSX props |
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g react-code-smell-detector
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or use as a dev dependency:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -D react-code-smell-detector
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Basic Analysis
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
react-smell /path/to/react/project
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### With Code Snippets
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
react-smell ./src -s
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Output Formats
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Console (default - colored output)
|
|
52
|
+
react-smell ./src
|
|
53
|
+
|
|
54
|
+
# JSON (for CI/CD integration)
|
|
55
|
+
react-smell ./src -f json
|
|
56
|
+
|
|
57
|
+
# Markdown (for documentation)
|
|
58
|
+
react-smell ./src -f markdown -o report.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Configuration
|
|
62
|
+
|
|
63
|
+
Create a `.smellrc.json` file:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
react-smell init
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or create manually:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"maxUseEffectsPerComponent": 3,
|
|
74
|
+
"maxPropDrillingDepth": 3,
|
|
75
|
+
"maxComponentLines": 300,
|
|
76
|
+
"maxPropsCount": 7
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### CLI Options
|
|
81
|
+
|
|
82
|
+
| Option | Description | Default |
|
|
83
|
+
|--------|-------------|---------|
|
|
84
|
+
| `-f, --format <format>` | Output format: console, json, markdown | `console` |
|
|
85
|
+
| `-s, --snippets` | Show code snippets in output | `false` |
|
|
86
|
+
| `-c, --config <file>` | Path to config file | `.smellrc.json` |
|
|
87
|
+
| `--max-effects <number>` | Max useEffects per component | `3` |
|
|
88
|
+
| `--max-props <number>` | Max props before warning | `7` |
|
|
89
|
+
| `--max-lines <number>` | Max lines per component | `300` |
|
|
90
|
+
| `--include <patterns>` | Glob patterns to include | `**/*.tsx,**/*.jsx` |
|
|
91
|
+
| `--exclude <patterns>` | Glob patterns to exclude | `node_modules,dist` |
|
|
92
|
+
| `-o, --output <file>` | Write output to file | - |
|
|
93
|
+
|
|
94
|
+
## Example Output
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
98
|
+
║ 🔍 React Code Smell Detector Report ║
|
|
99
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
100
|
+
|
|
101
|
+
📊 Technical Debt Score
|
|
102
|
+
|
|
103
|
+
Grade: C ███████████████░░░░░ 73/100
|
|
104
|
+
|
|
105
|
+
Breakdown:
|
|
106
|
+
⚡ useEffect: ██████████ 100
|
|
107
|
+
🔗 Prop Drilling: ██████████ 100
|
|
108
|
+
📐 Component Size:███████░░░ 70
|
|
109
|
+
💾 Memoization: ░░░░░░░░░░ 0
|
|
110
|
+
|
|
111
|
+
Estimated refactor time: 1-2 hours
|
|
112
|
+
|
|
113
|
+
📈 Summary
|
|
114
|
+
|
|
115
|
+
Files analyzed: 10
|
|
116
|
+
Components found: 21
|
|
117
|
+
Total issues: 5 warning(s), 26 info
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Programmatic API
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { analyzeProject, reportResults } from 'react-code-smell-detector';
|
|
124
|
+
|
|
125
|
+
const result = await analyzeProject({
|
|
126
|
+
rootDir: './src',
|
|
127
|
+
config: {
|
|
128
|
+
maxUseEffectsPerComponent: 3,
|
|
129
|
+
maxComponentLines: 300,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log(`Grade: ${result.debtScore.grade}`);
|
|
134
|
+
console.log(`Total issues: ${result.summary.totalSmells}`);
|
|
135
|
+
|
|
136
|
+
// Or use the reporter
|
|
137
|
+
const report = reportResults(result, {
|
|
138
|
+
format: 'markdown',
|
|
139
|
+
showCodeSnippets: true,
|
|
140
|
+
rootDir: './src',
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## CI/CD Integration
|
|
145
|
+
|
|
146
|
+
Use the JSON output format for CI pipelines:
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
# GitHub Actions example
|
|
150
|
+
- name: Check code smells
|
|
151
|
+
run: |
|
|
152
|
+
npx react-smell ./src -f json > smell-report.json
|
|
153
|
+
if [ $(jq '.summary.smellsBySeverity.error' smell-report.json) -gt 0 ]; then
|
|
154
|
+
echo "Code smell errors found!"
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Technical Debt Score
|
|
160
|
+
|
|
161
|
+
The score is calculated based on:
|
|
162
|
+
|
|
163
|
+
| Category | Weight | Description |
|
|
164
|
+
|----------|--------|-------------|
|
|
165
|
+
| useEffect Usage | 30% | Penalized for overuse, missing cleanup |
|
|
166
|
+
| Prop Drilling | 25% | Penalized for deep drilling, too many props |
|
|
167
|
+
| Component Size | 25% | Penalized for large components, deep nesting |
|
|
168
|
+
| Memoization | 20% | Penalized for unmemoized calculations |
|
|
169
|
+
|
|
170
|
+
**Grades:**
|
|
171
|
+
- **A**: 90-100 (Excellent)
|
|
172
|
+
- **B**: 80-89 (Good)
|
|
173
|
+
- **C**: 70-79 (Needs Improvement)
|
|
174
|
+
- **D**: 60-69 (Poor)
|
|
175
|
+
- **F**: <60 (Critical)
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AnalysisResult, DetectorConfig } from './types/index.js';
|
|
2
|
+
export interface AnalyzerOptions {
|
|
3
|
+
rootDir: string;
|
|
4
|
+
include?: string[];
|
|
5
|
+
exclude?: string[];
|
|
6
|
+
config?: Partial<DetectorConfig>;
|
|
7
|
+
}
|
|
8
|
+
export declare function analyzeProject(options: AnalyzerOptions): Promise<AnalysisResult>;
|
|
9
|
+
export { DEFAULT_CONFIG, DetectorConfig } from './types/index.js';
|
|
10
|
+
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAUA,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,CA0CtF;AA+ID,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/analyzer.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fg from 'fast-glob';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parseFile } from './parser/index.js';
|
|
4
|
+
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, } from './detectors/index.js';
|
|
5
|
+
import { DEFAULT_CONFIG, } from './types/index.js';
|
|
6
|
+
export async function analyzeProject(options) {
|
|
7
|
+
const { rootDir, include = ['**/*.tsx', '**/*.jsx'], exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'], config: userConfig = {}, } = options;
|
|
8
|
+
const config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
9
|
+
// Find all React files
|
|
10
|
+
const patterns = include.map(p => path.join(rootDir, p));
|
|
11
|
+
const files = await fg(patterns, {
|
|
12
|
+
ignore: exclude,
|
|
13
|
+
absolute: true,
|
|
14
|
+
});
|
|
15
|
+
const fileAnalyses = [];
|
|
16
|
+
// Analyze each file
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
try {
|
|
19
|
+
const parseResult = await parseFile(file);
|
|
20
|
+
const analysis = analyzeFile(parseResult, file, config);
|
|
21
|
+
if (analysis.components.length > 0 || analysis.smells.length > 0) {
|
|
22
|
+
fileAnalyses.push(analysis);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Skip files that can't be parsed
|
|
27
|
+
console.warn(`Warning: Could not parse ${file}: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Calculate summary and score
|
|
31
|
+
const summary = calculateSummary(fileAnalyses);
|
|
32
|
+
const debtScore = calculateTechnicalDebtScore(fileAnalyses, summary);
|
|
33
|
+
return {
|
|
34
|
+
files: fileAnalyses,
|
|
35
|
+
summary,
|
|
36
|
+
debtScore,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function analyzeFile(parseResult, filePath, config) {
|
|
40
|
+
const { components, imports, sourceCode } = parseResult;
|
|
41
|
+
const smells = [];
|
|
42
|
+
const componentInfos = [];
|
|
43
|
+
// Run all detectors on each component
|
|
44
|
+
components.forEach(component => {
|
|
45
|
+
// Collect component info
|
|
46
|
+
componentInfos.push({
|
|
47
|
+
name: component.name,
|
|
48
|
+
file: filePath,
|
|
49
|
+
startLine: component.startLine,
|
|
50
|
+
endLine: component.endLine,
|
|
51
|
+
lineCount: component.endLine - component.startLine + 1,
|
|
52
|
+
useEffectCount: component.hooks.useEffect.length,
|
|
53
|
+
useStateCount: component.hooks.useState.length,
|
|
54
|
+
useMemoCount: component.hooks.useMemo.length,
|
|
55
|
+
useCallbackCount: component.hooks.useCallback.length,
|
|
56
|
+
propsCount: component.props.length,
|
|
57
|
+
propsDrillingDepth: 0, // Calculated separately
|
|
58
|
+
hasExpensiveCalculation: false, // Will be set by memoization detector
|
|
59
|
+
});
|
|
60
|
+
// Run detectors
|
|
61
|
+
smells.push(...detectUseEffectOveruse(component, filePath, sourceCode, config));
|
|
62
|
+
smells.push(...detectPropDrilling(component, filePath, sourceCode, config));
|
|
63
|
+
smells.push(...detectLargeComponent(component, filePath, sourceCode, config));
|
|
64
|
+
smells.push(...detectUnmemoizedCalculations(component, filePath, sourceCode, config));
|
|
65
|
+
});
|
|
66
|
+
// Run cross-component analysis
|
|
67
|
+
smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
|
|
68
|
+
return {
|
|
69
|
+
file: filePath,
|
|
70
|
+
components: componentInfos,
|
|
71
|
+
smells,
|
|
72
|
+
imports,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function calculateSummary(files) {
|
|
76
|
+
const smellsByType = {
|
|
77
|
+
'useEffect-overuse': 0,
|
|
78
|
+
'prop-drilling': 0,
|
|
79
|
+
'large-component': 0,
|
|
80
|
+
'unmemoized-calculation': 0,
|
|
81
|
+
'missing-dependency': 0,
|
|
82
|
+
'state-in-loop': 0,
|
|
83
|
+
'inline-function-prop': 0,
|
|
84
|
+
'deep-nesting': 0,
|
|
85
|
+
};
|
|
86
|
+
const smellsBySeverity = {
|
|
87
|
+
error: 0,
|
|
88
|
+
warning: 0,
|
|
89
|
+
info: 0,
|
|
90
|
+
};
|
|
91
|
+
let totalSmells = 0;
|
|
92
|
+
let totalComponents = 0;
|
|
93
|
+
files.forEach(file => {
|
|
94
|
+
totalComponents += file.components.length;
|
|
95
|
+
file.smells.forEach(smell => {
|
|
96
|
+
totalSmells++;
|
|
97
|
+
smellsByType[smell.type]++;
|
|
98
|
+
smellsBySeverity[smell.severity]++;
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
totalFiles: files.length,
|
|
103
|
+
totalComponents,
|
|
104
|
+
totalSmells,
|
|
105
|
+
smellsByType,
|
|
106
|
+
smellsBySeverity,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function calculateTechnicalDebtScore(files, summary) {
|
|
110
|
+
// Calculate individual scores (0-100, higher is better)
|
|
111
|
+
// useEffect score: penalize based on useEffect-related issues
|
|
112
|
+
const useEffectIssues = summary.smellsByType['useEffect-overuse'];
|
|
113
|
+
const useEffectScore = Math.max(0, 100 - useEffectIssues * 15);
|
|
114
|
+
// Prop drilling score
|
|
115
|
+
const propDrillingIssues = summary.smellsByType['prop-drilling'];
|
|
116
|
+
const propDrillingScore = Math.max(0, 100 - propDrillingIssues * 12);
|
|
117
|
+
// Component size score
|
|
118
|
+
const sizeIssues = summary.smellsByType['large-component'] + summary.smellsByType['deep-nesting'];
|
|
119
|
+
const componentSizeScore = Math.max(0, 100 - sizeIssues * 10);
|
|
120
|
+
// Memoization score
|
|
121
|
+
const memoIssues = summary.smellsByType['unmemoized-calculation'] + summary.smellsByType['inline-function-prop'];
|
|
122
|
+
const memoizationScore = Math.max(0, 100 - memoIssues * 8);
|
|
123
|
+
// Overall score (weighted average)
|
|
124
|
+
const score = Math.round(useEffectScore * 0.3 +
|
|
125
|
+
propDrillingScore * 0.25 +
|
|
126
|
+
componentSizeScore * 0.25 +
|
|
127
|
+
memoizationScore * 0.2);
|
|
128
|
+
// Determine grade
|
|
129
|
+
let grade;
|
|
130
|
+
if (score >= 90)
|
|
131
|
+
grade = 'A';
|
|
132
|
+
else if (score >= 80)
|
|
133
|
+
grade = 'B';
|
|
134
|
+
else if (score >= 70)
|
|
135
|
+
grade = 'C';
|
|
136
|
+
else if (score >= 60)
|
|
137
|
+
grade = 'D';
|
|
138
|
+
else
|
|
139
|
+
grade = 'F';
|
|
140
|
+
// Estimate refactor time
|
|
141
|
+
const errorCount = summary.smellsBySeverity.error;
|
|
142
|
+
const warningCount = summary.smellsBySeverity.warning;
|
|
143
|
+
const totalIssues = errorCount * 30 + warningCount * 15; // minutes
|
|
144
|
+
let estimatedRefactorTime;
|
|
145
|
+
if (totalIssues < 30)
|
|
146
|
+
estimatedRefactorTime = '< 30 minutes';
|
|
147
|
+
else if (totalIssues < 60)
|
|
148
|
+
estimatedRefactorTime = '30 min - 1 hour';
|
|
149
|
+
else if (totalIssues < 120)
|
|
150
|
+
estimatedRefactorTime = '1-2 hours';
|
|
151
|
+
else if (totalIssues < 240)
|
|
152
|
+
estimatedRefactorTime = '2-4 hours';
|
|
153
|
+
else if (totalIssues < 480)
|
|
154
|
+
estimatedRefactorTime = '4-8 hours';
|
|
155
|
+
else
|
|
156
|
+
estimatedRefactorTime = '> 1 day';
|
|
157
|
+
return {
|
|
158
|
+
score,
|
|
159
|
+
grade,
|
|
160
|
+
breakdown: {
|
|
161
|
+
useEffectScore,
|
|
162
|
+
propDrillingScore,
|
|
163
|
+
componentSizeScore,
|
|
164
|
+
memoizationScore,
|
|
165
|
+
},
|
|
166
|
+
estimatedRefactorTime,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export { DEFAULT_CONFIG } from './types/index.js';
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { analyzeProject, DEFAULT_CONFIG } from './analyzer.js';
|
|
7
|
+
import { reportResults } from './reporter.js';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('react-smell')
|
|
12
|
+
.description('Detect code smells in React projects')
|
|
13
|
+
.version('1.0.0')
|
|
14
|
+
.argument('[directory]', 'Directory to analyze', '.')
|
|
15
|
+
.option('-f, --format <format>', 'Output format: console, json, markdown', 'console')
|
|
16
|
+
.option('-s, --snippets', 'Show code snippets in output', false)
|
|
17
|
+
.option('-c, --config <file>', 'Path to config file')
|
|
18
|
+
.option('--max-effects <number>', 'Max useEffects per component', parseInt)
|
|
19
|
+
.option('--max-props <number>', 'Max props before warning', parseInt)
|
|
20
|
+
.option('--max-lines <number>', 'Max lines per component', parseInt)
|
|
21
|
+
.option('--include <patterns>', 'Glob patterns to include (comma-separated)')
|
|
22
|
+
.option('--exclude <patterns>', 'Glob patterns to exclude (comma-separated)')
|
|
23
|
+
.option('-o, --output <file>', 'Write output to file')
|
|
24
|
+
.action(async (directory, options) => {
|
|
25
|
+
const rootDir = path.resolve(process.cwd(), directory);
|
|
26
|
+
// Check if directory exists
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(rootDir);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
console.error(chalk.red(`Error: Directory "${rootDir}" does not exist.`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const spinner = ora('Analyzing React project...').start();
|
|
35
|
+
try {
|
|
36
|
+
// Load config file if specified
|
|
37
|
+
let fileConfig = {};
|
|
38
|
+
if (options.config) {
|
|
39
|
+
try {
|
|
40
|
+
const configPath = path.resolve(process.cwd(), options.config);
|
|
41
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
42
|
+
fileConfig = JSON.parse(configContent);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
spinner.fail(`Could not load config file: ${error.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Build config from options
|
|
50
|
+
const config = {
|
|
51
|
+
...DEFAULT_CONFIG,
|
|
52
|
+
...fileConfig,
|
|
53
|
+
...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
|
|
54
|
+
...(options.maxProps && { maxPropsCount: options.maxProps }),
|
|
55
|
+
...(options.maxLines && { maxComponentLines: options.maxLines }),
|
|
56
|
+
};
|
|
57
|
+
const include = options.include?.split(',').map((p) => p.trim()) || undefined;
|
|
58
|
+
const exclude = options.exclude?.split(',').map((p) => p.trim()) || undefined;
|
|
59
|
+
const result = await analyzeProject({
|
|
60
|
+
rootDir,
|
|
61
|
+
include,
|
|
62
|
+
exclude,
|
|
63
|
+
config,
|
|
64
|
+
});
|
|
65
|
+
spinner.stop();
|
|
66
|
+
const output = reportResults(result, {
|
|
67
|
+
format: options.format,
|
|
68
|
+
showCodeSnippets: options.snippets,
|
|
69
|
+
rootDir,
|
|
70
|
+
});
|
|
71
|
+
if (options.output) {
|
|
72
|
+
const outputPath = path.resolve(process.cwd(), options.output);
|
|
73
|
+
await fs.writeFile(outputPath, output, 'utf-8');
|
|
74
|
+
console.log(chalk.green(`✓ Report written to ${outputPath}`));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(output);
|
|
78
|
+
}
|
|
79
|
+
// Exit with error code if there are errors
|
|
80
|
+
if (result.summary.smellsBySeverity.error > 0) {
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
spinner.fail(`Analysis failed: ${error.message}`);
|
|
86
|
+
if (process.env.DEBUG) {
|
|
87
|
+
console.error(error);
|
|
88
|
+
}
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// Init command to create config file
|
|
93
|
+
program
|
|
94
|
+
.command('init')
|
|
95
|
+
.description('Create a configuration file')
|
|
96
|
+
.action(async () => {
|
|
97
|
+
const configContent = JSON.stringify({
|
|
98
|
+
maxUseEffectsPerComponent: DEFAULT_CONFIG.maxUseEffectsPerComponent,
|
|
99
|
+
maxPropDrillingDepth: DEFAULT_CONFIG.maxPropDrillingDepth,
|
|
100
|
+
maxComponentLines: DEFAULT_CONFIG.maxComponentLines,
|
|
101
|
+
maxPropsCount: DEFAULT_CONFIG.maxPropsCount,
|
|
102
|
+
}, null, 2);
|
|
103
|
+
const configPath = path.join(process.cwd(), '.smellrc.json');
|
|
104
|
+
try {
|
|
105
|
+
await fs.access(configPath);
|
|
106
|
+
console.log(chalk.yellow('Config file already exists at .smellrc.json'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
await fs.writeFile(configPath, configContent, 'utf-8');
|
|
110
|
+
console.log(chalk.green('✓ Created .smellrc.json'));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
program.parse();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { detectUseEffectOveruse } from './useEffect.js';
|
|
2
|
+
export { detectPropDrilling, analyzePropDrillingDepth } from './propDrilling.js';
|
|
3
|
+
export { detectLargeComponent } from './largeComponent.js';
|
|
4
|
+
export { detectUnmemoizedCalculations } from './memoization.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
export declare function detectLargeComponent(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
|
|
4
|
+
//# sourceMappingURL=largeComponent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"largeComponent.d.ts","sourceRoot":"","sources":["../../src/detectors/largeComponent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAsDb"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getCodeSnippet } from '../parser/index.js';
|
|
2
|
+
import { DEFAULT_CONFIG } from '../types/index.js';
|
|
3
|
+
export function detectLargeComponent(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
|
|
4
|
+
const smells = [];
|
|
5
|
+
const lineCount = component.endLine - component.startLine + 1;
|
|
6
|
+
if (lineCount > config.maxComponentLines) {
|
|
7
|
+
smells.push({
|
|
8
|
+
type: 'large-component',
|
|
9
|
+
severity: 'warning',
|
|
10
|
+
message: `Component "${component.name}" has ${lineCount} lines (max recommended: ${config.maxComponentLines})`,
|
|
11
|
+
file: filePath,
|
|
12
|
+
line: component.startLine,
|
|
13
|
+
column: 0,
|
|
14
|
+
suggestion: 'Break this component into smaller, more focused components',
|
|
15
|
+
codeSnippet: getCodeSnippet(sourceCode, component.startLine),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
// Check for complex JSX nesting
|
|
19
|
+
if (component.jsxDepth > 10) {
|
|
20
|
+
smells.push({
|
|
21
|
+
type: 'deep-nesting',
|
|
22
|
+
severity: 'info',
|
|
23
|
+
message: `Component "${component.name}" has deeply nested JSX (depth: ~${component.jsxDepth})`,
|
|
24
|
+
file: filePath,
|
|
25
|
+
line: component.startLine,
|
|
26
|
+
column: 0,
|
|
27
|
+
suggestion: 'Extract nested elements into separate components for better readability',
|
|
28
|
+
codeSnippet: getCodeSnippet(sourceCode, component.startLine),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Check for too many hooks (complexity indicator)
|
|
32
|
+
const totalHooks = component.hooks.useEffect.length +
|
|
33
|
+
component.hooks.useState.length +
|
|
34
|
+
component.hooks.useMemo.length +
|
|
35
|
+
component.hooks.useCallback.length +
|
|
36
|
+
component.hooks.useRef.length +
|
|
37
|
+
component.hooks.custom.length;
|
|
38
|
+
if (totalHooks > 10) {
|
|
39
|
+
smells.push({
|
|
40
|
+
type: 'large-component',
|
|
41
|
+
severity: 'warning',
|
|
42
|
+
message: `Component "${component.name}" uses ${totalHooks} hooks - high complexity`,
|
|
43
|
+
file: filePath,
|
|
44
|
+
line: component.startLine,
|
|
45
|
+
column: 0,
|
|
46
|
+
suggestion: 'Extract related logic into custom hooks',
|
|
47
|
+
codeSnippet: getCodeSnippet(sourceCode, component.startLine),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return smells;
|
|
51
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
export declare function detectUnmemoizedCalculations(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
|
|
4
|
+
//# sourceMappingURL=memoization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memoization.d.ts","sourceRoot":"","sources":["../../src/detectors/memoization.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAsKb"}
|