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/src/parser/index.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import * as parser from '@babel/parser';
|
|
2
|
-
import _traverse, { NodePath } from '@babel/traverse';
|
|
3
|
-
import * as t from '@babel/types';
|
|
4
|
-
import fs from 'fs/promises';
|
|
5
|
-
|
|
6
|
-
// Handle ESM/CJS interop
|
|
7
|
-
const traverse = (_traverse as unknown as { default: typeof _traverse }).default || _traverse;
|
|
8
|
-
|
|
9
|
-
export interface ParsedComponent {
|
|
10
|
-
name: string;
|
|
11
|
-
startLine: number;
|
|
12
|
-
endLine: number;
|
|
13
|
-
node: t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression;
|
|
14
|
-
path: NodePath;
|
|
15
|
-
hooks: {
|
|
16
|
-
useEffect: t.CallExpression[];
|
|
17
|
-
useState: t.CallExpression[];
|
|
18
|
-
useMemo: t.CallExpression[];
|
|
19
|
-
useCallback: t.CallExpression[];
|
|
20
|
-
useRef: t.CallExpression[];
|
|
21
|
-
custom: t.CallExpression[];
|
|
22
|
-
};
|
|
23
|
-
props: string[];
|
|
24
|
-
jsxDepth: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ParseResult {
|
|
28
|
-
ast: t.File;
|
|
29
|
-
components: ParsedComponent[];
|
|
30
|
-
imports: string[];
|
|
31
|
-
sourceCode: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function parseFile(filePath: string): Promise<ParseResult> {
|
|
35
|
-
const sourceCode = await fs.readFile(filePath, 'utf-8');
|
|
36
|
-
return parseCode(sourceCode, filePath);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function parseCode(sourceCode: string, filePath: string = 'unknown'): ParseResult {
|
|
40
|
-
const ast = parser.parse(sourceCode, {
|
|
41
|
-
sourceType: 'module',
|
|
42
|
-
plugins: [
|
|
43
|
-
'jsx',
|
|
44
|
-
'typescript',
|
|
45
|
-
'decorators-legacy',
|
|
46
|
-
'classProperties',
|
|
47
|
-
'optionalChaining',
|
|
48
|
-
'nullishCoalescingOperator',
|
|
49
|
-
],
|
|
50
|
-
sourceFilename: filePath,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const components: ParsedComponent[] = [];
|
|
54
|
-
const imports: string[] = [];
|
|
55
|
-
|
|
56
|
-
traverse(ast, {
|
|
57
|
-
ImportDeclaration(path) {
|
|
58
|
-
imports.push(path.node.source.value);
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
FunctionDeclaration(path) {
|
|
62
|
-
if (isReactComponent(path.node.id?.name, path)) {
|
|
63
|
-
components.push(extractComponentInfo(path.node.id?.name || 'Anonymous', path));
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
VariableDeclarator(path) {
|
|
68
|
-
const init = path.node.init;
|
|
69
|
-
const id = path.node.id;
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
t.isIdentifier(id) &&
|
|
73
|
-
(t.isArrowFunctionExpression(init) || t.isFunctionExpression(init))
|
|
74
|
-
) {
|
|
75
|
-
if (isReactComponent(id.name, path)) {
|
|
76
|
-
components.push(extractComponentInfo(id.name, path, init));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
return { ast, components, imports, sourceCode };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isReactComponent(name: string | undefined, path: NodePath): boolean {
|
|
86
|
-
if (!name) return false;
|
|
87
|
-
|
|
88
|
-
// Component names start with uppercase
|
|
89
|
-
if (!/^[A-Z]/.test(name)) return false;
|
|
90
|
-
|
|
91
|
-
// Check if it returns JSX
|
|
92
|
-
let hasJSX = false;
|
|
93
|
-
path.traverse({
|
|
94
|
-
JSXElement() {
|
|
95
|
-
hasJSX = true;
|
|
96
|
-
},
|
|
97
|
-
JSXFragment() {
|
|
98
|
-
hasJSX = true;
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
return hasJSX;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function extractComponentInfo(
|
|
106
|
-
name: string,
|
|
107
|
-
path: NodePath,
|
|
108
|
-
node?: t.ArrowFunctionExpression | t.FunctionExpression
|
|
109
|
-
): ParsedComponent {
|
|
110
|
-
const actualNode = node || (path.node as t.FunctionDeclaration);
|
|
111
|
-
const loc = actualNode.loc;
|
|
112
|
-
|
|
113
|
-
const hooks = {
|
|
114
|
-
useEffect: [] as t.CallExpression[],
|
|
115
|
-
useState: [] as t.CallExpression[],
|
|
116
|
-
useMemo: [] as t.CallExpression[],
|
|
117
|
-
useCallback: [] as t.CallExpression[],
|
|
118
|
-
useRef: [] as t.CallExpression[],
|
|
119
|
-
custom: [] as t.CallExpression[],
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const props: string[] = [];
|
|
123
|
-
let jsxDepth = 0;
|
|
124
|
-
|
|
125
|
-
// Extract hooks
|
|
126
|
-
path.traverse({
|
|
127
|
-
CallExpression(callPath) {
|
|
128
|
-
const callee = callPath.node.callee;
|
|
129
|
-
if (t.isIdentifier(callee)) {
|
|
130
|
-
const hookName = callee.name;
|
|
131
|
-
if (hookName === 'useEffect') hooks.useEffect.push(callPath.node);
|
|
132
|
-
else if (hookName === 'useState') hooks.useState.push(callPath.node);
|
|
133
|
-
else if (hookName === 'useMemo') hooks.useMemo.push(callPath.node);
|
|
134
|
-
else if (hookName === 'useCallback') hooks.useCallback.push(callPath.node);
|
|
135
|
-
else if (hookName === 'useRef') hooks.useRef.push(callPath.node);
|
|
136
|
-
else if (hookName.startsWith('use')) hooks.custom.push(callPath.node);
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Extract props
|
|
142
|
-
const params = t.isFunctionDeclaration(actualNode)
|
|
143
|
-
? actualNode.params
|
|
144
|
-
: actualNode.params;
|
|
145
|
-
|
|
146
|
-
if (params.length > 0) {
|
|
147
|
-
const firstParam = params[0];
|
|
148
|
-
if (t.isObjectPattern(firstParam)) {
|
|
149
|
-
firstParam.properties.forEach(prop => {
|
|
150
|
-
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
151
|
-
props.push(prop.key.name);
|
|
152
|
-
} else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
153
|
-
props.push(`...${prop.argument.name}`);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
} else if (t.isIdentifier(firstParam)) {
|
|
157
|
-
props.push(firstParam.name);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Calculate JSX nesting depth
|
|
162
|
-
path.traverse({
|
|
163
|
-
JSXElement: {
|
|
164
|
-
enter() {
|
|
165
|
-
jsxDepth++;
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
name,
|
|
172
|
-
startLine: loc?.start.line || 0,
|
|
173
|
-
endLine: loc?.end.line || 0,
|
|
174
|
-
node: actualNode as any,
|
|
175
|
-
path,
|
|
176
|
-
hooks,
|
|
177
|
-
props,
|
|
178
|
-
jsxDepth: Math.floor(jsxDepth / 2), // Approximate depth
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function getCodeSnippet(sourceCode: string, line: number, context: number = 2): string {
|
|
183
|
-
const lines = sourceCode.split('\n');
|
|
184
|
-
const start = Math.max(0, line - context - 1);
|
|
185
|
-
const end = Math.min(lines.length, line + context);
|
|
186
|
-
|
|
187
|
-
return lines
|
|
188
|
-
.slice(start, end)
|
|
189
|
-
.map((l, i) => {
|
|
190
|
-
const lineNum = start + i + 1;
|
|
191
|
-
const marker = lineNum === line ? '>' : ' ';
|
|
192
|
-
return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;
|
|
193
|
-
})
|
|
194
|
-
.join('\n');
|
|
195
|
-
}
|
package/src/reporter.ts
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { AnalysisResult, CodeSmell, SmellSeverity } from './types/index.js';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
export interface ReporterOptions {
|
|
6
|
-
format: 'console' | 'json' | 'markdown';
|
|
7
|
-
showCodeSnippets: boolean;
|
|
8
|
-
rootDir: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function reportResults(result: AnalysisResult, options: ReporterOptions): string {
|
|
12
|
-
switch (options.format) {
|
|
13
|
-
case 'json':
|
|
14
|
-
return JSON.stringify(result, null, 2);
|
|
15
|
-
case 'markdown':
|
|
16
|
-
return formatMarkdown(result, options);
|
|
17
|
-
case 'console':
|
|
18
|
-
default:
|
|
19
|
-
return formatConsole(result, options);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function formatConsole(result: AnalysisResult, options: ReporterOptions): string {
|
|
24
|
-
const lines: string[] = [];
|
|
25
|
-
const { summary, debtScore, files } = result;
|
|
26
|
-
|
|
27
|
-
// Header
|
|
28
|
-
lines.push('');
|
|
29
|
-
lines.push(chalk.bold.cyan('╔══════════════════════════════════════════════════════════════╗'));
|
|
30
|
-
lines.push(chalk.bold.cyan('║') + chalk.bold.white(' 🔍 React Code Smell Detector Report ') + chalk.bold.cyan('║'));
|
|
31
|
-
lines.push(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝'));
|
|
32
|
-
lines.push('');
|
|
33
|
-
|
|
34
|
-
// Technical Debt Score
|
|
35
|
-
const gradeColor = getGradeColor(debtScore.grade);
|
|
36
|
-
lines.push(chalk.bold('📊 Technical Debt Score'));
|
|
37
|
-
lines.push('');
|
|
38
|
-
lines.push(` ${chalk.bold('Grade:')} ${gradeColor(debtScore.grade)} ${getScoreBar(debtScore.score)} ${debtScore.score}/100`);
|
|
39
|
-
lines.push('');
|
|
40
|
-
lines.push(chalk.dim(' Breakdown:'));
|
|
41
|
-
lines.push(` ${chalk.yellow('⚡')} useEffect: ${getSmallBar(debtScore.breakdown.useEffectScore)} ${debtScore.breakdown.useEffectScore}`);
|
|
42
|
-
lines.push(` ${chalk.blue('🔗')} Prop Drilling: ${getSmallBar(debtScore.breakdown.propDrillingScore)} ${debtScore.breakdown.propDrillingScore}`);
|
|
43
|
-
lines.push(` ${chalk.magenta('📐')} Component Size:${getSmallBar(debtScore.breakdown.componentSizeScore)} ${debtScore.breakdown.componentSizeScore}`);
|
|
44
|
-
lines.push(` ${chalk.green('💾')} Memoization: ${getSmallBar(debtScore.breakdown.memoizationScore)} ${debtScore.breakdown.memoizationScore}`);
|
|
45
|
-
lines.push('');
|
|
46
|
-
lines.push(` ${chalk.dim('Estimated refactor time:')} ${chalk.yellow(debtScore.estimatedRefactorTime)}`);
|
|
47
|
-
lines.push('');
|
|
48
|
-
|
|
49
|
-
// Summary
|
|
50
|
-
lines.push(chalk.bold('📈 Summary'));
|
|
51
|
-
lines.push('');
|
|
52
|
-
lines.push(` Files analyzed: ${chalk.cyan(summary.totalFiles)}`);
|
|
53
|
-
lines.push(` Components found: ${chalk.cyan(summary.totalComponents)}`);
|
|
54
|
-
lines.push(` Total issues: ${getSeverityLabel(summary.totalSmells, summary.smellsBySeverity)}`);
|
|
55
|
-
lines.push('');
|
|
56
|
-
|
|
57
|
-
// Issues by type
|
|
58
|
-
if (summary.totalSmells > 0) {
|
|
59
|
-
lines.push(chalk.bold('🏷️ Issues by Type'));
|
|
60
|
-
lines.push('');
|
|
61
|
-
Object.entries(summary.smellsByType).forEach(([type, count]) => {
|
|
62
|
-
if (count > 0) {
|
|
63
|
-
lines.push(` ${chalk.dim('•')} ${formatSmellType(type)}: ${chalk.yellow(count)}`);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
lines.push('');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Detailed findings
|
|
70
|
-
if (files.some(f => f.smells.length > 0)) {
|
|
71
|
-
lines.push(chalk.bold('📋 Detailed Findings'));
|
|
72
|
-
lines.push('');
|
|
73
|
-
|
|
74
|
-
files.forEach(file => {
|
|
75
|
-
if (file.smells.length === 0) return;
|
|
76
|
-
|
|
77
|
-
const relativePath = path.relative(options.rootDir, file.file);
|
|
78
|
-
lines.push(chalk.bold.underline(relativePath));
|
|
79
|
-
lines.push('');
|
|
80
|
-
|
|
81
|
-
file.smells.forEach(smell => {
|
|
82
|
-
const icon = getSeverityIcon(smell.severity);
|
|
83
|
-
const color = getSeverityColor(smell.severity);
|
|
84
|
-
|
|
85
|
-
lines.push(` ${icon} ${color(smell.message)}`);
|
|
86
|
-
lines.push(` ${chalk.dim('Line')} ${smell.line} ${chalk.dim('•')} ${chalk.italic.cyan(smell.suggestion)}`);
|
|
87
|
-
|
|
88
|
-
if (options.showCodeSnippets && smell.codeSnippet) {
|
|
89
|
-
lines.push('');
|
|
90
|
-
smell.codeSnippet.split('\n').forEach(line => {
|
|
91
|
-
if (line.startsWith('>')) {
|
|
92
|
-
lines.push(chalk.red(line));
|
|
93
|
-
} else {
|
|
94
|
-
lines.push(chalk.dim(line));
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
lines.push('');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Footer
|
|
104
|
-
if (summary.totalSmells === 0) {
|
|
105
|
-
lines.push(chalk.green.bold('✨ No code smells detected! Your code looks great.'));
|
|
106
|
-
} else {
|
|
107
|
-
lines.push(chalk.dim('─'.repeat(64)));
|
|
108
|
-
lines.push(chalk.dim(`Found ${summary.totalSmells} issue(s). Run with --help for more options.`));
|
|
109
|
-
}
|
|
110
|
-
lines.push('');
|
|
111
|
-
|
|
112
|
-
return lines.join('\n');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function formatMarkdown(result: AnalysisResult, options: ReporterOptions): string {
|
|
116
|
-
const lines: string[] = [];
|
|
117
|
-
const { summary, debtScore, files } = result;
|
|
118
|
-
|
|
119
|
-
lines.push('# React Code Smell Detector Report');
|
|
120
|
-
lines.push('');
|
|
121
|
-
lines.push('## Technical Debt Score');
|
|
122
|
-
lines.push('');
|
|
123
|
-
lines.push(`| Metric | Score |`);
|
|
124
|
-
lines.push(`|--------|-------|`);
|
|
125
|
-
lines.push(`| **Overall Grade** | **${debtScore.grade}** (${debtScore.score}/100) |`);
|
|
126
|
-
lines.push(`| useEffect Usage | ${debtScore.breakdown.useEffectScore}/100 |`);
|
|
127
|
-
lines.push(`| Prop Drilling | ${debtScore.breakdown.propDrillingScore}/100 |`);
|
|
128
|
-
lines.push(`| Component Size | ${debtScore.breakdown.componentSizeScore}/100 |`);
|
|
129
|
-
lines.push(`| Memoization | ${debtScore.breakdown.memoizationScore}/100 |`);
|
|
130
|
-
lines.push('');
|
|
131
|
-
lines.push(`**Estimated Refactor Time:** ${debtScore.estimatedRefactorTime}`);
|
|
132
|
-
lines.push('');
|
|
133
|
-
|
|
134
|
-
lines.push('## Summary');
|
|
135
|
-
lines.push('');
|
|
136
|
-
lines.push(`- **Files analyzed:** ${summary.totalFiles}`);
|
|
137
|
-
lines.push(`- **Components found:** ${summary.totalComponents}`);
|
|
138
|
-
lines.push(`- **Total issues:** ${summary.totalSmells}`);
|
|
139
|
-
lines.push(` - Errors: ${summary.smellsBySeverity.error}`);
|
|
140
|
-
lines.push(` - Warnings: ${summary.smellsBySeverity.warning}`);
|
|
141
|
-
lines.push(` - Info: ${summary.smellsBySeverity.info}`);
|
|
142
|
-
lines.push('');
|
|
143
|
-
|
|
144
|
-
if (summary.totalSmells > 0) {
|
|
145
|
-
lines.push('## Issues by Type');
|
|
146
|
-
lines.push('');
|
|
147
|
-
Object.entries(summary.smellsByType).forEach(([type, count]) => {
|
|
148
|
-
if (count > 0) {
|
|
149
|
-
lines.push(`- ${formatSmellType(type)}: ${count}`);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
lines.push('');
|
|
153
|
-
|
|
154
|
-
lines.push('## Detailed Findings');
|
|
155
|
-
lines.push('');
|
|
156
|
-
|
|
157
|
-
files.forEach(file => {
|
|
158
|
-
if (file.smells.length === 0) return;
|
|
159
|
-
|
|
160
|
-
const relativePath = path.relative(options.rootDir, file.file);
|
|
161
|
-
lines.push(`### ${relativePath}`);
|
|
162
|
-
lines.push('');
|
|
163
|
-
|
|
164
|
-
file.smells.forEach(smell => {
|
|
165
|
-
const icon = smell.severity === 'error' ? '🔴' : smell.severity === 'warning' ? '🟡' : '🔵';
|
|
166
|
-
lines.push(`#### ${icon} ${smell.message}`);
|
|
167
|
-
lines.push('');
|
|
168
|
-
lines.push(`- **Line:** ${smell.line}`);
|
|
169
|
-
lines.push(`- **Severity:** ${smell.severity}`);
|
|
170
|
-
lines.push(`- **Suggestion:** ${smell.suggestion}`);
|
|
171
|
-
|
|
172
|
-
if (options.showCodeSnippets && smell.codeSnippet) {
|
|
173
|
-
lines.push('');
|
|
174
|
-
lines.push('```tsx');
|
|
175
|
-
lines.push(smell.codeSnippet);
|
|
176
|
-
lines.push('```');
|
|
177
|
-
}
|
|
178
|
-
lines.push('');
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return lines.join('\n');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Helper functions
|
|
187
|
-
function getGradeColor(grade: string): (text: string) => string {
|
|
188
|
-
switch (grade) {
|
|
189
|
-
case 'A': return chalk.green.bold;
|
|
190
|
-
case 'B': return chalk.greenBright.bold;
|
|
191
|
-
case 'C': return chalk.yellow.bold;
|
|
192
|
-
case 'D': return chalk.rgb(255, 165, 0).bold;
|
|
193
|
-
case 'F': return chalk.red.bold;
|
|
194
|
-
default: return chalk.white.bold;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function getScoreBar(score: number): string {
|
|
199
|
-
const filled = Math.round(score / 5);
|
|
200
|
-
const empty = 20 - filled;
|
|
201
|
-
const color = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
|
|
202
|
-
return color('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function getSmallBar(score: number): string {
|
|
206
|
-
const filled = Math.round(score / 10);
|
|
207
|
-
const empty = 10 - filled;
|
|
208
|
-
const color = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
|
|
209
|
-
return color('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function getSeverityIcon(severity: SmellSeverity): string {
|
|
213
|
-
switch (severity) {
|
|
214
|
-
case 'error': return chalk.red('✖');
|
|
215
|
-
case 'warning': return chalk.yellow('⚠');
|
|
216
|
-
case 'info': return chalk.blue('ℹ');
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function getSeverityColor(severity: SmellSeverity): (text: string) => string {
|
|
221
|
-
switch (severity) {
|
|
222
|
-
case 'error': return chalk.red;
|
|
223
|
-
case 'warning': return chalk.yellow;
|
|
224
|
-
case 'info': return chalk.blue;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function getSeverityLabel(total: number, bySeverity: Record<SmellSeverity, number>): string {
|
|
229
|
-
const parts: string[] = [];
|
|
230
|
-
if (bySeverity.error > 0) parts.push(chalk.red(`${bySeverity.error} error(s)`));
|
|
231
|
-
if (bySeverity.warning > 0) parts.push(chalk.yellow(`${bySeverity.warning} warning(s)`));
|
|
232
|
-
if (bySeverity.info > 0) parts.push(chalk.blue(`${bySeverity.info} info`));
|
|
233
|
-
return parts.length > 0 ? parts.join(', ') : chalk.green('0');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function formatSmellType(type: string): string {
|
|
237
|
-
const labels: Record<string, string> = {
|
|
238
|
-
'useEffect-overuse': '⚡ useEffect Overuse',
|
|
239
|
-
'prop-drilling': '🔗 Prop Drilling',
|
|
240
|
-
'large-component': '📐 Large Component',
|
|
241
|
-
'unmemoized-calculation': '💾 Unmemoized Calculation',
|
|
242
|
-
'missing-dependency': '🔍 Missing Dependency',
|
|
243
|
-
'state-in-loop': '🔄 State in Loop',
|
|
244
|
-
'inline-function-prop': '📎 Inline Function Prop',
|
|
245
|
-
'deep-nesting': '📊 Deep Nesting',
|
|
246
|
-
'missing-key': '🔑 Missing Key',
|
|
247
|
-
'hooks-rules-violation': '⚠️ Hooks Rules Violation',
|
|
248
|
-
'dependency-array-issue': '📋 Dependency Array Issue',
|
|
249
|
-
'nested-ternary': '❓ Nested Ternary',
|
|
250
|
-
'dead-code': '💀 Dead Code',
|
|
251
|
-
'magic-value': '🔢 Magic Value',
|
|
252
|
-
// Next.js
|
|
253
|
-
'nextjs-client-server-boundary': '▲ Next.js Client/Server Boundary',
|
|
254
|
-
'nextjs-missing-metadata': '▲ Next.js Missing Metadata',
|
|
255
|
-
'nextjs-image-unoptimized': '▲ Next.js Unoptimized Image',
|
|
256
|
-
'nextjs-router-misuse': '▲ Next.js Router Misuse',
|
|
257
|
-
// React Native
|
|
258
|
-
'rn-inline-style': '📱 RN Inline Style',
|
|
259
|
-
'rn-missing-accessibility': '📱 RN Missing Accessibility',
|
|
260
|
-
'rn-performance-issue': '📱 RN Performance Issue',
|
|
261
|
-
// Node.js
|
|
262
|
-
'nodejs-callback-hell': '🟢 Node.js Callback Hell',
|
|
263
|
-
'nodejs-unhandled-promise': '🟢 Node.js Unhandled Promise',
|
|
264
|
-
'nodejs-sync-io': '🟢 Node.js Sync I/O',
|
|
265
|
-
'nodejs-missing-error-handling': '🟢 Node.js Missing Error Handling',
|
|
266
|
-
// JavaScript
|
|
267
|
-
'js-var-usage': '📜 JS var Usage',
|
|
268
|
-
'js-loose-equality': '📜 JS Loose Equality',
|
|
269
|
-
'js-implicit-coercion': '📜 JS Implicit Coercion',
|
|
270
|
-
'js-global-pollution': '📜 JS Global Pollution',
|
|
271
|
-
// TypeScript
|
|
272
|
-
'ts-any-usage': '🔷 TS any Usage',
|
|
273
|
-
'ts-missing-return-type': '🔷 TS Missing Return Type',
|
|
274
|
-
'ts-non-null-assertion': '🔷 TS Non-null Assertion',
|
|
275
|
-
'ts-type-assertion': '🔷 TS Type Assertion',
|
|
276
|
-
// Debug
|
|
277
|
-
'debug-statement': '🐛 Debug Statement',
|
|
278
|
-
'todo-comment': '📝 TODO/FIXME Comment',
|
|
279
|
-
// Security
|
|
280
|
-
'security-xss': '🔒 XSS Vulnerability',
|
|
281
|
-
'security-eval': '🔒 Eval Usage',
|
|
282
|
-
'security-secrets': '🔒 Exposed Secret',
|
|
283
|
-
// Accessibility
|
|
284
|
-
'a11y-missing-alt': '♿ Missing Alt Text',
|
|
285
|
-
'a11y-missing-label': '♿ Missing Label',
|
|
286
|
-
'a11y-interactive-role': '♿ Interactive Role',
|
|
287
|
-
'a11y-keyboard': '♿ Keyboard Handler',
|
|
288
|
-
'a11y-semantic': '♿ Semantic HTML',
|
|
289
|
-
};
|
|
290
|
-
return labels[type] || type;
|
|
291
|
-
}
|
package/src/types/index.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
export type SmellSeverity = 'error' | 'warning' | 'info';
|
|
2
|
-
|
|
3
|
-
export type SmellType =
|
|
4
|
-
| 'useEffect-overuse'
|
|
5
|
-
| 'prop-drilling'
|
|
6
|
-
| 'large-component'
|
|
7
|
-
| 'unmemoized-calculation'
|
|
8
|
-
| 'missing-dependency'
|
|
9
|
-
| 'state-in-loop'
|
|
10
|
-
| 'inline-function-prop'
|
|
11
|
-
| 'deep-nesting'
|
|
12
|
-
| 'missing-key'
|
|
13
|
-
| 'hooks-rules-violation'
|
|
14
|
-
| 'dependency-array-issue'
|
|
15
|
-
| 'nested-ternary'
|
|
16
|
-
| 'dead-code'
|
|
17
|
-
| 'magic-value'
|
|
18
|
-
// Debug/cleanup
|
|
19
|
-
| 'debug-statement'
|
|
20
|
-
| 'todo-comment'
|
|
21
|
-
// Security
|
|
22
|
-
| 'security-xss'
|
|
23
|
-
| 'security-eval'
|
|
24
|
-
| 'security-secrets'
|
|
25
|
-
// Accessibility
|
|
26
|
-
| 'a11y-missing-alt'
|
|
27
|
-
| 'a11y-missing-label'
|
|
28
|
-
| 'a11y-interactive-role'
|
|
29
|
-
| 'a11y-keyboard'
|
|
30
|
-
| 'a11y-semantic'
|
|
31
|
-
// Next.js specific
|
|
32
|
-
| 'nextjs-client-server-boundary'
|
|
33
|
-
| 'nextjs-missing-metadata'
|
|
34
|
-
| 'nextjs-image-unoptimized'
|
|
35
|
-
| 'nextjs-router-misuse'
|
|
36
|
-
// React Native specific
|
|
37
|
-
| 'rn-inline-style'
|
|
38
|
-
| 'rn-missing-accessibility'
|
|
39
|
-
| 'rn-performance-issue'
|
|
40
|
-
// Node.js specific
|
|
41
|
-
| 'nodejs-callback-hell'
|
|
42
|
-
| 'nodejs-unhandled-promise'
|
|
43
|
-
| 'nodejs-sync-io'
|
|
44
|
-
| 'nodejs-missing-error-handling'
|
|
45
|
-
// JavaScript specific
|
|
46
|
-
| 'js-var-usage'
|
|
47
|
-
| 'js-loose-equality'
|
|
48
|
-
| 'js-implicit-coercion'
|
|
49
|
-
| 'js-global-pollution'
|
|
50
|
-
// TypeScript specific
|
|
51
|
-
| 'ts-any-usage'
|
|
52
|
-
| 'ts-missing-return-type'
|
|
53
|
-
| 'ts-non-null-assertion'
|
|
54
|
-
| 'ts-type-assertion';
|
|
55
|
-
|
|
56
|
-
export interface CodeSmell {
|
|
57
|
-
type: SmellType;
|
|
58
|
-
severity: SmellSeverity;
|
|
59
|
-
message: string;
|
|
60
|
-
file: string;
|
|
61
|
-
line: number;
|
|
62
|
-
column: number;
|
|
63
|
-
suggestion: string;
|
|
64
|
-
codeSnippet?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface ComponentInfo {
|
|
68
|
-
name: string;
|
|
69
|
-
file: string;
|
|
70
|
-
startLine: number;
|
|
71
|
-
endLine: number;
|
|
72
|
-
lineCount: number;
|
|
73
|
-
useEffectCount: number;
|
|
74
|
-
useStateCount: number;
|
|
75
|
-
useMemoCount: number;
|
|
76
|
-
useCallbackCount: number;
|
|
77
|
-
propsCount: number;
|
|
78
|
-
propsDrillingDepth: number;
|
|
79
|
-
hasExpensiveCalculation: boolean;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface FileAnalysis {
|
|
83
|
-
file: string;
|
|
84
|
-
components: ComponentInfo[];
|
|
85
|
-
smells: CodeSmell[];
|
|
86
|
-
imports: string[];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface AnalysisResult {
|
|
90
|
-
files: FileAnalysis[];
|
|
91
|
-
summary: AnalysisSummary;
|
|
92
|
-
debtScore: TechnicalDebtScore;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface AnalysisSummary {
|
|
96
|
-
totalFiles: number;
|
|
97
|
-
totalComponents: number;
|
|
98
|
-
totalSmells: number;
|
|
99
|
-
smellsByType: Record<SmellType, number>;
|
|
100
|
-
smellsBySeverity: Record<SmellSeverity, number>;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface TechnicalDebtScore {
|
|
104
|
-
score: number; // 0-100, 100 = no debt
|
|
105
|
-
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
106
|
-
breakdown: {
|
|
107
|
-
useEffectScore: number;
|
|
108
|
-
propDrillingScore: number;
|
|
109
|
-
componentSizeScore: number;
|
|
110
|
-
memoizationScore: number;
|
|
111
|
-
};
|
|
112
|
-
estimatedRefactorTime: string; // e.g., "2-4 hours"
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface DetectorConfig {
|
|
116
|
-
maxUseEffectsPerComponent: number;
|
|
117
|
-
maxPropDrillingDepth: number;
|
|
118
|
-
maxComponentLines: number;
|
|
119
|
-
maxPropsCount: number;
|
|
120
|
-
checkMemoization: boolean;
|
|
121
|
-
checkMissingKeys: boolean;
|
|
122
|
-
checkHooksRules: boolean;
|
|
123
|
-
checkDependencyArrays: boolean;
|
|
124
|
-
maxTernaryDepth: number;
|
|
125
|
-
checkDeadCode: boolean;
|
|
126
|
-
checkMagicValues: boolean;
|
|
127
|
-
magicNumberThreshold: number;
|
|
128
|
-
// Framework detection
|
|
129
|
-
checkNextjs: boolean;
|
|
130
|
-
checkReactNative: boolean;
|
|
131
|
-
checkNodejs: boolean;
|
|
132
|
-
checkJavascript: boolean;
|
|
133
|
-
checkTypescript: boolean;
|
|
134
|
-
maxCallbackDepth: number;
|
|
135
|
-
// New detectors
|
|
136
|
-
checkDebugStatements: boolean;
|
|
137
|
-
checkSecurity: boolean;
|
|
138
|
-
checkAccessibility: boolean;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export const DEFAULT_CONFIG: DetectorConfig = {
|
|
142
|
-
maxUseEffectsPerComponent: 3,
|
|
143
|
-
maxPropDrillingDepth: 3,
|
|
144
|
-
maxComponentLines: 300,
|
|
145
|
-
maxPropsCount: 7,
|
|
146
|
-
checkMemoization: true,
|
|
147
|
-
checkMissingKeys: true,
|
|
148
|
-
checkHooksRules: true,
|
|
149
|
-
checkDependencyArrays: true,
|
|
150
|
-
maxTernaryDepth: 2,
|
|
151
|
-
checkDeadCode: true,
|
|
152
|
-
checkMagicValues: true,
|
|
153
|
-
magicNumberThreshold: 10,
|
|
154
|
-
// Framework detection - auto-enabled based on project
|
|
155
|
-
checkNextjs: true,
|
|
156
|
-
checkReactNative: true,
|
|
157
|
-
checkNodejs: true,
|
|
158
|
-
checkJavascript: true,
|
|
159
|
-
checkTypescript: true,
|
|
160
|
-
maxCallbackDepth: 3,
|
|
161
|
-
// New detectors
|
|
162
|
-
checkDebugStatements: true,
|
|
163
|
-
checkSecurity: true,
|
|
164
|
-
checkAccessibility: true,
|
|
165
|
-
};
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"declaration": true,
|
|
10
|
-
"declarationMap": true,
|
|
11
|
-
"strict": true,
|
|
12
|
-
"esModuleInterop": true,
|
|
13
|
-
"skipLibCheck": true,
|
|
14
|
-
"forceConsistentCasingInFileNames": true,
|
|
15
|
-
"resolveJsonModule": true
|
|
16
|
-
},
|
|
17
|
-
"include": ["src/**/*"],
|
|
18
|
-
"exclude": ["node_modules", "dist"]
|
|
19
|
-
}
|