react-code-smell-detector 1.0.1 → 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 +31 -1
- package/dist/detectors/index.d.ts +5 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +6 -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/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 +24 -0
- package/dist/types/index.d.ts +7 -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 +35 -0
- package/src/detectors/index.ts +6 -0
- package/src/detectors/javascript.ts +169 -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 +24 -0
- package/src/types/index.ts +40 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { getCodeSnippet } from '../parser/index.js';
|
|
3
|
+
import { DEFAULT_CONFIG } from '../types/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Detects React Native-specific code smells:
|
|
6
|
+
* - Inline styles instead of StyleSheet
|
|
7
|
+
* - Missing accessibility props
|
|
8
|
+
* - Performance anti-patterns
|
|
9
|
+
*/
|
|
10
|
+
export function detectReactNativeIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG, imports = []) {
|
|
11
|
+
if (!config.checkReactNative)
|
|
12
|
+
return [];
|
|
13
|
+
// Only run on React Native projects
|
|
14
|
+
const isRNProject = imports.some(imp => imp.includes('react-native') || imp.includes('@react-native'));
|
|
15
|
+
if (!isRNProject)
|
|
16
|
+
return [];
|
|
17
|
+
const smells = [];
|
|
18
|
+
const inlineStyleLines = new Set();
|
|
19
|
+
// Check for inline styles instead of StyleSheet
|
|
20
|
+
component.path.traverse({
|
|
21
|
+
JSXAttribute(path) {
|
|
22
|
+
if (!t.isJSXIdentifier(path.node.name))
|
|
23
|
+
return;
|
|
24
|
+
if (path.node.name.name !== 'style')
|
|
25
|
+
return;
|
|
26
|
+
const value = path.node.value;
|
|
27
|
+
if (!t.isJSXExpressionContainer(value))
|
|
28
|
+
return;
|
|
29
|
+
// Check if it's an inline object (not a StyleSheet reference)
|
|
30
|
+
if (t.isObjectExpression(value.expression)) {
|
|
31
|
+
const loc = path.node.loc;
|
|
32
|
+
const line = loc?.start.line || 0;
|
|
33
|
+
if (!inlineStyleLines.has(line)) {
|
|
34
|
+
inlineStyleLines.add(line);
|
|
35
|
+
smells.push({
|
|
36
|
+
type: 'rn-inline-style',
|
|
37
|
+
severity: 'warning',
|
|
38
|
+
message: `Inline style object in "${component.name}" - prefer StyleSheet.create()`,
|
|
39
|
+
file: filePath,
|
|
40
|
+
line,
|
|
41
|
+
column: loc?.start.column || 0,
|
|
42
|
+
suggestion: 'Use StyleSheet.create() for better performance: const styles = StyleSheet.create({ ... })',
|
|
43
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// Check for missing accessibility props on interactive elements
|
|
50
|
+
const interactiveComponents = ['TouchableOpacity', 'TouchableHighlight', 'Pressable', 'Button', 'TouchableWithoutFeedback'];
|
|
51
|
+
component.path.traverse({
|
|
52
|
+
JSXOpeningElement(path) {
|
|
53
|
+
if (!t.isJSXIdentifier(path.node.name))
|
|
54
|
+
return;
|
|
55
|
+
const componentName = path.node.name.name;
|
|
56
|
+
if (!interactiveComponents.includes(componentName))
|
|
57
|
+
return;
|
|
58
|
+
// Check for accessibility props
|
|
59
|
+
const hasAccessibilityLabel = path.node.attributes.some(attr => {
|
|
60
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
61
|
+
return ['accessibilityLabel', 'accessible', 'accessibilityRole', 'accessibilityHint'].includes(attr.name.name);
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
});
|
|
65
|
+
if (!hasAccessibilityLabel) {
|
|
66
|
+
const loc = path.node.loc;
|
|
67
|
+
smells.push({
|
|
68
|
+
type: 'rn-missing-accessibility',
|
|
69
|
+
severity: 'info',
|
|
70
|
+
message: `${componentName} missing accessibility props in "${component.name}"`,
|
|
71
|
+
file: filePath,
|
|
72
|
+
line: loc?.start.line || 0,
|
|
73
|
+
column: loc?.start.column || 0,
|
|
74
|
+
suggestion: 'Add accessibilityLabel and accessibilityRole for screen readers',
|
|
75
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
// Check for performance anti-patterns
|
|
81
|
+
component.path.traverse({
|
|
82
|
+
// Detect creating anonymous functions in render (for onPress, etc.)
|
|
83
|
+
JSXAttribute(path) {
|
|
84
|
+
if (!t.isJSXIdentifier(path.node.name))
|
|
85
|
+
return;
|
|
86
|
+
const propName = path.node.name.name;
|
|
87
|
+
if (!['onPress', 'onPressIn', 'onPressOut', 'onLongPress'].includes(propName))
|
|
88
|
+
return;
|
|
89
|
+
const value = path.node.value;
|
|
90
|
+
if (!t.isJSXExpressionContainer(value))
|
|
91
|
+
return;
|
|
92
|
+
// Check for arrow functions or function expressions
|
|
93
|
+
if (t.isArrowFunctionExpression(value.expression) || t.isFunctionExpression(value.expression)) {
|
|
94
|
+
const loc = path.node.loc;
|
|
95
|
+
smells.push({
|
|
96
|
+
type: 'rn-performance-issue',
|
|
97
|
+
severity: 'info',
|
|
98
|
+
message: `Inline function for ${propName} in "${component.name}" creates new reference each render`,
|
|
99
|
+
file: filePath,
|
|
100
|
+
line: loc?.start.line || 0,
|
|
101
|
+
column: loc?.start.column || 0,
|
|
102
|
+
suggestion: 'Extract to useCallback or class method to prevent unnecessary re-renders',
|
|
103
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
// Detect array spreads in render that could be memoized
|
|
108
|
+
SpreadElement(path) {
|
|
109
|
+
const loc = path.node.loc;
|
|
110
|
+
// Only flag if inside JSX or return statement
|
|
111
|
+
let inJSX = false;
|
|
112
|
+
let current = path.parentPath;
|
|
113
|
+
while (current) {
|
|
114
|
+
if (t.isJSXElement(current.node) || t.isJSXFragment(current.node)) {
|
|
115
|
+
inJSX = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
current = current.parentPath;
|
|
119
|
+
}
|
|
120
|
+
if (inJSX && t.isArrayExpression(path.parent)) {
|
|
121
|
+
smells.push({
|
|
122
|
+
type: 'rn-performance-issue',
|
|
123
|
+
severity: 'info',
|
|
124
|
+
message: `Array spread in render may cause performance issues in "${component.name}"`,
|
|
125
|
+
file: filePath,
|
|
126
|
+
line: loc?.start.line || 0,
|
|
127
|
+
column: loc?.start.column || 0,
|
|
128
|
+
suggestion: 'Consider memoizing with useMemo if this array is passed as a prop',
|
|
129
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return smells;
|
|
135
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects TypeScript-specific code smells:
|
|
5
|
+
* - Overuse of 'any' type
|
|
6
|
+
* - Missing return type on functions
|
|
7
|
+
* - Non-null assertion operator (!)
|
|
8
|
+
* - Type assertions (as) that could be avoided
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectTypescriptIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
|
|
11
|
+
//# sourceMappingURL=typescript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typescript.d.ts","sourceRoot":"","sources":["../../src/detectors/typescript.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAsIb"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { getCodeSnippet } from '../parser/index.js';
|
|
3
|
+
import { DEFAULT_CONFIG } from '../types/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Detects TypeScript-specific code smells:
|
|
6
|
+
* - Overuse of 'any' type
|
|
7
|
+
* - Missing return type on functions
|
|
8
|
+
* - Non-null assertion operator (!)
|
|
9
|
+
* - Type assertions (as) that could be avoided
|
|
10
|
+
*/
|
|
11
|
+
export function detectTypescriptIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
|
|
12
|
+
if (!config.checkTypescript)
|
|
13
|
+
return [];
|
|
14
|
+
// Only run on TypeScript files
|
|
15
|
+
if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx'))
|
|
16
|
+
return [];
|
|
17
|
+
const smells = [];
|
|
18
|
+
// Detect 'any' type usage
|
|
19
|
+
component.path.traverse({
|
|
20
|
+
TSAnyKeyword(path) {
|
|
21
|
+
const loc = path.node.loc;
|
|
22
|
+
smells.push({
|
|
23
|
+
type: 'ts-any-usage',
|
|
24
|
+
severity: 'warning',
|
|
25
|
+
message: `Using "any" type in "${component.name}"`,
|
|
26
|
+
file: filePath,
|
|
27
|
+
line: loc?.start.line || 0,
|
|
28
|
+
column: loc?.start.column || 0,
|
|
29
|
+
suggestion: 'Use a specific type, "unknown", or create an interface',
|
|
30
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
// Detect functions without return type (only in complex functions)
|
|
35
|
+
component.path.traverse({
|
|
36
|
+
FunctionDeclaration(path) {
|
|
37
|
+
// Skip if function has explicit return type
|
|
38
|
+
if (path.node.returnType)
|
|
39
|
+
return;
|
|
40
|
+
// Check if function body has return statements
|
|
41
|
+
let hasReturn = false;
|
|
42
|
+
path.traverse({
|
|
43
|
+
ReturnStatement(returnPath) {
|
|
44
|
+
if (returnPath.node.argument) {
|
|
45
|
+
hasReturn = true;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
if (hasReturn) {
|
|
50
|
+
const loc = path.node.loc;
|
|
51
|
+
smells.push({
|
|
52
|
+
type: 'ts-missing-return-type',
|
|
53
|
+
severity: 'info',
|
|
54
|
+
message: `Function "${path.node.id?.name || 'anonymous'}" missing return type`,
|
|
55
|
+
file: filePath,
|
|
56
|
+
line: loc?.start.line || 0,
|
|
57
|
+
column: loc?.start.column || 0,
|
|
58
|
+
suggestion: 'Add explicit return type: function name(): ReturnType { ... }',
|
|
59
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
ArrowFunctionExpression(path) {
|
|
64
|
+
// Only check arrow functions assigned to variables with 5+ lines
|
|
65
|
+
if (!path.node.returnType) {
|
|
66
|
+
const body = path.node.body;
|
|
67
|
+
// Skip simple arrow functions (single expression)
|
|
68
|
+
if (!t.isBlockStatement(body))
|
|
69
|
+
return;
|
|
70
|
+
// Check complexity - only flag if function is substantial
|
|
71
|
+
const startLine = path.node.loc?.start.line || 0;
|
|
72
|
+
const endLine = path.node.loc?.end.line || 0;
|
|
73
|
+
if (endLine - startLine >= 5) {
|
|
74
|
+
// Check if parent is variable declarator (assigned to variable)
|
|
75
|
+
const parent = path.parent;
|
|
76
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
77
|
+
const loc = path.node.loc;
|
|
78
|
+
smells.push({
|
|
79
|
+
type: 'ts-missing-return-type',
|
|
80
|
+
severity: 'info',
|
|
81
|
+
message: `Arrow function "${parent.id.name}" missing return type`,
|
|
82
|
+
file: filePath,
|
|
83
|
+
line: loc?.start.line || 0,
|
|
84
|
+
column: loc?.start.column || 0,
|
|
85
|
+
suggestion: 'Add return type: const name = (): ReturnType => { ... }',
|
|
86
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
// Detect non-null assertion operator (!)
|
|
94
|
+
component.path.traverse({
|
|
95
|
+
TSNonNullExpression(path) {
|
|
96
|
+
const loc = path.node.loc;
|
|
97
|
+
smells.push({
|
|
98
|
+
type: 'ts-non-null-assertion',
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
message: `Non-null assertion (!) bypasses type safety in "${component.name}"`,
|
|
101
|
+
file: filePath,
|
|
102
|
+
line: loc?.start.line || 0,
|
|
103
|
+
column: loc?.start.column || 0,
|
|
104
|
+
suggestion: 'Use optional chaining (?.) or proper null checks instead',
|
|
105
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
// Detect type assertions (as keyword) - these can hide type errors
|
|
110
|
+
component.path.traverse({
|
|
111
|
+
TSAsExpression(path) {
|
|
112
|
+
// Skip assertions to 'const' (used for const assertions)
|
|
113
|
+
if (t.isTSTypeReference(path.node.typeAnnotation)) {
|
|
114
|
+
const typeName = path.node.typeAnnotation.typeName;
|
|
115
|
+
if (t.isIdentifier(typeName) && typeName.name === 'const')
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Skip double assertions (as unknown as Type) - already flagged by TypeScript
|
|
119
|
+
if (t.isTSAsExpression(path.node.expression))
|
|
120
|
+
return;
|
|
121
|
+
const loc = path.node.loc;
|
|
122
|
+
smells.push({
|
|
123
|
+
type: 'ts-type-assertion',
|
|
124
|
+
severity: 'info',
|
|
125
|
+
message: `Type assertion (as) bypasses type checking in "${component.name}"`,
|
|
126
|
+
file: filePath,
|
|
127
|
+
line: loc?.start.line || 0,
|
|
128
|
+
column: loc?.start.column || 0,
|
|
129
|
+
suggestion: 'Consider using type guards or proper typing instead of assertions',
|
|
130
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return smells;
|
|
135
|
+
}
|
package/dist/reporter.js
CHANGED
|
@@ -218,6 +218,30 @@ function formatSmellType(type) {
|
|
|
218
218
|
'nested-ternary': '❓ Nested Ternary',
|
|
219
219
|
'dead-code': '💀 Dead Code',
|
|
220
220
|
'magic-value': '🔢 Magic Value',
|
|
221
|
+
// Next.js
|
|
222
|
+
'nextjs-client-server-boundary': '▲ Next.js Client/Server Boundary',
|
|
223
|
+
'nextjs-missing-metadata': '▲ Next.js Missing Metadata',
|
|
224
|
+
'nextjs-image-unoptimized': '▲ Next.js Unoptimized Image',
|
|
225
|
+
'nextjs-router-misuse': '▲ Next.js Router Misuse',
|
|
226
|
+
// React Native
|
|
227
|
+
'rn-inline-style': '📱 RN Inline Style',
|
|
228
|
+
'rn-missing-accessibility': '📱 RN Missing Accessibility',
|
|
229
|
+
'rn-performance-issue': '📱 RN Performance Issue',
|
|
230
|
+
// Node.js
|
|
231
|
+
'nodejs-callback-hell': '🟢 Node.js Callback Hell',
|
|
232
|
+
'nodejs-unhandled-promise': '🟢 Node.js Unhandled Promise',
|
|
233
|
+
'nodejs-sync-io': '🟢 Node.js Sync I/O',
|
|
234
|
+
'nodejs-missing-error-handling': '🟢 Node.js Missing Error Handling',
|
|
235
|
+
// JavaScript
|
|
236
|
+
'js-var-usage': '📜 JS var Usage',
|
|
237
|
+
'js-loose-equality': '📜 JS Loose Equality',
|
|
238
|
+
'js-implicit-coercion': '📜 JS Implicit Coercion',
|
|
239
|
+
'js-global-pollution': '📜 JS Global Pollution',
|
|
240
|
+
// TypeScript
|
|
241
|
+
'ts-any-usage': '🔷 TS any Usage',
|
|
242
|
+
'ts-missing-return-type': '🔷 TS Missing Return Type',
|
|
243
|
+
'ts-non-null-assertion': '🔷 TS Non-null Assertion',
|
|
244
|
+
'ts-type-assertion': '🔷 TS Type Assertion',
|
|
221
245
|
};
|
|
222
246
|
return labels[type] || type;
|
|
223
247
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type SmellSeverity = 'error' | 'warning' | 'info';
|
|
2
|
-
export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting' | 'missing-key' | 'hooks-rules-violation' | 'dependency-array-issue' | 'nested-ternary' | 'dead-code' | 'magic-value';
|
|
2
|
+
export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting' | 'missing-key' | 'hooks-rules-violation' | 'dependency-array-issue' | 'nested-ternary' | 'dead-code' | 'magic-value' | 'nextjs-client-server-boundary' | 'nextjs-missing-metadata' | 'nextjs-image-unoptimized' | 'nextjs-router-misuse' | 'rn-inline-style' | 'rn-missing-accessibility' | 'rn-performance-issue' | 'nodejs-callback-hell' | 'nodejs-unhandled-promise' | 'nodejs-sync-io' | 'nodejs-missing-error-handling' | 'js-var-usage' | 'js-loose-equality' | 'js-implicit-coercion' | 'js-global-pollution' | 'ts-any-usage' | 'ts-missing-return-type' | 'ts-non-null-assertion' | 'ts-type-assertion';
|
|
3
3
|
export interface CodeSmell {
|
|
4
4
|
type: SmellType;
|
|
5
5
|
severity: SmellSeverity;
|
|
@@ -66,6 +66,12 @@ export interface DetectorConfig {
|
|
|
66
66
|
checkDeadCode: boolean;
|
|
67
67
|
checkMagicValues: boolean;
|
|
68
68
|
magicNumberThreshold: number;
|
|
69
|
+
checkNextjs: boolean;
|
|
70
|
+
checkReactNative: boolean;
|
|
71
|
+
checkNodejs: boolean;
|
|
72
|
+
checkJavascript: boolean;
|
|
73
|
+
checkTypescript: boolean;
|
|
74
|
+
maxCallbackDepth: number;
|
|
69
75
|
}
|
|
70
76
|
export declare const DEFAULT_CONFIG: DetectorConfig;
|
|
71
77
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,MAAM,MAAM,SAAS,GACjB,mBAAmB,GACnB,eAAe,GACf,iBAAiB,GACjB,wBAAwB,GACxB,oBAAoB,GACpB,eAAe,GACf,sBAAsB,GACtB,cAAc,GACd,aAAa,GACb,uBAAuB,GACvB,wBAAwB,GACxB,gBAAgB,GAChB,WAAW,GACX,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types/index.js
CHANGED
|
@@ -11,4 +11,11 @@ export const DEFAULT_CONFIG = {
|
|
|
11
11
|
checkDeadCode: true,
|
|
12
12
|
checkMagicValues: true,
|
|
13
13
|
magicNumberThreshold: 10,
|
|
14
|
+
// Framework detection - auto-enabled based on project
|
|
15
|
+
checkNextjs: true,
|
|
16
|
+
checkReactNative: true,
|
|
17
|
+
checkNodejs: true,
|
|
18
|
+
checkJavascript: true,
|
|
19
|
+
checkTypescript: true,
|
|
20
|
+
maxCallbackDepth: 3,
|
|
14
21
|
};
|
package/package.json
CHANGED
package/src/analyzer.ts
CHANGED
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
detectNestedTernaries,
|
|
14
14
|
detectDeadCode,
|
|
15
15
|
detectMagicValues,
|
|
16
|
+
detectNextjsIssues,
|
|
17
|
+
detectReactNativeIssues,
|
|
18
|
+
detectNodejsIssues,
|
|
19
|
+
detectJavascriptIssues,
|
|
20
|
+
detectTypescriptIssues,
|
|
16
21
|
} from './detectors/index.js';
|
|
17
22
|
import {
|
|
18
23
|
AnalysisResult,
|
|
@@ -112,6 +117,12 @@ function analyzeFile(parseResult: ParseResult, filePath: string, config: Detecto
|
|
|
112
117
|
smells.push(...detectNestedTernaries(component, filePath, sourceCode, config));
|
|
113
118
|
smells.push(...detectDeadCode(component, filePath, sourceCode, config));
|
|
114
119
|
smells.push(...detectMagicValues(component, filePath, sourceCode, config));
|
|
120
|
+
// Framework-specific detectors
|
|
121
|
+
smells.push(...detectNextjsIssues(component, filePath, sourceCode, config, imports));
|
|
122
|
+
smells.push(...detectReactNativeIssues(component, filePath, sourceCode, config, imports));
|
|
123
|
+
smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
|
|
124
|
+
smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
|
|
125
|
+
smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
|
|
115
126
|
});
|
|
116
127
|
|
|
117
128
|
// Run cross-component analysis
|
|
@@ -141,6 +152,30 @@ function calculateSummary(files: FileAnalysis[]): AnalysisSummary {
|
|
|
141
152
|
'nested-ternary': 0,
|
|
142
153
|
'dead-code': 0,
|
|
143
154
|
'magic-value': 0,
|
|
155
|
+
// Next.js
|
|
156
|
+
'nextjs-client-server-boundary': 0,
|
|
157
|
+
'nextjs-missing-metadata': 0,
|
|
158
|
+
'nextjs-image-unoptimized': 0,
|
|
159
|
+
'nextjs-router-misuse': 0,
|
|
160
|
+
// React Native
|
|
161
|
+
'rn-inline-style': 0,
|
|
162
|
+
'rn-missing-accessibility': 0,
|
|
163
|
+
'rn-performance-issue': 0,
|
|
164
|
+
// Node.js
|
|
165
|
+
'nodejs-callback-hell': 0,
|
|
166
|
+
'nodejs-unhandled-promise': 0,
|
|
167
|
+
'nodejs-sync-io': 0,
|
|
168
|
+
'nodejs-missing-error-handling': 0,
|
|
169
|
+
// JavaScript
|
|
170
|
+
'js-var-usage': 0,
|
|
171
|
+
'js-loose-equality': 0,
|
|
172
|
+
'js-implicit-coercion': 0,
|
|
173
|
+
'js-global-pollution': 0,
|
|
174
|
+
// TypeScript
|
|
175
|
+
'ts-any-usage': 0,
|
|
176
|
+
'ts-missing-return-type': 0,
|
|
177
|
+
'ts-non-null-assertion': 0,
|
|
178
|
+
'ts-type-assertion': 0,
|
|
144
179
|
};
|
|
145
180
|
|
|
146
181
|
const smellsBySeverity: Record<SmellSeverity, number> = {
|
package/src/detectors/index.ts
CHANGED
|
@@ -8,3 +8,9 @@ export { detectDependencyArrayIssues } from './dependencyArray.js';
|
|
|
8
8
|
export { detectNestedTernaries } from './nestedTernary.js';
|
|
9
9
|
export { detectDeadCode, detectUnusedImports } from './deadCode.js';
|
|
10
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
|
+
}
|