react-code-smell-detector 1.0.0 → 1.1.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 +43 -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 +11 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +12 -0
- package/dist/detectors/javascript.d.ts +11 -0
- package/dist/detectors/javascript.d.ts.map +1 -0
- package/dist/detectors/javascript.js +148 -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/detectors/nextjs.d.ts +11 -0
- package/dist/detectors/nextjs.d.ts.map +1 -0
- package/dist/detectors/nextjs.js +103 -0
- package/dist/detectors/nodejs.d.ts +11 -0
- package/dist/detectors/nodejs.d.ts.map +1 -0
- package/dist/detectors/nodejs.js +169 -0
- package/dist/detectors/reactNative.d.ts +10 -0
- package/dist/detectors/reactNative.d.ts.map +1 -0
- package/dist/detectors/reactNative.js +135 -0
- package/dist/detectors/typescript.d.ts +11 -0
- package/dist/detectors/typescript.d.ts.map +1 -0
- package/dist/detectors/typescript.js +135 -0
- package/dist/reporter.js +30 -0
- package/dist/types/index.d.ts +14 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +14 -0
- package/package.json +1 -1
- package/src/analyzer.ts +53 -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 +12 -0
- package/src/detectors/javascript.ts +169 -0
- package/src/detectors/magicValues.ts +114 -0
- package/src/detectors/missingKey.ts +105 -0
- package/src/detectors/nestedTernary.ts +75 -0
- package/src/detectors/nextjs.ts +124 -0
- package/src/detectors/nodejs.ts +199 -0
- package/src/detectors/reactNative.ts +154 -0
- package/src/detectors/typescript.ts +151 -0
- package/src/reporter.ts +30 -0
- package/src/types/index.ts +59 -1
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
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 potential issues with useEffect dependency arrays
|
|
7
|
+
*/
|
|
8
|
+
export function detectDependencyArrayIssues(
|
|
9
|
+
component: ParsedComponent,
|
|
10
|
+
filePath: string,
|
|
11
|
+
sourceCode: string,
|
|
12
|
+
config: DetectorConfig = DEFAULT_CONFIG
|
|
13
|
+
): CodeSmell[] {
|
|
14
|
+
if (!config.checkDependencyArrays) return [];
|
|
15
|
+
|
|
16
|
+
const smells: CodeSmell[] = [];
|
|
17
|
+
|
|
18
|
+
// Analyze each useEffect
|
|
19
|
+
component.hooks.useEffect.forEach(effectCall => {
|
|
20
|
+
const args = effectCall.arguments;
|
|
21
|
+
const callback = args[0];
|
|
22
|
+
const depsArray = args[1];
|
|
23
|
+
|
|
24
|
+
const loc = effectCall.loc;
|
|
25
|
+
const line = loc?.start.line || 0;
|
|
26
|
+
|
|
27
|
+
// Check 1: Missing dependency array entirely
|
|
28
|
+
if (!depsArray) {
|
|
29
|
+
smells.push({
|
|
30
|
+
type: 'dependency-array-issue',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
message: `useEffect without dependency array in "${component.name}" runs on every render`,
|
|
33
|
+
file: filePath,
|
|
34
|
+
line,
|
|
35
|
+
column: loc?.start.column || 0,
|
|
36
|
+
suggestion: 'Add a dependency array. Use [] for mount-only effect, or [dep1, dep2] for reactive dependencies.',
|
|
37
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check 2: Empty dependency array with variables used inside
|
|
43
|
+
if (t.isArrayExpression(depsArray) && depsArray.elements.length === 0) {
|
|
44
|
+
// Collect variables used in the effect callback
|
|
45
|
+
const usedVariables = new Set<string>();
|
|
46
|
+
|
|
47
|
+
if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
|
|
48
|
+
collectVariablesUsed(callback.body, usedVariables);
|
|
49
|
+
|
|
50
|
+
// Filter out common globals and React hooks
|
|
51
|
+
const globals = new Set(['console', 'window', 'document', 'setTimeout', 'setInterval',
|
|
52
|
+
'clearTimeout', 'clearInterval', 'fetch', 'Promise', 'JSON', 'Math', 'Date', 'Array',
|
|
53
|
+
'Object', 'String', 'Number', 'Boolean', 'Error', 'undefined', 'null', 'true', 'false']);
|
|
54
|
+
|
|
55
|
+
// Check if there are local variables that might need to be dependencies
|
|
56
|
+
const potentialMissing = [...usedVariables].filter(v =>
|
|
57
|
+
!globals.has(v) &&
|
|
58
|
+
!v.startsWith('set') && // Setters from useState are stable
|
|
59
|
+
v !== 'props' &&
|
|
60
|
+
v !== 'children'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Only warn if there appear to be external variables used
|
|
64
|
+
if (potentialMissing.length > 3) {
|
|
65
|
+
smells.push({
|
|
66
|
+
type: 'dependency-array-issue',
|
|
67
|
+
severity: 'info',
|
|
68
|
+
message: `useEffect in "${component.name}" has empty deps array but uses external variables`,
|
|
69
|
+
file: filePath,
|
|
70
|
+
line,
|
|
71
|
+
column: loc?.start.column || 0,
|
|
72
|
+
suggestion: `Consider adding dependencies: [${potentialMissing.slice(0, 3).join(', ')}...]`,
|
|
73
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check 3: Dependency array with object/array literals
|
|
80
|
+
if (t.isArrayExpression(depsArray)) {
|
|
81
|
+
depsArray.elements.forEach((elem, index) => {
|
|
82
|
+
if (t.isObjectExpression(elem) || t.isArrayExpression(elem)) {
|
|
83
|
+
smells.push({
|
|
84
|
+
type: 'dependency-array-issue',
|
|
85
|
+
severity: 'warning',
|
|
86
|
+
message: `Object/array literal in useEffect dependency array in "${component.name}" causes infinite re-renders`,
|
|
87
|
+
file: filePath,
|
|
88
|
+
line,
|
|
89
|
+
column: loc?.start.column || 0,
|
|
90
|
+
suggestion: 'Move the object/array to a useMemo or extract to a constant outside the component.',
|
|
91
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for inline functions
|
|
96
|
+
if (t.isArrowFunctionExpression(elem) || t.isFunctionExpression(elem)) {
|
|
97
|
+
smells.push({
|
|
98
|
+
type: 'dependency-array-issue',
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
message: `Inline function in useEffect dependency array in "${component.name}" causes infinite re-renders`,
|
|
101
|
+
file: filePath,
|
|
102
|
+
line,
|
|
103
|
+
column: loc?.start.column || 0,
|
|
104
|
+
suggestion: 'Wrap the function in useCallback before using as a dependency.',
|
|
105
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Also check useMemo and useCallback
|
|
113
|
+
[...component.hooks.useMemo, ...component.hooks.useCallback].forEach(hookCall => {
|
|
114
|
+
const depsArray = hookCall.arguments[1];
|
|
115
|
+
const loc = hookCall.loc;
|
|
116
|
+
const line = loc?.start.line || 0;
|
|
117
|
+
|
|
118
|
+
if (!depsArray) {
|
|
119
|
+
const hookName = t.isIdentifier((hookCall.callee as t.Identifier))
|
|
120
|
+
? ((hookCall.callee as t.Identifier).name)
|
|
121
|
+
: 'useMemo/useCallback';
|
|
122
|
+
|
|
123
|
+
smells.push({
|
|
124
|
+
type: 'dependency-array-issue',
|
|
125
|
+
severity: 'warning',
|
|
126
|
+
message: `${hookName} without dependency array in "${component.name}"`,
|
|
127
|
+
file: filePath,
|
|
128
|
+
line,
|
|
129
|
+
column: loc?.start.column || 0,
|
|
130
|
+
suggestion: 'Add a dependency array to specify when the value should be recalculated.',
|
|
131
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return smells;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Helper to collect variable names used in a code block
|
|
141
|
+
*/
|
|
142
|
+
function collectVariablesUsed(node: t.Node, variables: Set<string>): void {
|
|
143
|
+
if (t.isIdentifier(node)) {
|
|
144
|
+
variables.add(node.name);
|
|
145
|
+
} else if (t.isMemberExpression(node)) {
|
|
146
|
+
if (t.isIdentifier(node.object)) {
|
|
147
|
+
variables.add(node.object.name);
|
|
148
|
+
} else {
|
|
149
|
+
collectVariablesUsed(node.object, variables);
|
|
150
|
+
}
|
|
151
|
+
} else if (t.isBlockStatement(node)) {
|
|
152
|
+
node.body.forEach(stmt => collectVariablesUsed(stmt, variables));
|
|
153
|
+
} else if (t.isExpressionStatement(node)) {
|
|
154
|
+
collectVariablesUsed(node.expression, variables);
|
|
155
|
+
} else if (t.isCallExpression(node)) {
|
|
156
|
+
if (t.isIdentifier(node.callee)) {
|
|
157
|
+
variables.add(node.callee.name);
|
|
158
|
+
}
|
|
159
|
+
node.arguments.forEach(arg => {
|
|
160
|
+
if (t.isNode(arg)) collectVariablesUsed(arg, variables);
|
|
161
|
+
});
|
|
162
|
+
} else if (t.isConditionalExpression(node)) {
|
|
163
|
+
collectVariablesUsed(node.test, variables);
|
|
164
|
+
collectVariablesUsed(node.consequent, variables);
|
|
165
|
+
collectVariablesUsed(node.alternate, variables);
|
|
166
|
+
} else if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
|
|
167
|
+
collectVariablesUsed(node.left, variables);
|
|
168
|
+
collectVariablesUsed(node.right, variables);
|
|
169
|
+
} else if (t.isReturnStatement(node) && node.argument) {
|
|
170
|
+
collectVariablesUsed(node.argument, variables);
|
|
171
|
+
} else if (t.isIfStatement(node)) {
|
|
172
|
+
collectVariablesUsed(node.test, variables);
|
|
173
|
+
collectVariablesUsed(node.consequent, variables);
|
|
174
|
+
if (node.alternate) collectVariablesUsed(node.alternate, variables);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -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,15 @@ 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';
|
|
11
|
+
// Framework-specific detectors
|
|
12
|
+
export { detectNextjsIssues } from './nextjs.js';
|
|
13
|
+
export { detectReactNativeIssues } from './reactNative.js';
|
|
14
|
+
export { detectNodejsIssues } from './nodejs.js';
|
|
15
|
+
export { detectJavascriptIssues } from './javascript.js';
|
|
16
|
+
export { detectTypescriptIssues } from './typescript.js';
|
|
@@ -0,0 +1,169 @@
|
|
|
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 vanilla JavaScript code smells:
|
|
7
|
+
* - var usage (should use let/const)
|
|
8
|
+
* - Loose equality (== instead of ===)
|
|
9
|
+
* - Implicit type coercion
|
|
10
|
+
* - Global variable pollution
|
|
11
|
+
*/
|
|
12
|
+
export function detectJavascriptIssues(
|
|
13
|
+
component: ParsedComponent,
|
|
14
|
+
filePath: string,
|
|
15
|
+
sourceCode: string,
|
|
16
|
+
config: DetectorConfig = DEFAULT_CONFIG
|
|
17
|
+
): CodeSmell[] {
|
|
18
|
+
if (!config.checkJavascript) return [];
|
|
19
|
+
|
|
20
|
+
const smells: CodeSmell[] = [];
|
|
21
|
+
|
|
22
|
+
// Detect var usage (should use let/const)
|
|
23
|
+
component.path.traverse({
|
|
24
|
+
VariableDeclaration(path) {
|
|
25
|
+
if (path.node.kind === 'var') {
|
|
26
|
+
const loc = path.node.loc;
|
|
27
|
+
smells.push({
|
|
28
|
+
type: 'js-var-usage',
|
|
29
|
+
severity: 'warning',
|
|
30
|
+
message: `Using "var" instead of "let" or "const" in "${component.name}"`,
|
|
31
|
+
file: filePath,
|
|
32
|
+
line: loc?.start.line || 0,
|
|
33
|
+
column: loc?.start.column || 0,
|
|
34
|
+
suggestion: 'Use "const" for values that never change, "let" for reassignable variables',
|
|
35
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Detect loose equality (== and != instead of === and !==)
|
|
42
|
+
component.path.traverse({
|
|
43
|
+
BinaryExpression(path) {
|
|
44
|
+
const { operator, left, right } = path.node;
|
|
45
|
+
|
|
46
|
+
// Skip if comparing with null/undefined (sometimes intentional)
|
|
47
|
+
const isNullCheck =
|
|
48
|
+
(t.isNullLiteral(left) || t.isNullLiteral(right)) ||
|
|
49
|
+
(t.isIdentifier(left) && left.name === 'undefined') ||
|
|
50
|
+
(t.isIdentifier(right) && right.name === 'undefined');
|
|
51
|
+
|
|
52
|
+
if (operator === '==' && !isNullCheck) {
|
|
53
|
+
const loc = path.node.loc;
|
|
54
|
+
smells.push({
|
|
55
|
+
type: 'js-loose-equality',
|
|
56
|
+
severity: 'warning',
|
|
57
|
+
message: `Using loose equality "==" in "${component.name}"`,
|
|
58
|
+
file: filePath,
|
|
59
|
+
line: loc?.start.line || 0,
|
|
60
|
+
column: loc?.start.column || 0,
|
|
61
|
+
suggestion: 'Use strict equality "===" to avoid type coercion bugs',
|
|
62
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (operator === '!=' && !isNullCheck) {
|
|
67
|
+
const loc = path.node.loc;
|
|
68
|
+
smells.push({
|
|
69
|
+
type: 'js-loose-equality',
|
|
70
|
+
severity: 'warning',
|
|
71
|
+
message: `Using loose inequality "!=" in "${component.name}"`,
|
|
72
|
+
file: filePath,
|
|
73
|
+
line: loc?.start.line || 0,
|
|
74
|
+
column: loc?.start.column || 0,
|
|
75
|
+
suggestion: 'Use strict inequality "!==" to avoid type coercion bugs',
|
|
76
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Detect implicit type coercion patterns
|
|
83
|
+
component.path.traverse({
|
|
84
|
+
// +string to convert to number
|
|
85
|
+
UnaryExpression(path) {
|
|
86
|
+
if (path.node.operator === '+' && t.isIdentifier(path.node.argument)) {
|
|
87
|
+
const loc = path.node.loc;
|
|
88
|
+
smells.push({
|
|
89
|
+
type: 'js-implicit-coercion',
|
|
90
|
+
severity: 'info',
|
|
91
|
+
message: `Implicit number coercion with unary + in "${component.name}"`,
|
|
92
|
+
file: filePath,
|
|
93
|
+
line: loc?.start.line || 0,
|
|
94
|
+
column: loc?.start.column || 0,
|
|
95
|
+
suggestion: 'Use explicit conversion: Number(value) or parseInt(value, 10)',
|
|
96
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// !!value for boolean coercion - detected via UnaryExpression
|
|
102
|
+
// LogicalExpression handles &&, ||, ?? - skip for !! detection
|
|
103
|
+
|
|
104
|
+
// String concatenation with + that mixes types
|
|
105
|
+
BinaryExpression(path) {
|
|
106
|
+
if (path.node.operator === '+') {
|
|
107
|
+
const { left, right } = path.node;
|
|
108
|
+
const leftIsString = t.isStringLiteral(left) || t.isTemplateLiteral(left);
|
|
109
|
+
const rightIsString = t.isStringLiteral(right) || t.isTemplateLiteral(right);
|
|
110
|
+
const leftIsNumber = t.isNumericLiteral(left);
|
|
111
|
+
const rightIsNumber = t.isNumericLiteral(right);
|
|
112
|
+
|
|
113
|
+
// Mixed string + number concatenation
|
|
114
|
+
if ((leftIsString && rightIsNumber) || (leftIsNumber && rightIsString)) {
|
|
115
|
+
const loc = path.node.loc;
|
|
116
|
+
smells.push({
|
|
117
|
+
type: 'js-implicit-coercion',
|
|
118
|
+
severity: 'info',
|
|
119
|
+
message: `Implicit string coercion in "${component.name}"`,
|
|
120
|
+
file: filePath,
|
|
121
|
+
line: loc?.start.line || 0,
|
|
122
|
+
column: loc?.start.column || 0,
|
|
123
|
+
suggestion: 'Use template literal for clarity: `${value}` or String(value)',
|
|
124
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Detect potential global pollution (assignments without declaration)
|
|
132
|
+
component.path.traverse({
|
|
133
|
+
AssignmentExpression(path) {
|
|
134
|
+
const { left } = path.node;
|
|
135
|
+
|
|
136
|
+
if (t.isIdentifier(left)) {
|
|
137
|
+
// Check if this identifier is declared in scope
|
|
138
|
+
const binding = path.scope.getBinding(left.name);
|
|
139
|
+
|
|
140
|
+
if (!binding) {
|
|
141
|
+
// Check if it's a well-known global
|
|
142
|
+
const knownGlobals = [
|
|
143
|
+
'window', 'document', 'console', 'localStorage', 'sessionStorage',
|
|
144
|
+
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
145
|
+
'fetch', 'XMLHttpRequest', 'WebSocket', 'module', 'exports',
|
|
146
|
+
'require', 'process', 'global', '__dirname', '__filename',
|
|
147
|
+
'Buffer', 'Promise', 'Map', 'Set', 'Symbol',
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
if (!knownGlobals.includes(left.name)) {
|
|
151
|
+
const loc = path.node.loc;
|
|
152
|
+
smells.push({
|
|
153
|
+
type: 'js-global-pollution',
|
|
154
|
+
severity: 'error',
|
|
155
|
+
message: `Implicit global variable "${left.name}" in "${component.name}"`,
|
|
156
|
+
file: filePath,
|
|
157
|
+
line: loc?.start.line || 0,
|
|
158
|
+
column: loc?.start.column || 0,
|
|
159
|
+
suggestion: `Declare the variable: const ${left.name} = ... or let ${left.name} = ...`,
|
|
160
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return smells;
|
|
169
|
+
}
|