react-code-smell-detector 1.0.0 → 1.0.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/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +13 -1
- package/dist/cli.js +0 -0
- package/dist/detectors/deadCode.d.ts +14 -0
- package/dist/detectors/deadCode.d.ts.map +1 -0
- package/dist/detectors/deadCode.js +141 -0
- package/dist/detectors/dependencyArray.d.ts +7 -0
- package/dist/detectors/dependencyArray.d.ts.map +1 -0
- package/dist/detectors/dependencyArray.js +164 -0
- package/dist/detectors/hooksRules.d.ts +7 -0
- package/dist/detectors/hooksRules.d.ts.map +1 -0
- package/dist/detectors/hooksRules.js +81 -0
- package/dist/detectors/index.d.ts +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +6 -0
- package/dist/detectors/magicValues.d.ts +7 -0
- package/dist/detectors/magicValues.d.ts.map +1 -0
- package/dist/detectors/magicValues.js +99 -0
- package/dist/detectors/missingKey.d.ts +7 -0
- package/dist/detectors/missingKey.d.ts.map +1 -0
- package/dist/detectors/missingKey.js +93 -0
- package/dist/detectors/nestedTernary.d.ts +7 -0
- package/dist/detectors/nestedTernary.d.ts.map +1 -0
- package/dist/detectors/nestedTernary.js +58 -0
- package/dist/reporter.js +6 -0
- package/dist/types/index.d.ts +8 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +7 -0
- package/package.json +1 -1
- package/src/analyzer.ts +18 -0
- package/src/detectors/deadCode.ts +163 -0
- package/src/detectors/dependencyArray.ts +176 -0
- package/src/detectors/hooksRules.ts +101 -0
- package/src/detectors/index.ts +6 -0
- package/src/detectors/magicValues.ts +114 -0
- package/src/detectors/missingKey.ts +105 -0
- package/src/detectors/nestedTernary.ts +75 -0
- package/src/reporter.ts +6 -0
- package/src/types/index.ts +21 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { NodePath } from '@babel/traverse';
|
|
3
|
+
import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
|
|
4
|
+
import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detects hooks used inside conditionals or loops (violates Rules of Hooks)
|
|
8
|
+
*/
|
|
9
|
+
export function detectHooksRulesViolations(
|
|
10
|
+
component: ParsedComponent,
|
|
11
|
+
filePath: string,
|
|
12
|
+
sourceCode: string,
|
|
13
|
+
config: DetectorConfig = DEFAULT_CONFIG
|
|
14
|
+
): CodeSmell[] {
|
|
15
|
+
if (!config.checkHooksRules) return [];
|
|
16
|
+
|
|
17
|
+
const smells: CodeSmell[] = [];
|
|
18
|
+
const hooks = [
|
|
19
|
+
'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback',
|
|
20
|
+
'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect',
|
|
21
|
+
'useDebugValue', 'useDeferredValue', 'useTransition', 'useId',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
component.path.traverse({
|
|
25
|
+
CallExpression(path) {
|
|
26
|
+
// Check if it's a hook call
|
|
27
|
+
const callee = path.node.callee;
|
|
28
|
+
let hookName: string | null = null;
|
|
29
|
+
|
|
30
|
+
if (t.isIdentifier(callee) && hooks.includes(callee.name)) {
|
|
31
|
+
hookName = callee.name;
|
|
32
|
+
} else if (t.isIdentifier(callee) && callee.name.startsWith('use') && callee.name[3]?.toUpperCase() === callee.name[3]) {
|
|
33
|
+
// Custom hooks (useXxx pattern)
|
|
34
|
+
hookName = callee.name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!hookName) return;
|
|
38
|
+
|
|
39
|
+
// Check if hook is inside a conditional or loop
|
|
40
|
+
let currentPath: NodePath | null = path.parentPath;
|
|
41
|
+
|
|
42
|
+
while (currentPath) {
|
|
43
|
+
const node = currentPath.node;
|
|
44
|
+
|
|
45
|
+
// Check for conditionals
|
|
46
|
+
if (t.isIfStatement(node) || t.isConditionalExpression(node)) {
|
|
47
|
+
const loc = path.node.loc;
|
|
48
|
+
smells.push({
|
|
49
|
+
type: 'hooks-rules-violation',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
message: `Hook "${hookName}" called inside a conditional in "${component.name}"`,
|
|
52
|
+
file: filePath,
|
|
53
|
+
line: loc?.start.line || 0,
|
|
54
|
+
column: loc?.start.column || 0,
|
|
55
|
+
suggestion: 'Hooks must be called at the top level of the component. Move the hook call outside the conditional.',
|
|
56
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for loops
|
|
62
|
+
if (
|
|
63
|
+
t.isForStatement(node) ||
|
|
64
|
+
t.isWhileStatement(node) ||
|
|
65
|
+
t.isDoWhileStatement(node) ||
|
|
66
|
+
t.isForInStatement(node) ||
|
|
67
|
+
t.isForOfStatement(node)
|
|
68
|
+
) {
|
|
69
|
+
const loc = path.node.loc;
|
|
70
|
+
smells.push({
|
|
71
|
+
type: 'hooks-rules-violation',
|
|
72
|
+
severity: 'error',
|
|
73
|
+
message: `Hook "${hookName}" called inside a loop in "${component.name}"`,
|
|
74
|
+
file: filePath,
|
|
75
|
+
line: loc?.start.line || 0,
|
|
76
|
+
column: loc?.start.column || 0,
|
|
77
|
+
suggestion: 'Hooks must be called at the top level. Consider restructuring to avoid calling hooks in loops.',
|
|
78
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
79
|
+
});
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for early return before hook (within same function)
|
|
84
|
+
// This would require more complex flow analysis
|
|
85
|
+
|
|
86
|
+
// Stop traversing when we reach the component function
|
|
87
|
+
if (
|
|
88
|
+
t.isFunctionDeclaration(node) ||
|
|
89
|
+
t.isArrowFunctionExpression(node) ||
|
|
90
|
+
t.isFunctionExpression(node)
|
|
91
|
+
) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
currentPath = currentPath.parentPath;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return smells;
|
|
101
|
+
}
|
package/src/detectors/index.ts
CHANGED
|
@@ -2,3 +2,9 @@ export { detectUseEffectOveruse } from './useEffect.js';
|
|
|
2
2
|
export { detectPropDrilling, analyzePropDrillingDepth } from './propDrilling.js';
|
|
3
3
|
export { detectLargeComponent } from './largeComponent.js';
|
|
4
4
|
export { detectUnmemoizedCalculations } from './memoization.js';
|
|
5
|
+
export { detectMissingKeys } from './missingKey.js';
|
|
6
|
+
export { detectHooksRulesViolations } from './hooksRules.js';
|
|
7
|
+
export { detectDependencyArrayIssues } from './dependencyArray.js';
|
|
8
|
+
export { detectNestedTernaries } from './nestedTernary.js';
|
|
9
|
+
export { detectDeadCode, detectUnusedImports } from './deadCode.js';
|
|
10
|
+
export { detectMagicValues } from './magicValues.js';
|
|
@@ -0,0 +1,114 @@
|
|
|
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 magic numbers and strings that should be constants
|
|
7
|
+
*/
|
|
8
|
+
export function detectMagicValues(
|
|
9
|
+
component: ParsedComponent,
|
|
10
|
+
filePath: string,
|
|
11
|
+
sourceCode: string,
|
|
12
|
+
config: DetectorConfig = DEFAULT_CONFIG
|
|
13
|
+
): CodeSmell[] {
|
|
14
|
+
if (!config.checkMagicValues) return [];
|
|
15
|
+
|
|
16
|
+
const smells: CodeSmell[] = [];
|
|
17
|
+
const threshold = config.magicNumberThreshold;
|
|
18
|
+
|
|
19
|
+
// Track locations to avoid duplicate reports
|
|
20
|
+
const reportedLines = new Set<number>();
|
|
21
|
+
|
|
22
|
+
component.path.traverse({
|
|
23
|
+
NumericLiteral(path) {
|
|
24
|
+
const value = path.node.value;
|
|
25
|
+
const loc = path.node.loc;
|
|
26
|
+
const line = loc?.start.line || 0;
|
|
27
|
+
|
|
28
|
+
// Skip common acceptable values
|
|
29
|
+
if (value === 0 || value === 1 || value === -1 || value === 2 || value === 100) return;
|
|
30
|
+
|
|
31
|
+
// Skip array indices and small numbers
|
|
32
|
+
if (value < threshold && value > -threshold) return;
|
|
33
|
+
|
|
34
|
+
// Skip if inside an object key position (e.g., style objects)
|
|
35
|
+
if (t.isObjectProperty(path.parent) && path.parent.key === path.node) return;
|
|
36
|
+
|
|
37
|
+
// Skip if it's a port number or similar config
|
|
38
|
+
if (value === 3000 || value === 8080 || value === 80 || value === 443) return;
|
|
39
|
+
|
|
40
|
+
// Skip if already reported this line
|
|
41
|
+
if (reportedLines.has(line)) return;
|
|
42
|
+
reportedLines.add(line);
|
|
43
|
+
|
|
44
|
+
// Check context - skip if it's in a variable declaration that names the constant
|
|
45
|
+
if (t.isVariableDeclarator(path.parent)) {
|
|
46
|
+
const varName = t.isIdentifier(path.parent.id) ? path.parent.id.name : '';
|
|
47
|
+
// If variable name is UPPER_CASE, it's already a constant
|
|
48
|
+
if (varName === varName.toUpperCase() && varName.includes('_')) return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
smells.push({
|
|
52
|
+
type: 'magic-value',
|
|
53
|
+
severity: 'info',
|
|
54
|
+
message: `Magic number ${value} in "${component.name}"`,
|
|
55
|
+
file: filePath,
|
|
56
|
+
line,
|
|
57
|
+
column: loc?.start.column || 0,
|
|
58
|
+
suggestion: `Extract to a named constant: const DESCRIPTIVE_NAME = ${value}`,
|
|
59
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
StringLiteral(path) {
|
|
64
|
+
const value = path.node.value;
|
|
65
|
+
const loc = path.node.loc;
|
|
66
|
+
const line = loc?.start.line || 0;
|
|
67
|
+
|
|
68
|
+
// Skip short strings, empty strings, common values
|
|
69
|
+
if (value.length < 10) return;
|
|
70
|
+
|
|
71
|
+
// Skip if it looks like a URL, path, or import
|
|
72
|
+
if (value.startsWith('http') || value.startsWith('/') || value.startsWith('.')) return;
|
|
73
|
+
|
|
74
|
+
// Skip if it's an import source
|
|
75
|
+
if (t.isImportDeclaration(path.parent)) return;
|
|
76
|
+
|
|
77
|
+
// Skip JSX text content (usually intentional)
|
|
78
|
+
if (t.isJSXText(path.parent) || t.isJSXExpressionContainer(path.parent)) return;
|
|
79
|
+
|
|
80
|
+
// Skip object property keys
|
|
81
|
+
if (t.isObjectProperty(path.parent) && path.parent.key === path.node) return;
|
|
82
|
+
|
|
83
|
+
// Skip if already reported this line
|
|
84
|
+
if (reportedLines.has(line)) return;
|
|
85
|
+
|
|
86
|
+
// Skip common patterns
|
|
87
|
+
if (
|
|
88
|
+
value.includes('className') ||
|
|
89
|
+
value.includes('px') ||
|
|
90
|
+
value.includes('rem') ||
|
|
91
|
+
value.includes('%')
|
|
92
|
+
) return;
|
|
93
|
+
|
|
94
|
+
// Check if it looks like a user-facing message or error
|
|
95
|
+
const looksLikeMessage = /^[A-Z][a-z]+.*[.!?]?$/.test(value) || value.includes(' ');
|
|
96
|
+
|
|
97
|
+
if (looksLikeMessage && value.length > 30) {
|
|
98
|
+
reportedLines.add(line);
|
|
99
|
+
smells.push({
|
|
100
|
+
type: 'magic-value',
|
|
101
|
+
severity: 'info',
|
|
102
|
+
message: `Hardcoded string in "${component.name}" should be in constants or i18n`,
|
|
103
|
+
file: filePath,
|
|
104
|
+
line,
|
|
105
|
+
column: loc?.start.column || 0,
|
|
106
|
+
suggestion: `Extract to a constants file or use i18n: "${value.substring(0, 30)}..."`,
|
|
107
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return smells;
|
|
114
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 .map() calls that render JSX without a key prop
|
|
7
|
+
*/
|
|
8
|
+
export function detectMissingKeys(
|
|
9
|
+
component: ParsedComponent,
|
|
10
|
+
filePath: string,
|
|
11
|
+
sourceCode: string,
|
|
12
|
+
config: DetectorConfig = DEFAULT_CONFIG
|
|
13
|
+
): CodeSmell[] {
|
|
14
|
+
if (!config.checkMissingKeys) return [];
|
|
15
|
+
|
|
16
|
+
const smells: CodeSmell[] = [];
|
|
17
|
+
|
|
18
|
+
component.path.traverse({
|
|
19
|
+
CallExpression(path) {
|
|
20
|
+
// Check if it's a .map() call
|
|
21
|
+
if (!t.isMemberExpression(path.node.callee)) return;
|
|
22
|
+
|
|
23
|
+
const prop = path.node.callee.property;
|
|
24
|
+
if (!t.isIdentifier(prop) || prop.name !== 'map') return;
|
|
25
|
+
|
|
26
|
+
// Check if the callback returns JSX
|
|
27
|
+
const callback = path.node.arguments[0];
|
|
28
|
+
if (!callback) return;
|
|
29
|
+
|
|
30
|
+
let callbackBody: t.Node | null = null;
|
|
31
|
+
|
|
32
|
+
if (t.isArrowFunctionExpression(callback)) {
|
|
33
|
+
callbackBody = callback.body;
|
|
34
|
+
} else if (t.isFunctionExpression(callback)) {
|
|
35
|
+
callbackBody = callback.body;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!callbackBody) return;
|
|
39
|
+
|
|
40
|
+
// Find JSX elements in the callback
|
|
41
|
+
let hasJSX = false;
|
|
42
|
+
let hasKey = false;
|
|
43
|
+
let jsxLine = path.node.loc?.start.line || 0;
|
|
44
|
+
|
|
45
|
+
const checkForKey = (node: t.Node) => {
|
|
46
|
+
if (t.isJSXElement(node)) {
|
|
47
|
+
hasJSX = true;
|
|
48
|
+
jsxLine = node.loc?.start.line || jsxLine;
|
|
49
|
+
|
|
50
|
+
// Check if the opening element has a key prop
|
|
51
|
+
const openingElement = node.openingElement;
|
|
52
|
+
const keyAttr = openingElement.attributes.find(attr => {
|
|
53
|
+
if (t.isJSXAttribute(attr)) {
|
|
54
|
+
return t.isJSXIdentifier(attr.name) && attr.name.name === 'key';
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (keyAttr) {
|
|
60
|
+
hasKey = true;
|
|
61
|
+
}
|
|
62
|
+
} else if (t.isJSXFragment(node)) {
|
|
63
|
+
hasJSX = true;
|
|
64
|
+
// Fragments can't have keys directly, so check children
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Traverse the callback body to find JSX
|
|
69
|
+
if (t.isJSXElement(callbackBody) || t.isJSXFragment(callbackBody)) {
|
|
70
|
+
checkForKey(callbackBody);
|
|
71
|
+
} else if (t.isBlockStatement(callbackBody)) {
|
|
72
|
+
path.traverse({
|
|
73
|
+
ReturnStatement(returnPath) {
|
|
74
|
+
if (returnPath.node.argument) {
|
|
75
|
+
checkForKey(returnPath.node.argument);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
JSXElement(jsxPath) {
|
|
79
|
+
// Only check top-level JSX in the map callback
|
|
80
|
+
if (jsxPath.parent === callbackBody ||
|
|
81
|
+
(t.isReturnStatement(jsxPath.parent) && jsxPath.parent.argument === jsxPath.node)) {
|
|
82
|
+
checkForKey(jsxPath.node);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (hasJSX && !hasKey) {
|
|
89
|
+
const loc = path.node.loc;
|
|
90
|
+
smells.push({
|
|
91
|
+
type: 'missing-key',
|
|
92
|
+
severity: 'error',
|
|
93
|
+
message: `Missing "key" prop in .map() that renders JSX in "${component.name}"`,
|
|
94
|
+
file: filePath,
|
|
95
|
+
line: jsxLine || loc?.start.line || 0,
|
|
96
|
+
column: loc?.start.column || 0,
|
|
97
|
+
suggestion: 'Add a unique "key" prop to the root element: <Element key={item.id}>',
|
|
98
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return smells;
|
|
105
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { NodePath } from '@babel/traverse';
|
|
3
|
+
import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
|
|
4
|
+
import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detects deeply nested ternary expressions (complex conditional rendering)
|
|
8
|
+
*/
|
|
9
|
+
export function detectNestedTernaries(
|
|
10
|
+
component: ParsedComponent,
|
|
11
|
+
filePath: string,
|
|
12
|
+
sourceCode: string,
|
|
13
|
+
config: DetectorConfig = DEFAULT_CONFIG
|
|
14
|
+
): CodeSmell[] {
|
|
15
|
+
const smells: CodeSmell[] = [];
|
|
16
|
+
const maxDepth = config.maxTernaryDepth;
|
|
17
|
+
|
|
18
|
+
component.path.traverse({
|
|
19
|
+
ConditionalExpression(path) {
|
|
20
|
+
// Only check the outermost ternary
|
|
21
|
+
if (isNestedInTernary(path)) return;
|
|
22
|
+
|
|
23
|
+
const depth = getTernaryDepth(path.node);
|
|
24
|
+
|
|
25
|
+
if (depth > maxDepth) {
|
|
26
|
+
const loc = path.node.loc;
|
|
27
|
+
smells.push({
|
|
28
|
+
type: 'nested-ternary',
|
|
29
|
+
severity: depth > maxDepth + 1 ? 'warning' : 'info',
|
|
30
|
+
message: `Nested ternary expression (depth: ${depth}) in "${component.name}"`,
|
|
31
|
+
file: filePath,
|
|
32
|
+
line: loc?.start.line || 0,
|
|
33
|
+
column: loc?.start.column || 0,
|
|
34
|
+
suggestion: `Refactor to use if/else, switch, or extract into separate components. Max recommended depth: ${maxDepth}`,
|
|
35
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return smells;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a ternary is nested inside another ternary
|
|
46
|
+
*/
|
|
47
|
+
function isNestedInTernary(path: NodePath): boolean {
|
|
48
|
+
let current: NodePath | null = path.parentPath;
|
|
49
|
+
|
|
50
|
+
while (current) {
|
|
51
|
+
if (t.isConditionalExpression(current.node)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
current = current.parentPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Calculate the depth of nested ternary expressions
|
|
62
|
+
*/
|
|
63
|
+
function getTernaryDepth(node: t.ConditionalExpression): number {
|
|
64
|
+
let maxChildDepth = 0;
|
|
65
|
+
|
|
66
|
+
if (t.isConditionalExpression(node.consequent)) {
|
|
67
|
+
maxChildDepth = Math.max(maxChildDepth, getTernaryDepth(node.consequent));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (t.isConditionalExpression(node.alternate)) {
|
|
71
|
+
maxChildDepth = Math.max(maxChildDepth, getTernaryDepth(node.alternate));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return 1 + maxChildDepth;
|
|
75
|
+
}
|
package/src/reporter.ts
CHANGED
|
@@ -243,6 +243,12 @@ function formatSmellType(type: string): string {
|
|
|
243
243
|
'state-in-loop': '🔄 State in Loop',
|
|
244
244
|
'inline-function-prop': '📎 Inline Function Prop',
|
|
245
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',
|
|
246
252
|
};
|
|
247
253
|
return labels[type] || type;
|
|
248
254
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -8,7 +8,13 @@ export type SmellType =
|
|
|
8
8
|
| 'missing-dependency'
|
|
9
9
|
| 'state-in-loop'
|
|
10
10
|
| 'inline-function-prop'
|
|
11
|
-
| 'deep-nesting'
|
|
11
|
+
| 'deep-nesting'
|
|
12
|
+
| 'missing-key'
|
|
13
|
+
| 'hooks-rules-violation'
|
|
14
|
+
| 'dependency-array-issue'
|
|
15
|
+
| 'nested-ternary'
|
|
16
|
+
| 'dead-code'
|
|
17
|
+
| 'magic-value';
|
|
12
18
|
|
|
13
19
|
export interface CodeSmell {
|
|
14
20
|
type: SmellType;
|
|
@@ -75,6 +81,13 @@ export interface DetectorConfig {
|
|
|
75
81
|
maxComponentLines: number;
|
|
76
82
|
maxPropsCount: number;
|
|
77
83
|
checkMemoization: boolean;
|
|
84
|
+
checkMissingKeys: boolean;
|
|
85
|
+
checkHooksRules: boolean;
|
|
86
|
+
checkDependencyArrays: boolean;
|
|
87
|
+
maxTernaryDepth: number;
|
|
88
|
+
checkDeadCode: boolean;
|
|
89
|
+
checkMagicValues: boolean;
|
|
90
|
+
magicNumberThreshold: number; // Numbers above this are flagged
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
export const DEFAULT_CONFIG: DetectorConfig = {
|
|
@@ -83,4 +96,11 @@ export const DEFAULT_CONFIG: DetectorConfig = {
|
|
|
83
96
|
maxComponentLines: 300,
|
|
84
97
|
maxPropsCount: 7,
|
|
85
98
|
checkMemoization: true,
|
|
99
|
+
checkMissingKeys: true,
|
|
100
|
+
checkHooksRules: true,
|
|
101
|
+
checkDependencyArrays: true,
|
|
102
|
+
maxTernaryDepth: 2,
|
|
103
|
+
checkDeadCode: true,
|
|
104
|
+
checkMagicValues: true,
|
|
105
|
+
magicNumberThreshold: 10,
|
|
86
106
|
};
|