react-code-smell-detector 1.1.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -11
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +65 -2
- package/dist/cli.js +134 -33
- package/dist/detectors/accessibility.d.ts +12 -0
- package/dist/detectors/accessibility.d.ts.map +1 -0
- package/dist/detectors/accessibility.js +191 -0
- package/dist/detectors/complexity.d.ts +17 -0
- package/dist/detectors/complexity.d.ts.map +1 -0
- package/dist/detectors/complexity.js +69 -0
- package/dist/detectors/debug.d.ts +10 -0
- package/dist/detectors/debug.d.ts.map +1 -0
- package/dist/detectors/debug.js +87 -0
- package/dist/detectors/imports.d.ts +22 -0
- package/dist/detectors/imports.d.ts.map +1 -0
- package/dist/detectors/imports.js +210 -0
- package/dist/detectors/index.d.ts +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +8 -0
- package/dist/detectors/memoryLeak.d.ts +7 -0
- package/dist/detectors/memoryLeak.d.ts.map +1 -0
- package/dist/detectors/memoryLeak.js +111 -0
- package/dist/detectors/security.d.ts +12 -0
- package/dist/detectors/security.d.ts.map +1 -0
- package/dist/detectors/security.js +161 -0
- package/dist/fixer.d.ts +23 -0
- package/dist/fixer.d.ts.map +1 -0
- package/dist/fixer.js +133 -0
- package/dist/git.d.ts +28 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +117 -0
- package/dist/htmlReporter.d.ts +6 -0
- package/dist/htmlReporter.d.ts.map +1 -0
- package/dist/htmlReporter.js +453 -0
- package/dist/reporter.js +26 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +13 -0
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +89 -0
- package/package.json +8 -2
- package/src/analyzer.ts +0 -269
- package/src/cli.ts +0 -125
- package/src/detectors/deadCode.ts +0 -163
- package/src/detectors/dependencyArray.ts +0 -176
- package/src/detectors/hooksRules.ts +0 -101
- package/src/detectors/index.ts +0 -16
- package/src/detectors/javascript.ts +0 -169
- package/src/detectors/largeComponent.ts +0 -63
- package/src/detectors/magicValues.ts +0 -114
- package/src/detectors/memoization.ts +0 -177
- package/src/detectors/missingKey.ts +0 -105
- package/src/detectors/nestedTernary.ts +0 -75
- package/src/detectors/nextjs.ts +0 -124
- package/src/detectors/nodejs.ts +0 -199
- package/src/detectors/propDrilling.ts +0 -103
- package/src/detectors/reactNative.ts +0 -154
- package/src/detectors/typescript.ts +0 -151
- package/src/detectors/useEffect.ts +0 -117
- package/src/index.ts +0 -4
- package/src/parser/index.ts +0 -195
- package/src/reporter.ts +0 -278
- package/src/types/index.ts +0 -144
- package/tsconfig.json +0 -19
package/src/analyzer.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
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
|
-
detectMissingKeys,
|
|
11
|
-
detectHooksRulesViolations,
|
|
12
|
-
detectDependencyArrayIssues,
|
|
13
|
-
detectNestedTernaries,
|
|
14
|
-
detectDeadCode,
|
|
15
|
-
detectMagicValues,
|
|
16
|
-
detectNextjsIssues,
|
|
17
|
-
detectReactNativeIssues,
|
|
18
|
-
detectNodejsIssues,
|
|
19
|
-
detectJavascriptIssues,
|
|
20
|
-
detectTypescriptIssues,
|
|
21
|
-
} from './detectors/index.js';
|
|
22
|
-
import {
|
|
23
|
-
AnalysisResult,
|
|
24
|
-
FileAnalysis,
|
|
25
|
-
ComponentInfo,
|
|
26
|
-
CodeSmell,
|
|
27
|
-
AnalysisSummary,
|
|
28
|
-
TechnicalDebtScore,
|
|
29
|
-
DetectorConfig,
|
|
30
|
-
DEFAULT_CONFIG,
|
|
31
|
-
SmellType,
|
|
32
|
-
SmellSeverity,
|
|
33
|
-
} from './types/index.js';
|
|
34
|
-
|
|
35
|
-
export interface AnalyzerOptions {
|
|
36
|
-
rootDir: string;
|
|
37
|
-
include?: string[];
|
|
38
|
-
exclude?: string[];
|
|
39
|
-
config?: Partial<DetectorConfig>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function analyzeProject(options: AnalyzerOptions): Promise<AnalysisResult> {
|
|
43
|
-
const {
|
|
44
|
-
rootDir,
|
|
45
|
-
include = ['**/*.tsx', '**/*.jsx'],
|
|
46
|
-
exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'],
|
|
47
|
-
config: userConfig = {},
|
|
48
|
-
} = options;
|
|
49
|
-
|
|
50
|
-
const config: DetectorConfig = { ...DEFAULT_CONFIG, ...userConfig };
|
|
51
|
-
|
|
52
|
-
// Find all React files
|
|
53
|
-
const patterns = include.map(p => path.join(rootDir, p));
|
|
54
|
-
const files = await fg(patterns, {
|
|
55
|
-
ignore: exclude,
|
|
56
|
-
absolute: true,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const fileAnalyses: FileAnalysis[] = [];
|
|
60
|
-
|
|
61
|
-
// Analyze each file
|
|
62
|
-
for (const file of files) {
|
|
63
|
-
try {
|
|
64
|
-
const parseResult = await parseFile(file);
|
|
65
|
-
const analysis = analyzeFile(parseResult, file, config);
|
|
66
|
-
if (analysis.components.length > 0 || analysis.smells.length > 0) {
|
|
67
|
-
fileAnalyses.push(analysis);
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
// Skip files that can't be parsed
|
|
71
|
-
console.warn(`Warning: Could not parse ${file}: ${(error as Error).message}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Calculate summary and score
|
|
76
|
-
const summary = calculateSummary(fileAnalyses);
|
|
77
|
-
const debtScore = calculateTechnicalDebtScore(fileAnalyses, summary);
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
files: fileAnalyses,
|
|
81
|
-
summary,
|
|
82
|
-
debtScore,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function analyzeFile(parseResult: ParseResult, filePath: string, config: DetectorConfig): FileAnalysis {
|
|
87
|
-
const { components, imports, sourceCode } = parseResult;
|
|
88
|
-
const smells: CodeSmell[] = [];
|
|
89
|
-
const componentInfos: ComponentInfo[] = [];
|
|
90
|
-
|
|
91
|
-
// Run all detectors on each component
|
|
92
|
-
components.forEach(component => {
|
|
93
|
-
// Collect component info
|
|
94
|
-
componentInfos.push({
|
|
95
|
-
name: component.name,
|
|
96
|
-
file: filePath,
|
|
97
|
-
startLine: component.startLine,
|
|
98
|
-
endLine: component.endLine,
|
|
99
|
-
lineCount: component.endLine - component.startLine + 1,
|
|
100
|
-
useEffectCount: component.hooks.useEffect.length,
|
|
101
|
-
useStateCount: component.hooks.useState.length,
|
|
102
|
-
useMemoCount: component.hooks.useMemo.length,
|
|
103
|
-
useCallbackCount: component.hooks.useCallback.length,
|
|
104
|
-
propsCount: component.props.length,
|
|
105
|
-
propsDrillingDepth: 0, // Calculated separately
|
|
106
|
-
hasExpensiveCalculation: false, // Will be set by memoization detector
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Run detectors
|
|
110
|
-
smells.push(...detectUseEffectOveruse(component, filePath, sourceCode, config));
|
|
111
|
-
smells.push(...detectPropDrilling(component, filePath, sourceCode, config));
|
|
112
|
-
smells.push(...detectLargeComponent(component, filePath, sourceCode, config));
|
|
113
|
-
smells.push(...detectUnmemoizedCalculations(component, filePath, sourceCode, config));
|
|
114
|
-
smells.push(...detectMissingKeys(component, filePath, sourceCode, config));
|
|
115
|
-
smells.push(...detectHooksRulesViolations(component, filePath, sourceCode, config));
|
|
116
|
-
smells.push(...detectDependencyArrayIssues(component, filePath, sourceCode, config));
|
|
117
|
-
smells.push(...detectNestedTernaries(component, filePath, sourceCode, config));
|
|
118
|
-
smells.push(...detectDeadCode(component, filePath, sourceCode, config));
|
|
119
|
-
smells.push(...detectMagicValues(component, filePath, sourceCode, config));
|
|
120
|
-
// Framework-specific detectors
|
|
121
|
-
smells.push(...detectNextjsIssues(component, filePath, sourceCode, config, imports));
|
|
122
|
-
smells.push(...detectReactNativeIssues(component, filePath, sourceCode, config, imports));
|
|
123
|
-
smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
|
|
124
|
-
smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
|
|
125
|
-
smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Run cross-component analysis
|
|
129
|
-
smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
file: filePath,
|
|
133
|
-
components: componentInfos,
|
|
134
|
-
smells,
|
|
135
|
-
imports,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function calculateSummary(files: FileAnalysis[]): AnalysisSummary {
|
|
140
|
-
const smellsByType: Record<SmellType, number> = {
|
|
141
|
-
'useEffect-overuse': 0,
|
|
142
|
-
'prop-drilling': 0,
|
|
143
|
-
'large-component': 0,
|
|
144
|
-
'unmemoized-calculation': 0,
|
|
145
|
-
'missing-dependency': 0,
|
|
146
|
-
'state-in-loop': 0,
|
|
147
|
-
'inline-function-prop': 0,
|
|
148
|
-
'deep-nesting': 0,
|
|
149
|
-
'missing-key': 0,
|
|
150
|
-
'hooks-rules-violation': 0,
|
|
151
|
-
'dependency-array-issue': 0,
|
|
152
|
-
'nested-ternary': 0,
|
|
153
|
-
'dead-code': 0,
|
|
154
|
-
'magic-value': 0,
|
|
155
|
-
// Next.js
|
|
156
|
-
'nextjs-client-server-boundary': 0,
|
|
157
|
-
'nextjs-missing-metadata': 0,
|
|
158
|
-
'nextjs-image-unoptimized': 0,
|
|
159
|
-
'nextjs-router-misuse': 0,
|
|
160
|
-
// React Native
|
|
161
|
-
'rn-inline-style': 0,
|
|
162
|
-
'rn-missing-accessibility': 0,
|
|
163
|
-
'rn-performance-issue': 0,
|
|
164
|
-
// Node.js
|
|
165
|
-
'nodejs-callback-hell': 0,
|
|
166
|
-
'nodejs-unhandled-promise': 0,
|
|
167
|
-
'nodejs-sync-io': 0,
|
|
168
|
-
'nodejs-missing-error-handling': 0,
|
|
169
|
-
// JavaScript
|
|
170
|
-
'js-var-usage': 0,
|
|
171
|
-
'js-loose-equality': 0,
|
|
172
|
-
'js-implicit-coercion': 0,
|
|
173
|
-
'js-global-pollution': 0,
|
|
174
|
-
// TypeScript
|
|
175
|
-
'ts-any-usage': 0,
|
|
176
|
-
'ts-missing-return-type': 0,
|
|
177
|
-
'ts-non-null-assertion': 0,
|
|
178
|
-
'ts-type-assertion': 0,
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const smellsBySeverity: Record<SmellSeverity, number> = {
|
|
182
|
-
error: 0,
|
|
183
|
-
warning: 0,
|
|
184
|
-
info: 0,
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
let totalSmells = 0;
|
|
188
|
-
let totalComponents = 0;
|
|
189
|
-
|
|
190
|
-
files.forEach(file => {
|
|
191
|
-
totalComponents += file.components.length;
|
|
192
|
-
file.smells.forEach(smell => {
|
|
193
|
-
totalSmells++;
|
|
194
|
-
smellsByType[smell.type]++;
|
|
195
|
-
smellsBySeverity[smell.severity]++;
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
totalFiles: files.length,
|
|
201
|
-
totalComponents,
|
|
202
|
-
totalSmells,
|
|
203
|
-
smellsByType,
|
|
204
|
-
smellsBySeverity,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function calculateTechnicalDebtScore(files: FileAnalysis[], summary: AnalysisSummary): TechnicalDebtScore {
|
|
209
|
-
// Calculate individual scores (0-100, higher is better)
|
|
210
|
-
|
|
211
|
-
// useEffect score: penalize based on useEffect-related issues
|
|
212
|
-
const useEffectIssues = summary.smellsByType['useEffect-overuse'];
|
|
213
|
-
const useEffectScore = Math.max(0, 100 - useEffectIssues * 15);
|
|
214
|
-
|
|
215
|
-
// Prop drilling score
|
|
216
|
-
const propDrillingIssues = summary.smellsByType['prop-drilling'];
|
|
217
|
-
const propDrillingScore = Math.max(0, 100 - propDrillingIssues * 12);
|
|
218
|
-
|
|
219
|
-
// Component size score
|
|
220
|
-
const sizeIssues = summary.smellsByType['large-component'] + summary.smellsByType['deep-nesting'];
|
|
221
|
-
const componentSizeScore = Math.max(0, 100 - sizeIssues * 10);
|
|
222
|
-
|
|
223
|
-
// Memoization score
|
|
224
|
-
const memoIssues = summary.smellsByType['unmemoized-calculation'] + summary.smellsByType['inline-function-prop'];
|
|
225
|
-
const memoizationScore = Math.max(0, 100 - memoIssues * 8);
|
|
226
|
-
|
|
227
|
-
// Overall score (weighted average)
|
|
228
|
-
const score = Math.round(
|
|
229
|
-
useEffectScore * 0.3 +
|
|
230
|
-
propDrillingScore * 0.25 +
|
|
231
|
-
componentSizeScore * 0.25 +
|
|
232
|
-
memoizationScore * 0.2
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
// Determine grade
|
|
236
|
-
let grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
237
|
-
if (score >= 90) grade = 'A';
|
|
238
|
-
else if (score >= 80) grade = 'B';
|
|
239
|
-
else if (score >= 70) grade = 'C';
|
|
240
|
-
else if (score >= 60) grade = 'D';
|
|
241
|
-
else grade = 'F';
|
|
242
|
-
|
|
243
|
-
// Estimate refactor time
|
|
244
|
-
const errorCount = summary.smellsBySeverity.error;
|
|
245
|
-
const warningCount = summary.smellsBySeverity.warning;
|
|
246
|
-
const totalIssues = errorCount * 30 + warningCount * 15; // minutes
|
|
247
|
-
|
|
248
|
-
let estimatedRefactorTime: string;
|
|
249
|
-
if (totalIssues < 30) estimatedRefactorTime = '< 30 minutes';
|
|
250
|
-
else if (totalIssues < 60) estimatedRefactorTime = '30 min - 1 hour';
|
|
251
|
-
else if (totalIssues < 120) estimatedRefactorTime = '1-2 hours';
|
|
252
|
-
else if (totalIssues < 240) estimatedRefactorTime = '2-4 hours';
|
|
253
|
-
else if (totalIssues < 480) estimatedRefactorTime = '4-8 hours';
|
|
254
|
-
else estimatedRefactorTime = '> 1 day';
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
score,
|
|
258
|
-
grade,
|
|
259
|
-
breakdown: {
|
|
260
|
-
useEffectScore,
|
|
261
|
-
propDrillingScore,
|
|
262
|
-
componentSizeScore,
|
|
263
|
-
memoizationScore,
|
|
264
|
-
},
|
|
265
|
-
estimatedRefactorTime,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export { DEFAULT_CONFIG, DetectorConfig } from './types/index.js';
|
package/src/cli.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
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();
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import * as t from '@babel/types';
|
|
2
|
-
import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
|
|
3
|
-
import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Detects potentially dead code: unused variables, imports, and functions
|
|
7
|
-
*/
|
|
8
|
-
export function detectDeadCode(
|
|
9
|
-
component: ParsedComponent,
|
|
10
|
-
filePath: string,
|
|
11
|
-
sourceCode: string,
|
|
12
|
-
config: DetectorConfig = DEFAULT_CONFIG
|
|
13
|
-
): CodeSmell[] {
|
|
14
|
-
if (!config.checkDeadCode) return [];
|
|
15
|
-
|
|
16
|
-
const smells: CodeSmell[] = [];
|
|
17
|
-
|
|
18
|
-
// Track declared and used identifiers within the component
|
|
19
|
-
const declared = new Map<string, { line: number; column: number; type: string }>();
|
|
20
|
-
const used = new Set<string>();
|
|
21
|
-
|
|
22
|
-
// Collect all declared variables in the component
|
|
23
|
-
component.path.traverse({
|
|
24
|
-
VariableDeclarator(path) {
|
|
25
|
-
if (t.isIdentifier(path.node.id)) {
|
|
26
|
-
const loc = path.node.loc;
|
|
27
|
-
declared.set(path.node.id.name, {
|
|
28
|
-
line: loc?.start.line || 0,
|
|
29
|
-
column: loc?.start.column || 0,
|
|
30
|
-
type: 'variable',
|
|
31
|
-
});
|
|
32
|
-
} else if (t.isObjectPattern(path.node.id)) {
|
|
33
|
-
// Destructured variables
|
|
34
|
-
path.node.id.properties.forEach(prop => {
|
|
35
|
-
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
36
|
-
const loc = prop.loc;
|
|
37
|
-
declared.set(prop.value.name, {
|
|
38
|
-
line: loc?.start.line || 0,
|
|
39
|
-
column: loc?.start.column || 0,
|
|
40
|
-
type: 'variable',
|
|
41
|
-
});
|
|
42
|
-
} else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
43
|
-
const loc = prop.loc;
|
|
44
|
-
declared.set(prop.argument.name, {
|
|
45
|
-
line: loc?.start.line || 0,
|
|
46
|
-
column: loc?.start.column || 0,
|
|
47
|
-
type: 'variable',
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
} else if (t.isArrayPattern(path.node.id)) {
|
|
52
|
-
// Array destructured variables
|
|
53
|
-
path.node.id.elements.forEach(elem => {
|
|
54
|
-
if (t.isIdentifier(elem)) {
|
|
55
|
-
const loc = elem.loc;
|
|
56
|
-
declared.set(elem.name, {
|
|
57
|
-
line: loc?.start.line || 0,
|
|
58
|
-
column: loc?.start.column || 0,
|
|
59
|
-
type: 'variable',
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
FunctionDeclaration(path) {
|
|
67
|
-
if (path.node.id) {
|
|
68
|
-
const loc = path.node.loc;
|
|
69
|
-
declared.set(path.node.id.name, {
|
|
70
|
-
line: loc?.start.line || 0,
|
|
71
|
-
column: loc?.start.column || 0,
|
|
72
|
-
type: 'function',
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Collect all used identifiers
|
|
79
|
-
component.path.traverse({
|
|
80
|
-
Identifier(path) {
|
|
81
|
-
// Skip if it's a declaration
|
|
82
|
-
if (
|
|
83
|
-
t.isVariableDeclarator(path.parent) && path.parent.id === path.node ||
|
|
84
|
-
t.isFunctionDeclaration(path.parent) && path.parent.id === path.node ||
|
|
85
|
-
t.isObjectProperty(path.parent) && path.parent.key === path.node ||
|
|
86
|
-
t.isImportSpecifier(path.parent) ||
|
|
87
|
-
t.isImportDefaultSpecifier(path.parent)
|
|
88
|
-
) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Skip property access (x.y - y is not a reference)
|
|
93
|
-
if (t.isMemberExpression(path.parent) && path.parent.property === path.node && !path.parent.computed) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
used.add(path.node.name);
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
JSXIdentifier(path) {
|
|
101
|
-
// Components used in JSX
|
|
102
|
-
if (t.isJSXOpeningElement(path.parent) || t.isJSXClosingElement(path.parent)) {
|
|
103
|
-
used.add(path.node.name);
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Find unused declarations
|
|
109
|
-
declared.forEach((info, name) => {
|
|
110
|
-
// Skip hooks and common patterns
|
|
111
|
-
if (name.startsWith('_') || name.startsWith('set')) return;
|
|
112
|
-
|
|
113
|
-
// Skip if it's the component name itself
|
|
114
|
-
if (name === component.name) return;
|
|
115
|
-
|
|
116
|
-
if (!used.has(name)) {
|
|
117
|
-
smells.push({
|
|
118
|
-
type: 'dead-code',
|
|
119
|
-
severity: 'info',
|
|
120
|
-
message: `Unused ${info.type} "${name}" in "${component.name}"`,
|
|
121
|
-
file: filePath,
|
|
122
|
-
line: info.line,
|
|
123
|
-
column: info.column,
|
|
124
|
-
suggestion: `Remove the unused ${info.type} or use it in the component.`,
|
|
125
|
-
codeSnippet: getCodeSnippet(sourceCode, info.line),
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
return smells;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Detects unused imports at the file level
|
|
135
|
-
*/
|
|
136
|
-
export function detectUnusedImports(
|
|
137
|
-
imports: Map<string, { source: string; line: number }>,
|
|
138
|
-
usedInFile: Set<string>,
|
|
139
|
-
filePath: string,
|
|
140
|
-
sourceCode: string
|
|
141
|
-
): CodeSmell[] {
|
|
142
|
-
const smells: CodeSmell[] = [];
|
|
143
|
-
|
|
144
|
-
imports.forEach((info, name) => {
|
|
145
|
-
// Skip React imports as they might be used implicitly
|
|
146
|
-
if (name === 'React' || info.source === 'react') return;
|
|
147
|
-
|
|
148
|
-
if (!usedInFile.has(name)) {
|
|
149
|
-
smells.push({
|
|
150
|
-
type: 'dead-code',
|
|
151
|
-
severity: 'info',
|
|
152
|
-
message: `Unused import "${name}" from "${info.source}"`,
|
|
153
|
-
file: filePath,
|
|
154
|
-
line: info.line,
|
|
155
|
-
column: 0,
|
|
156
|
-
suggestion: `Remove the unused import: import { ${name} } from '${info.source}'`,
|
|
157
|
-
codeSnippet: getCodeSnippet(sourceCode, info.line),
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
return smells;
|
|
163
|
-
}
|