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
|
@@ -1,177 +0,0 @@
|
|
|
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
|
-
export function detectUnmemoizedCalculations(
|
|
7
|
-
component: ParsedComponent,
|
|
8
|
-
filePath: string,
|
|
9
|
-
sourceCode: string,
|
|
10
|
-
config: DetectorConfig = DEFAULT_CONFIG
|
|
11
|
-
): CodeSmell[] {
|
|
12
|
-
if (!config.checkMemoization) return [];
|
|
13
|
-
|
|
14
|
-
const smells: CodeSmell[] = [];
|
|
15
|
-
|
|
16
|
-
// Look for expensive operations not wrapped in useMemo
|
|
17
|
-
component.path.traverse({
|
|
18
|
-
VariableDeclarator(varPath) {
|
|
19
|
-
const init = varPath.node.init;
|
|
20
|
-
if (!init) return;
|
|
21
|
-
|
|
22
|
-
const loc = init.loc;
|
|
23
|
-
if (!loc) return;
|
|
24
|
-
|
|
25
|
-
// Check if this is inside useMemo/useCallback
|
|
26
|
-
let isInsideHook = false;
|
|
27
|
-
let currentPath: NodePath | null = varPath.parentPath;
|
|
28
|
-
while (currentPath) {
|
|
29
|
-
if (
|
|
30
|
-
currentPath.isCallExpression() &&
|
|
31
|
-
t.isIdentifier(currentPath.node.callee) &&
|
|
32
|
-
['useMemo', 'useCallback'].includes(currentPath.node.callee.name)
|
|
33
|
-
) {
|
|
34
|
-
isInsideHook = true;
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
currentPath = currentPath.parentPath;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (isInsideHook) return;
|
|
41
|
-
|
|
42
|
-
// Detect expensive operations
|
|
43
|
-
let isExpensive = false;
|
|
44
|
-
let reason = '';
|
|
45
|
-
|
|
46
|
-
// Check for .map(), .filter(), .reduce(), .sort() on arrays
|
|
47
|
-
if (t.isCallExpression(init) && t.isMemberExpression(init.callee)) {
|
|
48
|
-
const prop = init.callee.property;
|
|
49
|
-
if (t.isIdentifier(prop)) {
|
|
50
|
-
const expensiveMethods = ['map', 'filter', 'reduce', 'sort', 'find', 'findIndex', 'flatMap'];
|
|
51
|
-
if (expensiveMethods.includes(prop.name)) {
|
|
52
|
-
isExpensive = true;
|
|
53
|
-
reason = `.${prop.name}() creates a new array on every render`;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Check for object/array literals with complex computations
|
|
59
|
-
if (t.isArrayExpression(init) && init.elements.length > 5) {
|
|
60
|
-
isExpensive = true;
|
|
61
|
-
reason = 'Large array literal recreated on every render';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (t.isObjectExpression(init) && init.properties.length > 5) {
|
|
65
|
-
isExpensive = true;
|
|
66
|
-
reason = 'Large object literal recreated on every render';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check for JSON operations
|
|
70
|
-
if (t.isCallExpression(init) && t.isMemberExpression(init.callee)) {
|
|
71
|
-
const obj = init.callee.object;
|
|
72
|
-
const prop = init.callee.property;
|
|
73
|
-
if (
|
|
74
|
-
t.isIdentifier(obj) &&
|
|
75
|
-
obj.name === 'JSON' &&
|
|
76
|
-
t.isIdentifier(prop) &&
|
|
77
|
-
['parse', 'stringify'].includes(prop.name)
|
|
78
|
-
) {
|
|
79
|
-
isExpensive = true;
|
|
80
|
-
reason = `JSON.${prop.name}() is expensive and runs on every render`;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Check for spread operations creating new objects
|
|
85
|
-
if (t.isObjectExpression(init)) {
|
|
86
|
-
const hasSpread = init.properties.some(p => t.isSpreadElement(p));
|
|
87
|
-
if (hasSpread && init.properties.length > 3) {
|
|
88
|
-
isExpensive = true;
|
|
89
|
-
reason = 'Spread operation creates new object reference on every render';
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (isExpensive) {
|
|
94
|
-
const id = varPath.node.id;
|
|
95
|
-
const varName = t.isIdentifier(id) ? id.name : 'variable';
|
|
96
|
-
|
|
97
|
-
smells.push({
|
|
98
|
-
type: 'unmemoized-calculation',
|
|
99
|
-
severity: 'warning',
|
|
100
|
-
message: `Unmemoized calculation "${varName}" in "${component.name}": ${reason}`,
|
|
101
|
-
file: filePath,
|
|
102
|
-
line: loc.start.line,
|
|
103
|
-
column: loc.start.column,
|
|
104
|
-
suggestion: `Wrap in useMemo: const ${varName} = useMemo(() => ..., [dependencies])`,
|
|
105
|
-
codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Check for inline function props (common performance issue)
|
|
112
|
-
component.path.traverse({
|
|
113
|
-
JSXAttribute(attrPath) {
|
|
114
|
-
const value = attrPath.node.value;
|
|
115
|
-
if (!t.isJSXExpressionContainer(value)) return;
|
|
116
|
-
|
|
117
|
-
const expr = value.expression;
|
|
118
|
-
const loc = expr.loc;
|
|
119
|
-
if (!loc) return;
|
|
120
|
-
|
|
121
|
-
// Check for inline arrow functions
|
|
122
|
-
if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
|
|
123
|
-
const attrName = t.isJSXIdentifier(attrPath.node.name)
|
|
124
|
-
? attrPath.node.name.name
|
|
125
|
-
: 'unknown';
|
|
126
|
-
|
|
127
|
-
// Only warn for non-trivial handlers
|
|
128
|
-
if (attrName.startsWith('on') && t.isArrowFunctionExpression(expr)) {
|
|
129
|
-
const body = expr.body;
|
|
130
|
-
// Skip simple one-liner callbacks that just call a function
|
|
131
|
-
if (t.isCallExpression(body)) {
|
|
132
|
-
// These are usually fine: onClick={() => doSomething()}
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (t.isBlockStatement(body) && body.body.length > 1) {
|
|
137
|
-
smells.push({
|
|
138
|
-
type: 'inline-function-prop',
|
|
139
|
-
severity: 'info',
|
|
140
|
-
message: `Inline function for "${attrName}" creates new reference on every render`,
|
|
141
|
-
file: filePath,
|
|
142
|
-
line: loc.start.line,
|
|
143
|
-
column: loc.start.column,
|
|
144
|
-
suggestion: 'Extract to useCallback or define outside render',
|
|
145
|
-
codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Check for inline objects/arrays passed as props
|
|
152
|
-
if (t.isObjectExpression(expr) || t.isArrayExpression(expr)) {
|
|
153
|
-
const attrName = t.isJSXIdentifier(attrPath.node.name)
|
|
154
|
-
? attrPath.node.name.name
|
|
155
|
-
: 'unknown';
|
|
156
|
-
|
|
157
|
-
// style prop is a common pattern, only warn for complex ones
|
|
158
|
-
if (attrName === 'style' && t.isObjectExpression(expr) && expr.properties.length <= 3) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
smells.push({
|
|
163
|
-
type: 'unmemoized-calculation',
|
|
164
|
-
severity: 'info',
|
|
165
|
-
message: `Inline ${t.isObjectExpression(expr) ? 'object' : 'array'} for "${attrName}" prop creates new reference on every render`,
|
|
166
|
-
file: filePath,
|
|
167
|
-
line: loc.start.line,
|
|
168
|
-
column: loc.start.column,
|
|
169
|
-
suggestion: 'Extract to a constant or useMemo',
|
|
170
|
-
codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return smells;
|
|
177
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import * as t from '@babel/types';
|
|
2
|
-
import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
|
|
3
|
-
import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Detects .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
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
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/detectors/nextjs.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import * as t from '@babel/types';
|
|
2
|
-
import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
|
|
3
|
-
import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Detects Next.js-specific code smells:
|
|
7
|
-
* - Missing 'use client' / 'use server' directives
|
|
8
|
-
* - Unoptimized images (using <img> instead of next/image)
|
|
9
|
-
* - Router misuse patterns
|
|
10
|
-
* - Missing metadata exports
|
|
11
|
-
*/
|
|
12
|
-
export function detectNextjsIssues(
|
|
13
|
-
component: ParsedComponent,
|
|
14
|
-
filePath: string,
|
|
15
|
-
sourceCode: string,
|
|
16
|
-
config: DetectorConfig = DEFAULT_CONFIG,
|
|
17
|
-
imports: string[] = []
|
|
18
|
-
): CodeSmell[] {
|
|
19
|
-
if (!config.checkNextjs) return [];
|
|
20
|
-
|
|
21
|
-
// Only run on Next.js projects (check for next imports)
|
|
22
|
-
const isNextProject = imports.some(imp =>
|
|
23
|
-
imp.includes('next/') || imp.includes('next')
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
// Also check file path patterns
|
|
27
|
-
const isAppRouter = filePath.includes('/app/') &&
|
|
28
|
-
(filePath.endsWith('page.tsx') || filePath.endsWith('page.jsx') ||
|
|
29
|
-
filePath.endsWith('layout.tsx') || filePath.endsWith('layout.jsx'));
|
|
30
|
-
|
|
31
|
-
const smells: CodeSmell[] = [];
|
|
32
|
-
|
|
33
|
-
// Check for unoptimized images (using <img> instead of next/image)
|
|
34
|
-
component.path.traverse({
|
|
35
|
-
JSXOpeningElement(path) {
|
|
36
|
-
if (t.isJSXIdentifier(path.node.name) && path.node.name.name === 'img') {
|
|
37
|
-
const loc = path.node.loc;
|
|
38
|
-
smells.push({
|
|
39
|
-
type: 'nextjs-image-unoptimized',
|
|
40
|
-
severity: 'warning',
|
|
41
|
-
message: `Using native <img> instead of next/image in "${component.name}"`,
|
|
42
|
-
file: filePath,
|
|
43
|
-
line: loc?.start.line || 0,
|
|
44
|
-
column: loc?.start.column || 0,
|
|
45
|
-
suggestion: 'Use next/image for automatic image optimization: import Image from "next/image"',
|
|
46
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Check for client-side hooks in server components (App Router)
|
|
53
|
-
if (isAppRouter && !sourceCode.includes("'use client'") && !sourceCode.includes('"use client"')) {
|
|
54
|
-
const clientHooks = ['useState', 'useEffect', 'useContext', 'useReducer', 'useRef'];
|
|
55
|
-
const usedClientHooks: string[] = [];
|
|
56
|
-
|
|
57
|
-
component.path.traverse({
|
|
58
|
-
CallExpression(path) {
|
|
59
|
-
if (t.isIdentifier(path.node.callee) && clientHooks.includes(path.node.callee.name)) {
|
|
60
|
-
usedClientHooks.push(path.node.callee.name);
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (usedClientHooks.length > 0) {
|
|
66
|
-
const loc = component.node.loc;
|
|
67
|
-
smells.push({
|
|
68
|
-
type: 'nextjs-client-server-boundary',
|
|
69
|
-
severity: 'error',
|
|
70
|
-
message: `Client hooks (${usedClientHooks.join(', ')}) used without 'use client' directive in "${component.name}"`,
|
|
71
|
-
file: filePath,
|
|
72
|
-
line: loc?.start.line || 1,
|
|
73
|
-
column: 0,
|
|
74
|
-
suggestion: "Add 'use client' at the top of the file, or move client logic to a separate component",
|
|
75
|
-
codeSnippet: getCodeSnippet(sourceCode, 1),
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Check for missing metadata in page/layout files
|
|
81
|
-
if (isAppRouter && filePath.includes('page.')) {
|
|
82
|
-
// This would require checking exports, which needs file-level analysis
|
|
83
|
-
const hasMetadata = sourceCode.includes('export const metadata') ||
|
|
84
|
-
sourceCode.includes('export function generateMetadata');
|
|
85
|
-
|
|
86
|
-
if (!hasMetadata && component.name === 'default') {
|
|
87
|
-
smells.push({
|
|
88
|
-
type: 'nextjs-missing-metadata',
|
|
89
|
-
severity: 'info',
|
|
90
|
-
message: 'Page component missing metadata export',
|
|
91
|
-
file: filePath,
|
|
92
|
-
line: 1,
|
|
93
|
-
column: 0,
|
|
94
|
-
suggestion: 'Add metadata for SEO: export const metadata = { title: "...", description: "..." }',
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Check for router misuse (using window.location instead of next/router)
|
|
100
|
-
component.path.traverse({
|
|
101
|
-
MemberExpression(path) {
|
|
102
|
-
if (
|
|
103
|
-
t.isIdentifier(path.node.object) &&
|
|
104
|
-
path.node.object.name === 'window' &&
|
|
105
|
-
t.isIdentifier(path.node.property) &&
|
|
106
|
-
path.node.property.name === 'location'
|
|
107
|
-
) {
|
|
108
|
-
const loc = path.node.loc;
|
|
109
|
-
smells.push({
|
|
110
|
-
type: 'nextjs-router-misuse',
|
|
111
|
-
severity: 'warning',
|
|
112
|
-
message: `Using window.location instead of Next.js router in "${component.name}"`,
|
|
113
|
-
file: filePath,
|
|
114
|
-
line: loc?.start.line || 0,
|
|
115
|
-
column: loc?.start.column || 0,
|
|
116
|
-
suggestion: 'Use next/navigation: import { useRouter } from "next/navigation"',
|
|
117
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return smells;
|
|
124
|
-
}
|
package/src/detectors/nodejs.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
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 Node.js-specific code smells:
|
|
8
|
-
* - Callback hell (deeply nested callbacks)
|
|
9
|
-
* - Unhandled promise rejections
|
|
10
|
-
* - Synchronous I/O operations
|
|
11
|
-
* - Missing error handling
|
|
12
|
-
*/
|
|
13
|
-
export function detectNodejsIssues(
|
|
14
|
-
component: ParsedComponent,
|
|
15
|
-
filePath: string,
|
|
16
|
-
sourceCode: string,
|
|
17
|
-
config: DetectorConfig = DEFAULT_CONFIG,
|
|
18
|
-
imports: string[] = []
|
|
19
|
-
): CodeSmell[] {
|
|
20
|
-
if (!config.checkNodejs) return [];
|
|
21
|
-
|
|
22
|
-
// Check if this looks like a Node.js file
|
|
23
|
-
const isNodeFile = imports.some(imp =>
|
|
24
|
-
imp.includes('fs') || imp.includes('path') || imp.includes('http') ||
|
|
25
|
-
imp.includes('express') || imp.includes('child_process') ||
|
|
26
|
-
imp.includes('crypto') || imp.includes('os') || imp.includes('stream')
|
|
27
|
-
) || filePath.includes('.server.') || filePath.includes('/api/');
|
|
28
|
-
|
|
29
|
-
if (!isNodeFile) return [];
|
|
30
|
-
|
|
31
|
-
const smells: CodeSmell[] = [];
|
|
32
|
-
|
|
33
|
-
// Detect callback hell (nested callbacks > maxCallbackDepth)
|
|
34
|
-
component.path.traverse({
|
|
35
|
-
CallExpression(path) {
|
|
36
|
-
const depth = getCallbackDepth(path);
|
|
37
|
-
|
|
38
|
-
if (depth > config.maxCallbackDepth) {
|
|
39
|
-
const loc = path.node.loc;
|
|
40
|
-
smells.push({
|
|
41
|
-
type: 'nodejs-callback-hell',
|
|
42
|
-
severity: 'warning',
|
|
43
|
-
message: `Callback hell detected (depth: ${depth}) in "${component.name}"`,
|
|
44
|
-
file: filePath,
|
|
45
|
-
line: loc?.start.line || 0,
|
|
46
|
-
column: loc?.start.column || 0,
|
|
47
|
-
suggestion: 'Refactor to async/await or use Promise.all() for parallel operations',
|
|
48
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Detect unhandled promise rejections (Promise without .catch or try/catch)
|
|
55
|
-
component.path.traverse({
|
|
56
|
-
CallExpression(path) {
|
|
57
|
-
// Check for .then() without .catch()
|
|
58
|
-
if (t.isMemberExpression(path.node.callee) &&
|
|
59
|
-
t.isIdentifier(path.node.callee.property) &&
|
|
60
|
-
path.node.callee.property.name === 'then') {
|
|
61
|
-
|
|
62
|
-
// Check if followed by .catch() in chain
|
|
63
|
-
const parent = path.parent;
|
|
64
|
-
let hasCatch = false;
|
|
65
|
-
|
|
66
|
-
if (t.isMemberExpression(parent)) {
|
|
67
|
-
const prop = (parent as t.MemberExpression).property;
|
|
68
|
-
if (t.isIdentifier(prop) && prop.name === 'catch') {
|
|
69
|
-
hasCatch = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Check if inside try block
|
|
74
|
-
let current: NodePath | null = path;
|
|
75
|
-
while (current) {
|
|
76
|
-
if (t.isTryStatement(current.node)) {
|
|
77
|
-
hasCatch = true;
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
current = current.parentPath;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!hasCatch) {
|
|
84
|
-
const loc = path.node.loc;
|
|
85
|
-
smells.push({
|
|
86
|
-
type: 'nodejs-unhandled-promise',
|
|
87
|
-
severity: 'warning',
|
|
88
|
-
message: `.then() without .catch() in "${component.name}"`,
|
|
89
|
-
file: filePath,
|
|
90
|
-
line: loc?.start.line || 0,
|
|
91
|
-
column: loc?.start.column || 0,
|
|
92
|
-
suggestion: 'Add .catch() to handle rejections, or use try/catch with async/await',
|
|
93
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Detect synchronous file operations
|
|
101
|
-
const syncMethods = ['readFileSync', 'writeFileSync', 'appendFileSync', 'readdirSync',
|
|
102
|
-
'statSync', 'mkdirSync', 'rmdirSync', 'unlinkSync', 'existsSync'];
|
|
103
|
-
|
|
104
|
-
component.path.traverse({
|
|
105
|
-
CallExpression(path) {
|
|
106
|
-
const callee = path.node.callee;
|
|
107
|
-
|
|
108
|
-
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
109
|
-
if (syncMethods.includes(callee.property.name)) {
|
|
110
|
-
const loc = path.node.loc;
|
|
111
|
-
smells.push({
|
|
112
|
-
type: 'nodejs-sync-io',
|
|
113
|
-
severity: 'warning',
|
|
114
|
-
message: `Synchronous file operation "${callee.property.name}" blocks event loop`,
|
|
115
|
-
file: filePath,
|
|
116
|
-
line: loc?.start.line || 0,
|
|
117
|
-
column: loc?.start.column || 0,
|
|
118
|
-
suggestion: `Use async version: ${callee.property.name.replace('Sync', '')} with await or promises`,
|
|
119
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Direct function call
|
|
125
|
-
if (t.isIdentifier(callee) && syncMethods.includes(callee.name)) {
|
|
126
|
-
const loc = path.node.loc;
|
|
127
|
-
smells.push({
|
|
128
|
-
type: 'nodejs-sync-io',
|
|
129
|
-
severity: 'warning',
|
|
130
|
-
message: `Synchronous file operation "${callee.name}" blocks event loop`,
|
|
131
|
-
file: filePath,
|
|
132
|
-
line: loc?.start.line || 0,
|
|
133
|
-
column: loc?.start.column || 0,
|
|
134
|
-
suggestion: `Use async version: ${callee.name.replace('Sync', '')} with await or promises`,
|
|
135
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Detect missing error handling in async functions
|
|
142
|
-
component.path.traverse({
|
|
143
|
-
AwaitExpression(path) {
|
|
144
|
-
// Check if inside try block
|
|
145
|
-
let insideTry = false;
|
|
146
|
-
let current: NodePath | null = path;
|
|
147
|
-
|
|
148
|
-
while (current) {
|
|
149
|
-
if (t.isTryStatement(current.node)) {
|
|
150
|
-
insideTry = true;
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
// Stop at function boundary
|
|
154
|
-
if (t.isFunction(current.node)) break;
|
|
155
|
-
current = current.parentPath;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (!insideTry) {
|
|
159
|
-
// Check if the parent function has error handling at call site
|
|
160
|
-
// This is a simplified check - in practice you'd want more context
|
|
161
|
-
const loc = path.node.loc;
|
|
162
|
-
smells.push({
|
|
163
|
-
type: 'nodejs-missing-error-handling',
|
|
164
|
-
severity: 'info',
|
|
165
|
-
message: `await without try/catch may cause unhandled rejections`,
|
|
166
|
-
file: filePath,
|
|
167
|
-
line: loc?.start.line || 0,
|
|
168
|
-
column: loc?.start.column || 0,
|
|
169
|
-
suggestion: 'Wrap await in try/catch or handle errors at the call site',
|
|
170
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return smells;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Calculate the depth of nested callbacks
|
|
181
|
-
*/
|
|
182
|
-
function getCallbackDepth(path: NodePath): number {
|
|
183
|
-
let depth = 0;
|
|
184
|
-
let current: NodePath | null = path;
|
|
185
|
-
|
|
186
|
-
while (current) {
|
|
187
|
-
const node = current.node;
|
|
188
|
-
|
|
189
|
-
// Count function expressions that are arguments to calls
|
|
190
|
-
if ((t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) &&
|
|
191
|
-
t.isCallExpression(current.parent)) {
|
|
192
|
-
depth++;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
current = current.parentPath;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return depth;
|
|
199
|
-
}
|