react-code-smell-detector 1.2.0 → 1.4.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 +200 -4
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +22 -1
- package/dist/baseline.d.ts +37 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +112 -0
- package/dist/cli.js +125 -26
- 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/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 +4 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +5 -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/unusedCode.d.ts +7 -0
- package/dist/detectors/unusedCode.d.ts.map +1 -0
- package/dist/detectors/unusedCode.js +78 -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 +31 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +137 -0
- package/dist/reporter.js +16 -0
- package/dist/types/index.d.ts +13 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +18 -0
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +89 -0
- package/dist/webhooks.d.ts +20 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +199 -0
- package/package.json +10 -2
- package/src/analyzer.ts +0 -324
- package/src/cli.ts +0 -159
- package/src/detectors/accessibility.ts +0 -212
- package/src/detectors/deadCode.ts +0 -163
- package/src/detectors/debug.ts +0 -103
- package/src/detectors/dependencyArray.ts +0 -176
- package/src/detectors/hooksRules.ts +0 -101
- package/src/detectors/index.ts +0 -20
- 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/security.ts +0 -179
- package/src/detectors/typescript.ts +0 -151
- package/src/detectors/useEffect.ts +0 -117
- package/src/htmlReporter.ts +0 -464
- package/src/index.ts +0 -4
- package/src/parser/index.ts +0 -195
- package/src/reporter.ts +0 -291
- package/src/types/index.ts +0 -165
- package/tsconfig.json +0 -19
package/dist/cli.js
CHANGED
|
@@ -6,24 +6,37 @@ import path from 'path';
|
|
|
6
6
|
import { analyzeProject, DEFAULT_CONFIG } from './analyzer.js';
|
|
7
7
|
import { reportResults } from './reporter.js';
|
|
8
8
|
import { generateHTMLReport } from './htmlReporter.js';
|
|
9
|
+
import { fixFile, isFixable } from './fixer.js';
|
|
10
|
+
import { startWatch } from './watcher.js';
|
|
11
|
+
import { getAllModifiedFiles, filterReactFiles, getGitInfo } from './git.js';
|
|
12
|
+
import { initializeBaseline, recordBaseline, formatTrendReport } from './baseline.js';
|
|
13
|
+
import { sendWebhookNotification, getWebhookConfig } from './webhooks.js';
|
|
9
14
|
import fs from 'fs/promises';
|
|
10
15
|
const program = new Command();
|
|
11
16
|
program
|
|
12
17
|
.name('react-smell')
|
|
13
18
|
.description('Detect code smells in React projects')
|
|
14
|
-
.version('1.
|
|
19
|
+
.version('1.3.0')
|
|
15
20
|
.argument('[directory]', 'Directory to analyze', '.')
|
|
16
21
|
.option('-f, --format <format>', 'Output format: console, json, markdown, html', 'console')
|
|
17
22
|
.option('-s, --snippets', 'Show code snippets in output', false)
|
|
18
23
|
.option('-c, --config <file>', 'Path to config file')
|
|
19
24
|
.option('--ci', 'CI mode: exit with code 1 if any issues found')
|
|
20
25
|
.option('--fail-on <severity>', 'Exit with code 1 if issues of this severity or higher (error, warning, info)', 'error')
|
|
26
|
+
.option('--fix', 'Auto-fix simple issues (console.log, var, ==, missing alt)')
|
|
27
|
+
.option('--watch', 'Watch mode: re-analyze on file changes')
|
|
28
|
+
.option('--changed', 'Only analyze git-modified files')
|
|
21
29
|
.option('--max-effects <number>', 'Max useEffects per component', parseInt)
|
|
22
30
|
.option('--max-props <number>', 'Max props before warning', parseInt)
|
|
23
31
|
.option('--max-lines <number>', 'Max lines per component', parseInt)
|
|
24
32
|
.option('--include <patterns>', 'Glob patterns to include (comma-separated)')
|
|
25
33
|
.option('--exclude <patterns>', 'Glob patterns to exclude (comma-separated)')
|
|
26
34
|
.option('-o, --output <file>', 'Write output to file')
|
|
35
|
+
.option('--baseline', 'Enable baseline tracking and trend analysis', false)
|
|
36
|
+
.option('--slack <url>', 'Slack webhook URL for notifications')
|
|
37
|
+
.option('--discord <url>', 'Discord webhook URL for notifications')
|
|
38
|
+
.option('--webhook <url>', 'Generic webhook URL for notifications')
|
|
39
|
+
.option('--webhook-threshold <number>', 'Only notify if smells exceed this threshold', parseInt)
|
|
27
40
|
.action(async (directory, options) => {
|
|
28
41
|
const rootDir = path.resolve(process.cwd(), directory);
|
|
29
42
|
// Check if directory exists
|
|
@@ -34,38 +47,103 @@ program
|
|
|
34
47
|
console.error(chalk.red(`Error: Directory "${rootDir}" does not exist.`));
|
|
35
48
|
process.exit(1);
|
|
36
49
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
45
|
-
fileConfig = JSON.parse(configContent);
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
spinner.fail(`Could not load config file: ${error.message}`);
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
50
|
+
// Load config file if specified
|
|
51
|
+
let fileConfig = {};
|
|
52
|
+
if (options.config) {
|
|
53
|
+
try {
|
|
54
|
+
const configPath = path.resolve(process.cwd(), options.config);
|
|
55
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
56
|
+
fileConfig = JSON.parse(configContent);
|
|
51
57
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error(chalk.red(`Could not load config file: ${error.message}`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Build config from options
|
|
64
|
+
const config = {
|
|
65
|
+
...DEFAULT_CONFIG,
|
|
66
|
+
...fileConfig,
|
|
67
|
+
...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
|
|
68
|
+
...(options.maxProps && { maxPropsCount: options.maxProps }),
|
|
69
|
+
...(options.maxLines && { maxComponentLines: options.maxLines }),
|
|
70
|
+
};
|
|
71
|
+
const include = options.include?.split(',').map((p) => p.trim()) || ['**/*.tsx', '**/*.jsx'];
|
|
72
|
+
const exclude = options.exclude?.split(',').map((p) => p.trim()) || ['**/node_modules/**', '**/dist/**'];
|
|
73
|
+
// Watch mode
|
|
74
|
+
if (options.watch) {
|
|
75
|
+
const watcher = startWatch({
|
|
63
76
|
rootDir,
|
|
64
77
|
include,
|
|
65
78
|
exclude,
|
|
66
79
|
config,
|
|
80
|
+
showSnippets: options.snippets,
|
|
81
|
+
});
|
|
82
|
+
// Handle Ctrl+C gracefully
|
|
83
|
+
process.on('SIGINT', () => {
|
|
84
|
+
watcher.close();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
});
|
|
87
|
+
return; // Don't continue to regular analysis
|
|
88
|
+
}
|
|
89
|
+
// Git changed files mode
|
|
90
|
+
let filesToAnalyze;
|
|
91
|
+
if (options.changed) {
|
|
92
|
+
const gitInfo = getGitInfo(rootDir);
|
|
93
|
+
if (!gitInfo.isGitRepo) {
|
|
94
|
+
console.error(chalk.red('Error: --changed requires a git repository'));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const modifiedFiles = getAllModifiedFiles(rootDir);
|
|
98
|
+
filesToAnalyze = filterReactFiles(modifiedFiles);
|
|
99
|
+
if (filesToAnalyze.length === 0) {
|
|
100
|
+
console.log(chalk.green('✓ No modified React files to analyze'));
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
console.log(chalk.cyan(`\n📝 Analyzing ${filesToAnalyze.length} modified file(s)...\n`));
|
|
104
|
+
}
|
|
105
|
+
const spinner = ora('Analyzing React project...').start();
|
|
106
|
+
try {
|
|
107
|
+
const result = await analyzeProject({
|
|
108
|
+
rootDir,
|
|
109
|
+
include: filesToAnalyze ? undefined : include,
|
|
110
|
+
exclude: filesToAnalyze ? undefined : exclude,
|
|
111
|
+
config,
|
|
67
112
|
});
|
|
68
113
|
spinner.stop();
|
|
114
|
+
// Initialize baseline if enabled
|
|
115
|
+
if (options.baseline) {
|
|
116
|
+
initializeBaseline(rootDir);
|
|
117
|
+
}
|
|
118
|
+
// Fix mode - apply auto-fixes
|
|
119
|
+
if (options.fix) {
|
|
120
|
+
const fixableSmells = result.files.flatMap(f => f.smells.filter(isFixable).map(s => ({ ...s, file: f.file })));
|
|
121
|
+
if (fixableSmells.length > 0) {
|
|
122
|
+
console.log(chalk.cyan(`\n🔧 Auto-fixing ${fixableSmells.length} issue(s)...\n`));
|
|
123
|
+
// Group by file
|
|
124
|
+
const smellsByFile = new Map();
|
|
125
|
+
fixableSmells.forEach(smell => {
|
|
126
|
+
const existing = smellsByFile.get(smell.file) || [];
|
|
127
|
+
existing.push(smell);
|
|
128
|
+
smellsByFile.set(smell.file, existing);
|
|
129
|
+
});
|
|
130
|
+
let totalFixed = 0;
|
|
131
|
+
for (const [file, smells] of smellsByFile) {
|
|
132
|
+
const fixResult = await fixFile(file, smells);
|
|
133
|
+
if (fixResult.fixedSmells.length > 0) {
|
|
134
|
+
console.log(chalk.green(` ✓ Fixed ${fixResult.fixedSmells.length} issue(s) in ${path.relative(rootDir, file)}`));
|
|
135
|
+
totalFixed += fixResult.fixedSmells.length;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
console.log(chalk.green(`\n✓ Fixed ${totalFixed} issue(s) total\n`));
|
|
139
|
+
// Re-analyze after fixes
|
|
140
|
+
const newResult = await analyzeProject({ rootDir, include, exclude, config });
|
|
141
|
+
console.log(chalk.dim(`Remaining issues: ${newResult.summary.totalSmells}\n`));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(chalk.yellow('\nNo auto-fixable issues found\n'));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
69
147
|
let output;
|
|
70
148
|
if (options.format === 'html') {
|
|
71
149
|
output = generateHTMLReport(result, rootDir);
|
|
@@ -89,6 +167,27 @@ program
|
|
|
89
167
|
else {
|
|
90
168
|
console.log(output);
|
|
91
169
|
}
|
|
170
|
+
// Record baseline and show trend analysis
|
|
171
|
+
if (options.baseline) {
|
|
172
|
+
const gitInfo = getGitInfo(rootDir);
|
|
173
|
+
const baselineRecord = recordBaseline(rootDir, result.files.flatMap(f => f.smells), gitInfo.currentCommit);
|
|
174
|
+
console.log(formatTrendReport(rootDir));
|
|
175
|
+
}
|
|
176
|
+
// Send webhook notification
|
|
177
|
+
const webhookConfig = getWebhookConfig(options.slack, options.discord, options.webhook);
|
|
178
|
+
if (webhookConfig) {
|
|
179
|
+
webhookConfig.threshold = options.webhookThreshold;
|
|
180
|
+
const gitInfo = getGitInfo(rootDir);
|
|
181
|
+
const metadata = {
|
|
182
|
+
branch: gitInfo.branch,
|
|
183
|
+
commit: gitInfo.currentCommit,
|
|
184
|
+
author: gitInfo.authorName,
|
|
185
|
+
};
|
|
186
|
+
const sent = await sendWebhookNotification(webhookConfig, result.files.flatMap(f => f.smells), path.basename(rootDir), metadata);
|
|
187
|
+
if (sent) {
|
|
188
|
+
console.log(chalk.green('✓ Notification sent to webhook'));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
92
191
|
// CI/CD exit code handling
|
|
93
192
|
const { smellsBySeverity } = result.summary;
|
|
94
193
|
let shouldFail = false;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
export interface ComplexityMetrics {
|
|
4
|
+
cyclomaticComplexity: number;
|
|
5
|
+
cognitiveComplexity: number;
|
|
6
|
+
maxNestingDepth: number;
|
|
7
|
+
linesOfCode: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Detect code complexity issues in a component
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectComplexity(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
13
|
+
/**
|
|
14
|
+
* Calculate complexity metrics for a component
|
|
15
|
+
*/
|
|
16
|
+
export declare function calculateComplexityMetrics(component: ParsedComponent): ComplexityMetrics;
|
|
17
|
+
//# sourceMappingURL=complexity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"complexity.d.ts","sourceRoot":"","sources":["../../src/detectors/complexity.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAqCb;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,eAAe,GAAG,iBAAiB,CA6BxF"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect code complexity issues in a component
|
|
3
|
+
*/
|
|
4
|
+
export function detectComplexity(component, filePath, sourceCode, config) {
|
|
5
|
+
if (!config.checkComplexity)
|
|
6
|
+
return [];
|
|
7
|
+
const smells = [];
|
|
8
|
+
const metrics = calculateComplexityMetrics(component);
|
|
9
|
+
const thresholds = {
|
|
10
|
+
cyclomatic: config.maxCyclomaticComplexity || 10,
|
|
11
|
+
cognitive: config.maxCognitiveComplexity || 15,
|
|
12
|
+
nesting: config.maxNestingDepth || 4,
|
|
13
|
+
};
|
|
14
|
+
if (metrics.cyclomaticComplexity > thresholds.cyclomatic) {
|
|
15
|
+
smells.push({
|
|
16
|
+
type: 'high-cyclomatic-complexity',
|
|
17
|
+
severity: metrics.cyclomaticComplexity > thresholds.cyclomatic * 1.5 ? 'error' : 'warning',
|
|
18
|
+
message: `Component "${component.name}" has cyclomatic complexity of ${metrics.cyclomaticComplexity} (threshold: ${thresholds.cyclomatic})`,
|
|
19
|
+
file: filePath,
|
|
20
|
+
line: component.startLine,
|
|
21
|
+
column: 0,
|
|
22
|
+
suggestion: 'Break down complex logic into smaller functions.',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (metrics.cognitiveComplexity > thresholds.cognitive) {
|
|
26
|
+
smells.push({
|
|
27
|
+
type: 'high-cognitive-complexity',
|
|
28
|
+
severity: metrics.cognitiveComplexity > thresholds.cognitive * 1.5 ? 'error' : 'warning',
|
|
29
|
+
message: `Component "${component.name}" has cognitive complexity of ${metrics.cognitiveComplexity} (threshold: ${thresholds.cognitive})`,
|
|
30
|
+
file: filePath,
|
|
31
|
+
line: component.startLine,
|
|
32
|
+
column: 0,
|
|
33
|
+
suggestion: 'Simplify nested conditions and flatten control flow.',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return smells;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Calculate complexity metrics for a component
|
|
40
|
+
*/
|
|
41
|
+
export function calculateComplexityMetrics(component) {
|
|
42
|
+
let cyclomaticComplexity = 1;
|
|
43
|
+
let cognitiveComplexity = 0;
|
|
44
|
+
component.path.traverse({
|
|
45
|
+
IfStatement() { cyclomaticComplexity++; cognitiveComplexity++; },
|
|
46
|
+
ConditionalExpression() { cyclomaticComplexity++; cognitiveComplexity++; },
|
|
47
|
+
LogicalExpression(path) {
|
|
48
|
+
if (path.node.operator === '&&' || path.node.operator === '||') {
|
|
49
|
+
cyclomaticComplexity++;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
SwitchCase(path) {
|
|
53
|
+
if (path.node.test !== null)
|
|
54
|
+
cyclomaticComplexity++;
|
|
55
|
+
},
|
|
56
|
+
ForStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
57
|
+
ForInStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
58
|
+
ForOfStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
59
|
+
WhileStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
60
|
+
DoWhileStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
61
|
+
CatchClause() { cyclomaticComplexity++; cognitiveComplexity++; },
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
cyclomaticComplexity,
|
|
65
|
+
cognitiveComplexity,
|
|
66
|
+
maxNestingDepth: component.jsxDepth,
|
|
67
|
+
linesOfCode: component.endLine - component.startLine + 1,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
2
|
+
export interface ImportInfo {
|
|
3
|
+
source: string;
|
|
4
|
+
specifiers: string[];
|
|
5
|
+
line: number;
|
|
6
|
+
isDefault: boolean;
|
|
7
|
+
isNamespace: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ImportAnalysisResult {
|
|
10
|
+
smells: CodeSmell[];
|
|
11
|
+
importGraph: Map<string, string[]>;
|
|
12
|
+
circularDeps: string[][];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Analyze imports across multiple files for issues
|
|
16
|
+
*/
|
|
17
|
+
export declare function analyzeImports(files: string[], rootDir: string, config: DetectorConfig): Promise<ImportAnalysisResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Standalone detection function matching other detector signatures
|
|
20
|
+
*/
|
|
21
|
+
export declare function detectImportIssues(component: any, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
22
|
+
//# sourceMappingURL=imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/detectors/imports.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAK9D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,oBAAoB,CAAC,CAkD/B;AA8KD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAGb"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import _traverse from '@babel/traverse';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
// Handle ESM/CJS interop
|
|
7
|
+
const traverse = typeof _traverse === 'function' ? _traverse : _traverse.default;
|
|
8
|
+
/**
|
|
9
|
+
* Analyze imports across multiple files for issues
|
|
10
|
+
*/
|
|
11
|
+
export async function analyzeImports(files, rootDir, config) {
|
|
12
|
+
const smells = [];
|
|
13
|
+
const importGraph = new Map();
|
|
14
|
+
// Build import graph
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
try {
|
|
17
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
18
|
+
const imports = extractImports(content, file);
|
|
19
|
+
const resolvedImports = [];
|
|
20
|
+
for (const imp of imports) {
|
|
21
|
+
// Skip external packages
|
|
22
|
+
if (!imp.source.startsWith('.') && !imp.source.startsWith('/')) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const resolvedPath = resolveImportPath(file, imp.source, rootDir);
|
|
26
|
+
if (resolvedPath) {
|
|
27
|
+
resolvedImports.push(resolvedPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
importGraph.set(file, resolvedImports);
|
|
31
|
+
// Detect import-related smells in this file
|
|
32
|
+
smells.push(...detectImportSmells(imports, file, content, config));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Skip files that can't be parsed
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Detect circular dependencies
|
|
39
|
+
const circularDeps = detectCircularDependencies(importGraph);
|
|
40
|
+
// Add circular dependency smells
|
|
41
|
+
for (const cycle of circularDeps) {
|
|
42
|
+
const cycleStr = cycle.map(f => path.basename(f)).join(' → ');
|
|
43
|
+
smells.push({
|
|
44
|
+
type: 'circular-dependency',
|
|
45
|
+
severity: 'warning',
|
|
46
|
+
message: `Circular dependency detected: ${cycleStr}`,
|
|
47
|
+
file: cycle[0],
|
|
48
|
+
line: 1,
|
|
49
|
+
column: 0,
|
|
50
|
+
suggestion: 'Refactor to break the cycle. Consider extracting shared logic to a separate module.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { smells, importGraph, circularDeps };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract all imports from a file
|
|
57
|
+
*/
|
|
58
|
+
function extractImports(sourceCode, filePath) {
|
|
59
|
+
const imports = [];
|
|
60
|
+
try {
|
|
61
|
+
const ast = parse(sourceCode, {
|
|
62
|
+
sourceType: 'module',
|
|
63
|
+
plugins: ['jsx', 'typescript'],
|
|
64
|
+
});
|
|
65
|
+
traverse(ast, {
|
|
66
|
+
ImportDeclaration(nodePath) {
|
|
67
|
+
const node = nodePath.node;
|
|
68
|
+
const specifiers = node.specifiers.map(spec => {
|
|
69
|
+
if (t.isImportDefaultSpecifier(spec))
|
|
70
|
+
return 'default';
|
|
71
|
+
if (t.isImportNamespaceSpecifier(spec))
|
|
72
|
+
return '*';
|
|
73
|
+
return spec.local.name;
|
|
74
|
+
});
|
|
75
|
+
imports.push({
|
|
76
|
+
source: node.source.value,
|
|
77
|
+
specifiers,
|
|
78
|
+
line: node.loc?.start.line || 0,
|
|
79
|
+
isDefault: node.specifiers.some(t.isImportDefaultSpecifier),
|
|
80
|
+
isNamespace: node.specifiers.some(t.isImportNamespaceSpecifier),
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Parse error, skip
|
|
87
|
+
}
|
|
88
|
+
return imports;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve an import path relative to the importing file
|
|
92
|
+
*/
|
|
93
|
+
function resolveImportPath(fromFile, importSource, rootDir) {
|
|
94
|
+
if (!importSource.startsWith('.')) {
|
|
95
|
+
return null; // External package
|
|
96
|
+
}
|
|
97
|
+
const dir = path.dirname(fromFile);
|
|
98
|
+
let resolved = path.resolve(dir, importSource);
|
|
99
|
+
// Try common extensions
|
|
100
|
+
const extensions = ['.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js'];
|
|
101
|
+
for (const ext of extensions) {
|
|
102
|
+
const tryPath = resolved + ext;
|
|
103
|
+
// We don't check existence here - just build the graph
|
|
104
|
+
if (!ext.includes('/')) {
|
|
105
|
+
return tryPath;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return resolved;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Detect circular dependencies in import graph using DFS
|
|
112
|
+
*/
|
|
113
|
+
function detectCircularDependencies(graph) {
|
|
114
|
+
const cycles = [];
|
|
115
|
+
const visited = new Set();
|
|
116
|
+
const recursionStack = new Set();
|
|
117
|
+
const path = [];
|
|
118
|
+
function dfs(node) {
|
|
119
|
+
visited.add(node);
|
|
120
|
+
recursionStack.add(node);
|
|
121
|
+
path.push(node);
|
|
122
|
+
const neighbors = graph.get(node) || [];
|
|
123
|
+
for (const neighbor of neighbors) {
|
|
124
|
+
if (!visited.has(neighbor)) {
|
|
125
|
+
dfs(neighbor);
|
|
126
|
+
}
|
|
127
|
+
else if (recursionStack.has(neighbor)) {
|
|
128
|
+
// Found a cycle
|
|
129
|
+
const cycleStart = path.indexOf(neighbor);
|
|
130
|
+
if (cycleStart !== -1) {
|
|
131
|
+
const cycle = path.slice(cycleStart).concat(neighbor);
|
|
132
|
+
// Avoid duplicate cycles
|
|
133
|
+
const cycleKey = [...cycle].sort().join('|');
|
|
134
|
+
if (!cycles.some(c => [...c].sort().join('|') === cycleKey)) {
|
|
135
|
+
cycles.push(cycle);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
path.pop();
|
|
141
|
+
recursionStack.delete(node);
|
|
142
|
+
}
|
|
143
|
+
for (const node of graph.keys()) {
|
|
144
|
+
if (!visited.has(node)) {
|
|
145
|
+
dfs(node);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return cycles;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Detect import-related code smells in a single file
|
|
152
|
+
*/
|
|
153
|
+
function detectImportSmells(imports, filePath, sourceCode, config) {
|
|
154
|
+
const smells = [];
|
|
155
|
+
// Check for barrel file imports (importing from index files)
|
|
156
|
+
for (const imp of imports) {
|
|
157
|
+
if (imp.source.endsWith('/index') || imp.source === '.') {
|
|
158
|
+
smells.push({
|
|
159
|
+
type: 'barrel-file-import',
|
|
160
|
+
severity: 'info',
|
|
161
|
+
message: `Barrel file import from "${imp.source}" may impact tree-shaking`,
|
|
162
|
+
file: filePath,
|
|
163
|
+
line: imp.line,
|
|
164
|
+
column: 0,
|
|
165
|
+
suggestion: 'Import directly from the source file instead of barrel/index files for better build optimization.',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Check for namespace imports (import * as)
|
|
170
|
+
for (const imp of imports) {
|
|
171
|
+
if (imp.isNamespace && !imp.source.startsWith('.')) {
|
|
172
|
+
smells.push({
|
|
173
|
+
type: 'namespace-import',
|
|
174
|
+
severity: 'info',
|
|
175
|
+
message: `Namespace import "* as" from "${imp.source}" may prevent tree-shaking`,
|
|
176
|
+
file: filePath,
|
|
177
|
+
line: imp.line,
|
|
178
|
+
column: 0,
|
|
179
|
+
suggestion: 'Import only the specific exports you need for better bundle size.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Check for too many imports from same source
|
|
184
|
+
const importCounts = new Map();
|
|
185
|
+
for (const imp of imports) {
|
|
186
|
+
const count = (importCounts.get(imp.source) || 0) + imp.specifiers.length;
|
|
187
|
+
importCounts.set(imp.source, count);
|
|
188
|
+
}
|
|
189
|
+
for (const [source, count] of importCounts) {
|
|
190
|
+
if (count > 10 && source.startsWith('.')) {
|
|
191
|
+
smells.push({
|
|
192
|
+
type: 'excessive-imports',
|
|
193
|
+
severity: 'warning',
|
|
194
|
+
message: `${count} imports from "${source}" suggests tight coupling`,
|
|
195
|
+
file: filePath,
|
|
196
|
+
line: imports.find(i => i.source === source)?.line || 1,
|
|
197
|
+
column: 0,
|
|
198
|
+
suggestion: 'Consider if this file has too many responsibilities or if modules should be reorganized.',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return smells;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Standalone detection function matching other detector signatures
|
|
206
|
+
*/
|
|
207
|
+
export function detectImportIssues(component, filePath, sourceCode, config) {
|
|
208
|
+
const imports = extractImports(sourceCode, filePath);
|
|
209
|
+
return detectImportSmells(imports, filePath, sourceCode, config);
|
|
210
|
+
}
|
|
@@ -16,4 +16,8 @@ export { detectTypescriptIssues } from './typescript.js';
|
|
|
16
16
|
export { detectDebugStatements } from './debug.js';
|
|
17
17
|
export { detectSecurityIssues } from './security.js';
|
|
18
18
|
export { detectAccessibilityIssues } from './accessibility.js';
|
|
19
|
+
export { detectComplexity, calculateComplexityMetrics } from './complexity.js';
|
|
20
|
+
export { detectMemoryLeaks } from './memoryLeak.js';
|
|
21
|
+
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
22
|
+
export { detectUnusedCode } from './unusedCode.js';
|
|
19
23
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/detectors/index.js
CHANGED
|
@@ -18,3 +18,8 @@ export { detectTypescriptIssues } from './typescript.js';
|
|
|
18
18
|
export { detectDebugStatements } from './debug.js';
|
|
19
19
|
export { detectSecurityIssues } from './security.js';
|
|
20
20
|
export { detectAccessibilityIssues } from './accessibility.js';
|
|
21
|
+
// Complexity, Memory Leaks, Imports
|
|
22
|
+
export { detectComplexity, calculateComplexityMetrics } from './complexity.js';
|
|
23
|
+
export { detectMemoryLeaks } from './memoryLeak.js';
|
|
24
|
+
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
25
|
+
export { detectUnusedCode } from './unusedCode.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect potential memory leaks in React components
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectMemoryLeaks(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
7
|
+
//# sourceMappingURL=memoryLeak.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memoryLeak.d.ts","sourceRoot":"","sources":["../../src/detectors/memoryLeak.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAgEb"}
|