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.
Files changed (47) hide show
  1. package/README.md +179 -0
  2. package/dist/analyzer.d.ts +10 -0
  3. package/dist/analyzer.d.ts.map +1 -0
  4. package/dist/analyzer.js +169 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +113 -0
  8. package/dist/detectors/index.d.ts +5 -0
  9. package/dist/detectors/index.d.ts.map +1 -0
  10. package/dist/detectors/index.js +4 -0
  11. package/dist/detectors/largeComponent.d.ts +4 -0
  12. package/dist/detectors/largeComponent.d.ts.map +1 -0
  13. package/dist/detectors/largeComponent.js +51 -0
  14. package/dist/detectors/memoization.d.ts +4 -0
  15. package/dist/detectors/memoization.d.ts.map +1 -0
  16. package/dist/detectors/memoization.js +150 -0
  17. package/dist/detectors/propDrilling.d.ts +5 -0
  18. package/dist/detectors/propDrilling.d.ts.map +1 -0
  19. package/dist/detectors/propDrilling.js +82 -0
  20. package/dist/detectors/useEffect.d.ts +4 -0
  21. package/dist/detectors/useEffect.d.ts.map +1 -0
  22. package/dist/detectors/useEffect.js +101 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +4 -0
  26. package/dist/parser/index.d.ts +29 -0
  27. package/dist/parser/index.d.ts.map +1 -0
  28. package/dist/parser/index.js +151 -0
  29. package/dist/reporter.d.ts +8 -0
  30. package/dist/reporter.d.ts.map +1 -0
  31. package/dist/reporter.js +217 -0
  32. package/dist/types/index.d.ts +64 -0
  33. package/dist/types/index.d.ts.map +1 -0
  34. package/dist/types/index.js +7 -0
  35. package/package.json +45 -0
  36. package/src/analyzer.ts +216 -0
  37. package/src/cli.ts +125 -0
  38. package/src/detectors/index.ts +4 -0
  39. package/src/detectors/largeComponent.ts +63 -0
  40. package/src/detectors/memoization.ts +177 -0
  41. package/src/detectors/propDrilling.ts +103 -0
  42. package/src/detectors/useEffect.ts +117 -0
  43. package/src/index.ts +4 -0
  44. package/src/parser/index.ts +195 -0
  45. package/src/reporter.ts +248 -0
  46. package/src/types/index.ts +86 -0
  47. package/tsconfig.json +19 -0
@@ -0,0 +1,217 @@
1
+ import chalk from 'chalk';
2
+ import path from 'path';
3
+ export function reportResults(result, options) {
4
+ switch (options.format) {
5
+ case 'json':
6
+ return JSON.stringify(result, null, 2);
7
+ case 'markdown':
8
+ return formatMarkdown(result, options);
9
+ case 'console':
10
+ default:
11
+ return formatConsole(result, options);
12
+ }
13
+ }
14
+ function formatConsole(result, options) {
15
+ const lines = [];
16
+ const { summary, debtScore, files } = result;
17
+ // Header
18
+ lines.push('');
19
+ lines.push(chalk.bold.cyan('╔══════════════════════════════════════════════════════════════╗'));
20
+ lines.push(chalk.bold.cyan('║') + chalk.bold.white(' 🔍 React Code Smell Detector Report ') + chalk.bold.cyan('║'));
21
+ lines.push(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝'));
22
+ lines.push('');
23
+ // Technical Debt Score
24
+ const gradeColor = getGradeColor(debtScore.grade);
25
+ lines.push(chalk.bold('📊 Technical Debt Score'));
26
+ lines.push('');
27
+ lines.push(` ${chalk.bold('Grade:')} ${gradeColor(debtScore.grade)} ${getScoreBar(debtScore.score)} ${debtScore.score}/100`);
28
+ lines.push('');
29
+ lines.push(chalk.dim(' Breakdown:'));
30
+ lines.push(` ${chalk.yellow('⚡')} useEffect: ${getSmallBar(debtScore.breakdown.useEffectScore)} ${debtScore.breakdown.useEffectScore}`);
31
+ lines.push(` ${chalk.blue('🔗')} Prop Drilling: ${getSmallBar(debtScore.breakdown.propDrillingScore)} ${debtScore.breakdown.propDrillingScore}`);
32
+ lines.push(` ${chalk.magenta('📐')} Component Size:${getSmallBar(debtScore.breakdown.componentSizeScore)} ${debtScore.breakdown.componentSizeScore}`);
33
+ lines.push(` ${chalk.green('💾')} Memoization: ${getSmallBar(debtScore.breakdown.memoizationScore)} ${debtScore.breakdown.memoizationScore}`);
34
+ lines.push('');
35
+ lines.push(` ${chalk.dim('Estimated refactor time:')} ${chalk.yellow(debtScore.estimatedRefactorTime)}`);
36
+ lines.push('');
37
+ // Summary
38
+ lines.push(chalk.bold('📈 Summary'));
39
+ lines.push('');
40
+ lines.push(` Files analyzed: ${chalk.cyan(summary.totalFiles)}`);
41
+ lines.push(` Components found: ${chalk.cyan(summary.totalComponents)}`);
42
+ lines.push(` Total issues: ${getSeverityLabel(summary.totalSmells, summary.smellsBySeverity)}`);
43
+ lines.push('');
44
+ // Issues by type
45
+ if (summary.totalSmells > 0) {
46
+ lines.push(chalk.bold('🏷️ Issues by Type'));
47
+ lines.push('');
48
+ Object.entries(summary.smellsByType).forEach(([type, count]) => {
49
+ if (count > 0) {
50
+ lines.push(` ${chalk.dim('•')} ${formatSmellType(type)}: ${chalk.yellow(count)}`);
51
+ }
52
+ });
53
+ lines.push('');
54
+ }
55
+ // Detailed findings
56
+ if (files.some(f => f.smells.length > 0)) {
57
+ lines.push(chalk.bold('📋 Detailed Findings'));
58
+ lines.push('');
59
+ files.forEach(file => {
60
+ if (file.smells.length === 0)
61
+ return;
62
+ const relativePath = path.relative(options.rootDir, file.file);
63
+ lines.push(chalk.bold.underline(relativePath));
64
+ lines.push('');
65
+ file.smells.forEach(smell => {
66
+ const icon = getSeverityIcon(smell.severity);
67
+ const color = getSeverityColor(smell.severity);
68
+ lines.push(` ${icon} ${color(smell.message)}`);
69
+ lines.push(` ${chalk.dim('Line')} ${smell.line} ${chalk.dim('•')} ${chalk.italic.cyan(smell.suggestion)}`);
70
+ if (options.showCodeSnippets && smell.codeSnippet) {
71
+ lines.push('');
72
+ smell.codeSnippet.split('\n').forEach(line => {
73
+ if (line.startsWith('>')) {
74
+ lines.push(chalk.red(line));
75
+ }
76
+ else {
77
+ lines.push(chalk.dim(line));
78
+ }
79
+ });
80
+ }
81
+ lines.push('');
82
+ });
83
+ });
84
+ }
85
+ // Footer
86
+ if (summary.totalSmells === 0) {
87
+ lines.push(chalk.green.bold('✨ No code smells detected! Your code looks great.'));
88
+ }
89
+ else {
90
+ lines.push(chalk.dim('─'.repeat(64)));
91
+ lines.push(chalk.dim(`Found ${summary.totalSmells} issue(s). Run with --help for more options.`));
92
+ }
93
+ lines.push('');
94
+ return lines.join('\n');
95
+ }
96
+ function formatMarkdown(result, options) {
97
+ const lines = [];
98
+ const { summary, debtScore, files } = result;
99
+ lines.push('# React Code Smell Detector Report');
100
+ lines.push('');
101
+ lines.push('## Technical Debt Score');
102
+ lines.push('');
103
+ lines.push(`| Metric | Score |`);
104
+ lines.push(`|--------|-------|`);
105
+ lines.push(`| **Overall Grade** | **${debtScore.grade}** (${debtScore.score}/100) |`);
106
+ lines.push(`| useEffect Usage | ${debtScore.breakdown.useEffectScore}/100 |`);
107
+ lines.push(`| Prop Drilling | ${debtScore.breakdown.propDrillingScore}/100 |`);
108
+ lines.push(`| Component Size | ${debtScore.breakdown.componentSizeScore}/100 |`);
109
+ lines.push(`| Memoization | ${debtScore.breakdown.memoizationScore}/100 |`);
110
+ lines.push('');
111
+ lines.push(`**Estimated Refactor Time:** ${debtScore.estimatedRefactorTime}`);
112
+ lines.push('');
113
+ lines.push('## Summary');
114
+ lines.push('');
115
+ lines.push(`- **Files analyzed:** ${summary.totalFiles}`);
116
+ lines.push(`- **Components found:** ${summary.totalComponents}`);
117
+ lines.push(`- **Total issues:** ${summary.totalSmells}`);
118
+ lines.push(` - Errors: ${summary.smellsBySeverity.error}`);
119
+ lines.push(` - Warnings: ${summary.smellsBySeverity.warning}`);
120
+ lines.push(` - Info: ${summary.smellsBySeverity.info}`);
121
+ lines.push('');
122
+ if (summary.totalSmells > 0) {
123
+ lines.push('## Issues by Type');
124
+ lines.push('');
125
+ Object.entries(summary.smellsByType).forEach(([type, count]) => {
126
+ if (count > 0) {
127
+ lines.push(`- ${formatSmellType(type)}: ${count}`);
128
+ }
129
+ });
130
+ lines.push('');
131
+ lines.push('## Detailed Findings');
132
+ lines.push('');
133
+ files.forEach(file => {
134
+ if (file.smells.length === 0)
135
+ return;
136
+ const relativePath = path.relative(options.rootDir, file.file);
137
+ lines.push(`### ${relativePath}`);
138
+ lines.push('');
139
+ file.smells.forEach(smell => {
140
+ const icon = smell.severity === 'error' ? '🔴' : smell.severity === 'warning' ? '🟡' : '🔵';
141
+ lines.push(`#### ${icon} ${smell.message}`);
142
+ lines.push('');
143
+ lines.push(`- **Line:** ${smell.line}`);
144
+ lines.push(`- **Severity:** ${smell.severity}`);
145
+ lines.push(`- **Suggestion:** ${smell.suggestion}`);
146
+ if (options.showCodeSnippets && smell.codeSnippet) {
147
+ lines.push('');
148
+ lines.push('```tsx');
149
+ lines.push(smell.codeSnippet);
150
+ lines.push('```');
151
+ }
152
+ lines.push('');
153
+ });
154
+ });
155
+ }
156
+ return lines.join('\n');
157
+ }
158
+ // Helper functions
159
+ function getGradeColor(grade) {
160
+ switch (grade) {
161
+ case 'A': return chalk.green.bold;
162
+ case 'B': return chalk.greenBright.bold;
163
+ case 'C': return chalk.yellow.bold;
164
+ case 'D': return chalk.rgb(255, 165, 0).bold;
165
+ case 'F': return chalk.red.bold;
166
+ default: return chalk.white.bold;
167
+ }
168
+ }
169
+ function getScoreBar(score) {
170
+ const filled = Math.round(score / 5);
171
+ const empty = 20 - filled;
172
+ const color = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
173
+ return color('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
174
+ }
175
+ function getSmallBar(score) {
176
+ const filled = Math.round(score / 10);
177
+ const empty = 10 - filled;
178
+ const color = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
179
+ return color('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
180
+ }
181
+ function getSeverityIcon(severity) {
182
+ switch (severity) {
183
+ case 'error': return chalk.red('✖');
184
+ case 'warning': return chalk.yellow('⚠');
185
+ case 'info': return chalk.blue('ℹ');
186
+ }
187
+ }
188
+ function getSeverityColor(severity) {
189
+ switch (severity) {
190
+ case 'error': return chalk.red;
191
+ case 'warning': return chalk.yellow;
192
+ case 'info': return chalk.blue;
193
+ }
194
+ }
195
+ function getSeverityLabel(total, bySeverity) {
196
+ const parts = [];
197
+ if (bySeverity.error > 0)
198
+ parts.push(chalk.red(`${bySeverity.error} error(s)`));
199
+ if (bySeverity.warning > 0)
200
+ parts.push(chalk.yellow(`${bySeverity.warning} warning(s)`));
201
+ if (bySeverity.info > 0)
202
+ parts.push(chalk.blue(`${bySeverity.info} info`));
203
+ return parts.length > 0 ? parts.join(', ') : chalk.green('0');
204
+ }
205
+ function formatSmellType(type) {
206
+ const labels = {
207
+ 'useEffect-overuse': '⚡ useEffect Overuse',
208
+ 'prop-drilling': '🔗 Prop Drilling',
209
+ 'large-component': '📐 Large Component',
210
+ 'unmemoized-calculation': '💾 Unmemoized Calculation',
211
+ 'missing-dependency': '🔍 Missing Dependency',
212
+ 'state-in-loop': '🔄 State in Loop',
213
+ 'inline-function-prop': '📎 Inline Function Prop',
214
+ 'deep-nesting': '📊 Deep Nesting',
215
+ };
216
+ return labels[type] || type;
217
+ }
@@ -0,0 +1,64 @@
1
+ export type SmellSeverity = 'error' | 'warning' | 'info';
2
+ export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting';
3
+ export interface CodeSmell {
4
+ type: SmellType;
5
+ severity: SmellSeverity;
6
+ message: string;
7
+ file: string;
8
+ line: number;
9
+ column: number;
10
+ suggestion: string;
11
+ codeSnippet?: string;
12
+ }
13
+ export interface ComponentInfo {
14
+ name: string;
15
+ file: string;
16
+ startLine: number;
17
+ endLine: number;
18
+ lineCount: number;
19
+ useEffectCount: number;
20
+ useStateCount: number;
21
+ useMemoCount: number;
22
+ useCallbackCount: number;
23
+ propsCount: number;
24
+ propsDrillingDepth: number;
25
+ hasExpensiveCalculation: boolean;
26
+ }
27
+ export interface FileAnalysis {
28
+ file: string;
29
+ components: ComponentInfo[];
30
+ smells: CodeSmell[];
31
+ imports: string[];
32
+ }
33
+ export interface AnalysisResult {
34
+ files: FileAnalysis[];
35
+ summary: AnalysisSummary;
36
+ debtScore: TechnicalDebtScore;
37
+ }
38
+ export interface AnalysisSummary {
39
+ totalFiles: number;
40
+ totalComponents: number;
41
+ totalSmells: number;
42
+ smellsByType: Record<SmellType, number>;
43
+ smellsBySeverity: Record<SmellSeverity, number>;
44
+ }
45
+ export interface TechnicalDebtScore {
46
+ score: number;
47
+ grade: 'A' | 'B' | 'C' | 'D' | 'F';
48
+ breakdown: {
49
+ useEffectScore: number;
50
+ propDrillingScore: number;
51
+ componentSizeScore: number;
52
+ memoizationScore: number;
53
+ };
54
+ estimatedRefactorTime: string;
55
+ }
56
+ export interface DetectorConfig {
57
+ maxUseEffectsPerComponent: number;
58
+ maxPropDrillingDepth: number;
59
+ maxComponentLines: number;
60
+ maxPropsCount: number;
61
+ checkMemoization: boolean;
62
+ }
63
+ export declare const DEFAULT_CONFIG: DetectorConfig;
64
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,MAAM,MAAM,SAAS,GACjB,mBAAmB,GACnB,eAAe,GACf,iBAAiB,GACjB,wBAAwB,GACxB,oBAAoB,GACpB,eAAe,GACf,sBAAsB,GACtB,cAAc,CAAC;AAEnB,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,kBAAkB,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACxC,gBAAgB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACnC,SAAS,EAAE;QACT,cAAc,EAAE,MAAM,CAAC;QACvB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,yBAAyB,EAAE,MAAM,CAAC;IAClC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,eAAO,MAAM,cAAc,EAAE,cAM5B,CAAC"}
@@ -0,0 +1,7 @@
1
+ export const DEFAULT_CONFIG = {
2
+ maxUseEffectsPerComponent: 3,
3
+ maxPropDrillingDepth: 3,
4
+ maxComponentLines: 300,
5
+ maxPropsCount: 7,
6
+ checkMemoization: true,
7
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "react-code-smell-detector",
3
+ "version": "1.0.0",
4
+ "description": "Detect code smells in React projects - useEffect overuse, prop drilling, large components, and more",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "react-smell": "dist/cli.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/cli.js",
14
+ "test": "vitest"
15
+ },
16
+ "keywords": [
17
+ "react",
18
+ "code-smell",
19
+ "linter",
20
+ "static-analysis",
21
+ "useEffect",
22
+ "prop-drilling",
23
+ "technical-debt"
24
+ ],
25
+ "author": "vsthakur101",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@babel/parser": "^7.23.0",
29
+ "@babel/traverse": "^7.23.0",
30
+ "@babel/types": "^7.23.0",
31
+ "chalk": "^5.3.0",
32
+ "commander": "^11.1.0",
33
+ "fast-glob": "^3.3.2",
34
+ "ora": "^8.0.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/babel__traverse": "^7.20.4",
38
+ "@types/node": "^20.10.0",
39
+ "typescript": "^5.3.0",
40
+ "vitest": "^1.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ }
45
+ }
@@ -0,0 +1,216 @@
1
+ import fg from 'fast-glob';
2
+ import path from 'path';
3
+ import { parseFile, ParseResult } from './parser/index.js';
4
+ import {
5
+ detectUseEffectOveruse,
6
+ detectPropDrilling,
7
+ analyzePropDrillingDepth,
8
+ detectLargeComponent,
9
+ detectUnmemoizedCalculations,
10
+ } from './detectors/index.js';
11
+ import {
12
+ AnalysisResult,
13
+ FileAnalysis,
14
+ ComponentInfo,
15
+ CodeSmell,
16
+ AnalysisSummary,
17
+ TechnicalDebtScore,
18
+ DetectorConfig,
19
+ DEFAULT_CONFIG,
20
+ SmellType,
21
+ SmellSeverity,
22
+ } from './types/index.js';
23
+
24
+ export interface AnalyzerOptions {
25
+ rootDir: string;
26
+ include?: string[];
27
+ exclude?: string[];
28
+ config?: Partial<DetectorConfig>;
29
+ }
30
+
31
+ export async function analyzeProject(options: AnalyzerOptions): Promise<AnalysisResult> {
32
+ const {
33
+ rootDir,
34
+ include = ['**/*.tsx', '**/*.jsx'],
35
+ exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'],
36
+ config: userConfig = {},
37
+ } = options;
38
+
39
+ const config: DetectorConfig = { ...DEFAULT_CONFIG, ...userConfig };
40
+
41
+ // Find all React files
42
+ const patterns = include.map(p => path.join(rootDir, p));
43
+ const files = await fg(patterns, {
44
+ ignore: exclude,
45
+ absolute: true,
46
+ });
47
+
48
+ const fileAnalyses: FileAnalysis[] = [];
49
+
50
+ // Analyze each file
51
+ for (const file of files) {
52
+ try {
53
+ const parseResult = await parseFile(file);
54
+ const analysis = analyzeFile(parseResult, file, config);
55
+ if (analysis.components.length > 0 || analysis.smells.length > 0) {
56
+ fileAnalyses.push(analysis);
57
+ }
58
+ } catch (error) {
59
+ // Skip files that can't be parsed
60
+ console.warn(`Warning: Could not parse ${file}: ${(error as Error).message}`);
61
+ }
62
+ }
63
+
64
+ // Calculate summary and score
65
+ const summary = calculateSummary(fileAnalyses);
66
+ const debtScore = calculateTechnicalDebtScore(fileAnalyses, summary);
67
+
68
+ return {
69
+ files: fileAnalyses,
70
+ summary,
71
+ debtScore,
72
+ };
73
+ }
74
+
75
+ function analyzeFile(parseResult: ParseResult, filePath: string, config: DetectorConfig): FileAnalysis {
76
+ const { components, imports, sourceCode } = parseResult;
77
+ const smells: CodeSmell[] = [];
78
+ const componentInfos: ComponentInfo[] = [];
79
+
80
+ // Run all detectors on each component
81
+ components.forEach(component => {
82
+ // Collect component info
83
+ componentInfos.push({
84
+ name: component.name,
85
+ file: filePath,
86
+ startLine: component.startLine,
87
+ endLine: component.endLine,
88
+ lineCount: component.endLine - component.startLine + 1,
89
+ useEffectCount: component.hooks.useEffect.length,
90
+ useStateCount: component.hooks.useState.length,
91
+ useMemoCount: component.hooks.useMemo.length,
92
+ useCallbackCount: component.hooks.useCallback.length,
93
+ propsCount: component.props.length,
94
+ propsDrillingDepth: 0, // Calculated separately
95
+ hasExpensiveCalculation: false, // Will be set by memoization detector
96
+ });
97
+
98
+ // Run detectors
99
+ smells.push(...detectUseEffectOveruse(component, filePath, sourceCode, config));
100
+ smells.push(...detectPropDrilling(component, filePath, sourceCode, config));
101
+ smells.push(...detectLargeComponent(component, filePath, sourceCode, config));
102
+ smells.push(...detectUnmemoizedCalculations(component, filePath, sourceCode, config));
103
+ });
104
+
105
+ // Run cross-component analysis
106
+ smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
107
+
108
+ return {
109
+ file: filePath,
110
+ components: componentInfos,
111
+ smells,
112
+ imports,
113
+ };
114
+ }
115
+
116
+ function calculateSummary(files: FileAnalysis[]): AnalysisSummary {
117
+ const smellsByType: Record<SmellType, number> = {
118
+ 'useEffect-overuse': 0,
119
+ 'prop-drilling': 0,
120
+ 'large-component': 0,
121
+ 'unmemoized-calculation': 0,
122
+ 'missing-dependency': 0,
123
+ 'state-in-loop': 0,
124
+ 'inline-function-prop': 0,
125
+ 'deep-nesting': 0,
126
+ };
127
+
128
+ const smellsBySeverity: Record<SmellSeverity, number> = {
129
+ error: 0,
130
+ warning: 0,
131
+ info: 0,
132
+ };
133
+
134
+ let totalSmells = 0;
135
+ let totalComponents = 0;
136
+
137
+ files.forEach(file => {
138
+ totalComponents += file.components.length;
139
+ file.smells.forEach(smell => {
140
+ totalSmells++;
141
+ smellsByType[smell.type]++;
142
+ smellsBySeverity[smell.severity]++;
143
+ });
144
+ });
145
+
146
+ return {
147
+ totalFiles: files.length,
148
+ totalComponents,
149
+ totalSmells,
150
+ smellsByType,
151
+ smellsBySeverity,
152
+ };
153
+ }
154
+
155
+ function calculateTechnicalDebtScore(files: FileAnalysis[], summary: AnalysisSummary): TechnicalDebtScore {
156
+ // Calculate individual scores (0-100, higher is better)
157
+
158
+ // useEffect score: penalize based on useEffect-related issues
159
+ const useEffectIssues = summary.smellsByType['useEffect-overuse'];
160
+ const useEffectScore = Math.max(0, 100 - useEffectIssues * 15);
161
+
162
+ // Prop drilling score
163
+ const propDrillingIssues = summary.smellsByType['prop-drilling'];
164
+ const propDrillingScore = Math.max(0, 100 - propDrillingIssues * 12);
165
+
166
+ // Component size score
167
+ const sizeIssues = summary.smellsByType['large-component'] + summary.smellsByType['deep-nesting'];
168
+ const componentSizeScore = Math.max(0, 100 - sizeIssues * 10);
169
+
170
+ // Memoization score
171
+ const memoIssues = summary.smellsByType['unmemoized-calculation'] + summary.smellsByType['inline-function-prop'];
172
+ const memoizationScore = Math.max(0, 100 - memoIssues * 8);
173
+
174
+ // Overall score (weighted average)
175
+ const score = Math.round(
176
+ useEffectScore * 0.3 +
177
+ propDrillingScore * 0.25 +
178
+ componentSizeScore * 0.25 +
179
+ memoizationScore * 0.2
180
+ );
181
+
182
+ // Determine grade
183
+ let grade: 'A' | 'B' | 'C' | 'D' | 'F';
184
+ if (score >= 90) grade = 'A';
185
+ else if (score >= 80) grade = 'B';
186
+ else if (score >= 70) grade = 'C';
187
+ else if (score >= 60) grade = 'D';
188
+ else grade = 'F';
189
+
190
+ // Estimate refactor time
191
+ const errorCount = summary.smellsBySeverity.error;
192
+ const warningCount = summary.smellsBySeverity.warning;
193
+ const totalIssues = errorCount * 30 + warningCount * 15; // minutes
194
+
195
+ let estimatedRefactorTime: string;
196
+ if (totalIssues < 30) estimatedRefactorTime = '< 30 minutes';
197
+ else if (totalIssues < 60) estimatedRefactorTime = '30 min - 1 hour';
198
+ else if (totalIssues < 120) estimatedRefactorTime = '1-2 hours';
199
+ else if (totalIssues < 240) estimatedRefactorTime = '2-4 hours';
200
+ else if (totalIssues < 480) estimatedRefactorTime = '4-8 hours';
201
+ else estimatedRefactorTime = '> 1 day';
202
+
203
+ return {
204
+ score,
205
+ grade,
206
+ breakdown: {
207
+ useEffectScore,
208
+ propDrillingScore,
209
+ componentSizeScore,
210
+ memoizationScore,
211
+ },
212
+ estimatedRefactorTime,
213
+ };
214
+ }
215
+
216
+ export { DEFAULT_CONFIG, DetectorConfig } from './types/index.js';
package/src/cli.ts ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import path from 'path';
7
+ import { analyzeProject, DEFAULT_CONFIG } from './analyzer.js';
8
+ import { reportResults } from './reporter.js';
9
+ import fs from 'fs/promises';
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('react-smell')
15
+ .description('Detect code smells in React projects')
16
+ .version('1.0.0')
17
+ .argument('[directory]', 'Directory to analyze', '.')
18
+ .option('-f, --format <format>', 'Output format: console, json, markdown', 'console')
19
+ .option('-s, --snippets', 'Show code snippets in output', false)
20
+ .option('-c, --config <file>', 'Path to config file')
21
+ .option('--max-effects <number>', 'Max useEffects per component', parseInt)
22
+ .option('--max-props <number>', 'Max props before warning', parseInt)
23
+ .option('--max-lines <number>', 'Max lines per component', parseInt)
24
+ .option('--include <patterns>', 'Glob patterns to include (comma-separated)')
25
+ .option('--exclude <patterns>', 'Glob patterns to exclude (comma-separated)')
26
+ .option('-o, --output <file>', 'Write output to file')
27
+ .action(async (directory, options) => {
28
+ const rootDir = path.resolve(process.cwd(), directory);
29
+
30
+ // Check if directory exists
31
+ try {
32
+ await fs.access(rootDir);
33
+ } catch {
34
+ console.error(chalk.red(`Error: Directory "${rootDir}" does not exist.`));
35
+ process.exit(1);
36
+ }
37
+
38
+ const spinner = ora('Analyzing React project...').start();
39
+
40
+ try {
41
+ // Load config file if specified
42
+ let fileConfig = {};
43
+ if (options.config) {
44
+ try {
45
+ const configPath = path.resolve(process.cwd(), options.config);
46
+ const configContent = await fs.readFile(configPath, 'utf-8');
47
+ fileConfig = JSON.parse(configContent);
48
+ } catch (error) {
49
+ spinner.fail(`Could not load config file: ${(error as Error).message}`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ // Build config from options
55
+ const config = {
56
+ ...DEFAULT_CONFIG,
57
+ ...fileConfig,
58
+ ...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
59
+ ...(options.maxProps && { maxPropsCount: options.maxProps }),
60
+ ...(options.maxLines && { maxComponentLines: options.maxLines }),
61
+ };
62
+
63
+ const include = options.include?.split(',').map((p: string) => p.trim()) || undefined;
64
+ const exclude = options.exclude?.split(',').map((p: string) => p.trim()) || undefined;
65
+
66
+ const result = await analyzeProject({
67
+ rootDir,
68
+ include,
69
+ exclude,
70
+ config,
71
+ });
72
+
73
+ spinner.stop();
74
+
75
+ const output = reportResults(result, {
76
+ format: options.format,
77
+ showCodeSnippets: options.snippets,
78
+ rootDir,
79
+ });
80
+
81
+ if (options.output) {
82
+ const outputPath = path.resolve(process.cwd(), options.output);
83
+ await fs.writeFile(outputPath, output, 'utf-8');
84
+ console.log(chalk.green(`✓ Report written to ${outputPath}`));
85
+ } else {
86
+ console.log(output);
87
+ }
88
+
89
+ // Exit with error code if there are errors
90
+ if (result.summary.smellsBySeverity.error > 0) {
91
+ process.exit(1);
92
+ }
93
+ } catch (error) {
94
+ spinner.fail(`Analysis failed: ${(error as Error).message}`);
95
+ if (process.env.DEBUG) {
96
+ console.error(error);
97
+ }
98
+ process.exit(1);
99
+ }
100
+ });
101
+
102
+ // Init command to create config file
103
+ program
104
+ .command('init')
105
+ .description('Create a configuration file')
106
+ .action(async () => {
107
+ const configContent = JSON.stringify({
108
+ maxUseEffectsPerComponent: DEFAULT_CONFIG.maxUseEffectsPerComponent,
109
+ maxPropDrillingDepth: DEFAULT_CONFIG.maxPropDrillingDepth,
110
+ maxComponentLines: DEFAULT_CONFIG.maxComponentLines,
111
+ maxPropsCount: DEFAULT_CONFIG.maxPropsCount,
112
+ }, null, 2);
113
+
114
+ const configPath = path.join(process.cwd(), '.smellrc.json');
115
+
116
+ try {
117
+ await fs.access(configPath);
118
+ console.log(chalk.yellow('Config file already exists at .smellrc.json'));
119
+ } catch {
120
+ await fs.writeFile(configPath, configContent, 'utf-8');
121
+ console.log(chalk.green('✓ Created .smellrc.json'));
122
+ }
123
+ });
124
+
125
+ program.parse();
@@ -0,0 +1,4 @@
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';