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,103 +0,0 @@
|
|
|
1
|
-
import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
|
|
2
|
-
import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
|
|
3
|
-
|
|
4
|
-
export function detectPropDrilling(
|
|
5
|
-
component: ParsedComponent,
|
|
6
|
-
filePath: string,
|
|
7
|
-
sourceCode: string,
|
|
8
|
-
config: DetectorConfig = DEFAULT_CONFIG
|
|
9
|
-
): CodeSmell[] {
|
|
10
|
-
const smells: CodeSmell[] = [];
|
|
11
|
-
|
|
12
|
-
// Check for too many props
|
|
13
|
-
if (component.props.length > config.maxPropsCount) {
|
|
14
|
-
smells.push({
|
|
15
|
-
type: 'prop-drilling',
|
|
16
|
-
severity: 'warning',
|
|
17
|
-
message: `Component "${component.name}" receives ${component.props.length} props (max recommended: ${config.maxPropsCount})`,
|
|
18
|
-
file: filePath,
|
|
19
|
-
line: component.startLine,
|
|
20
|
-
column: 0,
|
|
21
|
-
suggestion: 'Consider using Context, a state management library, or component composition',
|
|
22
|
-
codeSnippet: getCodeSnippet(sourceCode, component.startLine),
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Check for props that are just passed through (prop drilling indicators)
|
|
27
|
-
const passThroughProps = component.props.filter(prop => {
|
|
28
|
-
// Common patterns that indicate prop drilling
|
|
29
|
-
const drillingPatterns = ['data', 'config', 'settings', 'options', 'state', 'handlers', 'callbacks'];
|
|
30
|
-
return drillingPatterns.some(pattern => prop.toLowerCase().includes(pattern));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
if (passThroughProps.length >= 3) {
|
|
34
|
-
smells.push({
|
|
35
|
-
type: 'prop-drilling',
|
|
36
|
-
severity: 'info',
|
|
37
|
-
message: `Component "${component.name}" may be experiencing prop drilling: ${passThroughProps.join(', ')}`,
|
|
38
|
-
file: filePath,
|
|
39
|
-
line: component.startLine,
|
|
40
|
-
column: 0,
|
|
41
|
-
suggestion: 'Consider using React Context or a state management solution to avoid passing data through intermediate components',
|
|
42
|
-
codeSnippet: getCodeSnippet(sourceCode, component.startLine),
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Check for spread props (often indicates forwarding)
|
|
47
|
-
const spreadProps = component.props.filter(p => p.startsWith('...'));
|
|
48
|
-
if (spreadProps.length > 0 && component.props.length > 5) {
|
|
49
|
-
smells.push({
|
|
50
|
-
type: 'prop-drilling',
|
|
51
|
-
severity: 'info',
|
|
52
|
-
message: `Component "${component.name}" uses spread props with many other props - potential prop drilling`,
|
|
53
|
-
file: filePath,
|
|
54
|
-
line: component.startLine,
|
|
55
|
-
column: 0,
|
|
56
|
-
suggestion: 'Consider composing components differently or using Context for shared state',
|
|
57
|
-
codeSnippet: getCodeSnippet(sourceCode, component.startLine),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return smells;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function analyzePropDrillingDepth(
|
|
65
|
-
components: ParsedComponent[],
|
|
66
|
-
filePath: string,
|
|
67
|
-
sourceCode: string,
|
|
68
|
-
config: DetectorConfig = DEFAULT_CONFIG
|
|
69
|
-
): CodeSmell[] {
|
|
70
|
-
const smells: CodeSmell[] = [];
|
|
71
|
-
|
|
72
|
-
// Track props that appear in multiple components
|
|
73
|
-
const propUsage = new Map<string, string[]>();
|
|
74
|
-
|
|
75
|
-
components.forEach(comp => {
|
|
76
|
-
comp.props.forEach(prop => {
|
|
77
|
-
if (!prop.startsWith('...')) {
|
|
78
|
-
const existing = propUsage.get(prop) || [];
|
|
79
|
-
existing.push(comp.name);
|
|
80
|
-
propUsage.set(prop, existing);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Find props used in more than maxPropDrillingDepth components
|
|
86
|
-
propUsage.forEach((componentNames, propName) => {
|
|
87
|
-
if (componentNames.length > config.maxPropDrillingDepth) {
|
|
88
|
-
const firstComp = components.find(c => c.name === componentNames[0]);
|
|
89
|
-
smells.push({
|
|
90
|
-
type: 'prop-drilling',
|
|
91
|
-
severity: 'warning',
|
|
92
|
-
message: `Prop "${propName}" is passed through ${componentNames.length} components: ${componentNames.join(' → ')}`,
|
|
93
|
-
file: filePath,
|
|
94
|
-
line: firstComp?.startLine || 1,
|
|
95
|
-
column: 0,
|
|
96
|
-
suggestion: `Move "${propName}" to Context or a state management solution`,
|
|
97
|
-
codeSnippet: firstComp ? getCodeSnippet(sourceCode, firstComp.startLine) : undefined,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
return smells;
|
|
103
|
-
}
|
|
@@ -1,154 +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 React Native-specific code smells:
|
|
8
|
-
* - Inline styles instead of StyleSheet
|
|
9
|
-
* - Missing accessibility props
|
|
10
|
-
* - Performance anti-patterns
|
|
11
|
-
*/
|
|
12
|
-
export function detectReactNativeIssues(
|
|
13
|
-
component: ParsedComponent,
|
|
14
|
-
filePath: string,
|
|
15
|
-
sourceCode: string,
|
|
16
|
-
config: DetectorConfig = DEFAULT_CONFIG,
|
|
17
|
-
imports: string[] = []
|
|
18
|
-
): CodeSmell[] {
|
|
19
|
-
if (!config.checkReactNative) return [];
|
|
20
|
-
|
|
21
|
-
// Only run on React Native projects
|
|
22
|
-
const isRNProject = imports.some(imp =>
|
|
23
|
-
imp.includes('react-native') || imp.includes('@react-native')
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
if (!isRNProject) return [];
|
|
27
|
-
|
|
28
|
-
const smells: CodeSmell[] = [];
|
|
29
|
-
const inlineStyleLines = new Set<number>();
|
|
30
|
-
|
|
31
|
-
// Check for inline styles instead of StyleSheet
|
|
32
|
-
component.path.traverse({
|
|
33
|
-
JSXAttribute(path) {
|
|
34
|
-
if (!t.isJSXIdentifier(path.node.name)) return;
|
|
35
|
-
if (path.node.name.name !== 'style') return;
|
|
36
|
-
|
|
37
|
-
const value = path.node.value;
|
|
38
|
-
if (!t.isJSXExpressionContainer(value)) return;
|
|
39
|
-
|
|
40
|
-
// Check if it's an inline object (not a StyleSheet reference)
|
|
41
|
-
if (t.isObjectExpression(value.expression)) {
|
|
42
|
-
const loc = path.node.loc;
|
|
43
|
-
const line = loc?.start.line || 0;
|
|
44
|
-
|
|
45
|
-
if (!inlineStyleLines.has(line)) {
|
|
46
|
-
inlineStyleLines.add(line);
|
|
47
|
-
smells.push({
|
|
48
|
-
type: 'rn-inline-style',
|
|
49
|
-
severity: 'warning',
|
|
50
|
-
message: `Inline style object in "${component.name}" - prefer StyleSheet.create()`,
|
|
51
|
-
file: filePath,
|
|
52
|
-
line,
|
|
53
|
-
column: loc?.start.column || 0,
|
|
54
|
-
suggestion: 'Use StyleSheet.create() for better performance: const styles = StyleSheet.create({ ... })',
|
|
55
|
-
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Check for missing accessibility props on interactive elements
|
|
63
|
-
const interactiveComponents = ['TouchableOpacity', 'TouchableHighlight', 'Pressable', 'Button', 'TouchableWithoutFeedback'];
|
|
64
|
-
|
|
65
|
-
component.path.traverse({
|
|
66
|
-
JSXOpeningElement(path) {
|
|
67
|
-
if (!t.isJSXIdentifier(path.node.name)) return;
|
|
68
|
-
|
|
69
|
-
const componentName = path.node.name.name;
|
|
70
|
-
if (!interactiveComponents.includes(componentName)) return;
|
|
71
|
-
|
|
72
|
-
// Check for accessibility props
|
|
73
|
-
const hasAccessibilityLabel = path.node.attributes.some(attr => {
|
|
74
|
-
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
75
|
-
return ['accessibilityLabel', 'accessible', 'accessibilityRole', 'accessibilityHint'].includes(attr.name.name);
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (!hasAccessibilityLabel) {
|
|
81
|
-
const loc = path.node.loc;
|
|
82
|
-
smells.push({
|
|
83
|
-
type: 'rn-missing-accessibility',
|
|
84
|
-
severity: 'info',
|
|
85
|
-
message: `${componentName} missing accessibility props in "${component.name}"`,
|
|
86
|
-
file: filePath,
|
|
87
|
-
line: loc?.start.line || 0,
|
|
88
|
-
column: loc?.start.column || 0,
|
|
89
|
-
suggestion: 'Add accessibilityLabel and accessibilityRole for screen readers',
|
|
90
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Check for performance anti-patterns
|
|
97
|
-
component.path.traverse({
|
|
98
|
-
// Detect creating anonymous functions in render (for onPress, etc.)
|
|
99
|
-
JSXAttribute(path) {
|
|
100
|
-
if (!t.isJSXIdentifier(path.node.name)) return;
|
|
101
|
-
|
|
102
|
-
const propName = path.node.name.name;
|
|
103
|
-
if (!['onPress', 'onPressIn', 'onPressOut', 'onLongPress'].includes(propName)) return;
|
|
104
|
-
|
|
105
|
-
const value = path.node.value;
|
|
106
|
-
if (!t.isJSXExpressionContainer(value)) return;
|
|
107
|
-
|
|
108
|
-
// Check for arrow functions or function expressions
|
|
109
|
-
if (t.isArrowFunctionExpression(value.expression) || t.isFunctionExpression(value.expression)) {
|
|
110
|
-
const loc = path.node.loc;
|
|
111
|
-
smells.push({
|
|
112
|
-
type: 'rn-performance-issue',
|
|
113
|
-
severity: 'info',
|
|
114
|
-
message: `Inline function for ${propName} in "${component.name}" creates new reference each render`,
|
|
115
|
-
file: filePath,
|
|
116
|
-
line: loc?.start.line || 0,
|
|
117
|
-
column: loc?.start.column || 0,
|
|
118
|
-
suggestion: 'Extract to useCallback or class method to prevent unnecessary re-renders',
|
|
119
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
// Detect array spreads in render that could be memoized
|
|
125
|
-
SpreadElement(path) {
|
|
126
|
-
const loc = path.node.loc;
|
|
127
|
-
// Only flag if inside JSX or return statement
|
|
128
|
-
let inJSX = false;
|
|
129
|
-
let current: NodePath | null = path.parentPath;
|
|
130
|
-
while (current) {
|
|
131
|
-
if (t.isJSXElement(current.node) || t.isJSXFragment(current.node)) {
|
|
132
|
-
inJSX = true;
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
current = current.parentPath;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (inJSX && t.isArrayExpression(path.parent)) {
|
|
139
|
-
smells.push({
|
|
140
|
-
type: 'rn-performance-issue',
|
|
141
|
-
severity: 'info',
|
|
142
|
-
message: `Array spread in render may cause performance issues in "${component.name}"`,
|
|
143
|
-
file: filePath,
|
|
144
|
-
line: loc?.start.line || 0,
|
|
145
|
-
column: loc?.start.column || 0,
|
|
146
|
-
suggestion: 'Consider memoizing with useMemo if this array is passed as a prop',
|
|
147
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return smells;
|
|
154
|
-
}
|
|
@@ -1,179 +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 security vulnerabilities:
|
|
7
|
-
* - dangerouslySetInnerHTML usage
|
|
8
|
-
* - eval() and Function() constructor
|
|
9
|
-
* - innerHTML assignments
|
|
10
|
-
* - Unsafe URLs (javascript:, data:)
|
|
11
|
-
* - Exposed secrets/API keys
|
|
12
|
-
*/
|
|
13
|
-
export function detectSecurityIssues(
|
|
14
|
-
component: ParsedComponent,
|
|
15
|
-
filePath: string,
|
|
16
|
-
sourceCode: string,
|
|
17
|
-
config: DetectorConfig = DEFAULT_CONFIG
|
|
18
|
-
): CodeSmell[] {
|
|
19
|
-
if (!config.checkSecurity) return [];
|
|
20
|
-
|
|
21
|
-
const smells: CodeSmell[] = [];
|
|
22
|
-
|
|
23
|
-
// Detect dangerouslySetInnerHTML
|
|
24
|
-
component.path.traverse({
|
|
25
|
-
JSXAttribute(path) {
|
|
26
|
-
if (t.isJSXIdentifier(path.node.name) &&
|
|
27
|
-
path.node.name.name === 'dangerouslySetInnerHTML') {
|
|
28
|
-
const loc = path.node.loc;
|
|
29
|
-
smells.push({
|
|
30
|
-
type: 'security-xss',
|
|
31
|
-
severity: 'error',
|
|
32
|
-
message: `dangerouslySetInnerHTML is a security risk in "${component.name}"`,
|
|
33
|
-
file: filePath,
|
|
34
|
-
line: loc?.start.line || 0,
|
|
35
|
-
column: loc?.start.column || 0,
|
|
36
|
-
suggestion: 'Sanitize HTML with DOMPurify or use a safe alternative like converting to React elements',
|
|
37
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Detect eval() and Function() constructor
|
|
44
|
-
component.path.traverse({
|
|
45
|
-
CallExpression(path) {
|
|
46
|
-
const { callee } = path.node;
|
|
47
|
-
|
|
48
|
-
// eval()
|
|
49
|
-
if (t.isIdentifier(callee) && callee.name === 'eval') {
|
|
50
|
-
const loc = path.node.loc;
|
|
51
|
-
smells.push({
|
|
52
|
-
type: 'security-eval',
|
|
53
|
-
severity: 'error',
|
|
54
|
-
message: `eval() is a critical security risk in "${component.name}"`,
|
|
55
|
-
file: filePath,
|
|
56
|
-
line: loc?.start.line || 0,
|
|
57
|
-
column: loc?.start.column || 0,
|
|
58
|
-
suggestion: 'Never use eval(). Parse JSON with JSON.parse() or restructure logic to avoid dynamic code execution.',
|
|
59
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
// new Function()
|
|
65
|
-
NewExpression(path) {
|
|
66
|
-
const { callee } = path.node;
|
|
67
|
-
if (t.isIdentifier(callee) && callee.name === 'Function') {
|
|
68
|
-
const loc = path.node.loc;
|
|
69
|
-
smells.push({
|
|
70
|
-
type: 'security-eval',
|
|
71
|
-
severity: 'error',
|
|
72
|
-
message: `new Function() is equivalent to eval() and is a security risk`,
|
|
73
|
-
file: filePath,
|
|
74
|
-
line: loc?.start.line || 0,
|
|
75
|
-
column: loc?.start.column || 0,
|
|
76
|
-
suggestion: 'Avoid creating functions from strings. Restructure to use static function definitions.',
|
|
77
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Detect innerHTML assignments
|
|
84
|
-
component.path.traverse({
|
|
85
|
-
AssignmentExpression(path) {
|
|
86
|
-
const { left } = path.node;
|
|
87
|
-
|
|
88
|
-
if (t.isMemberExpression(left) && t.isIdentifier(left.property)) {
|
|
89
|
-
if (left.property.name === 'innerHTML' || left.property.name === 'outerHTML') {
|
|
90
|
-
const loc = path.node.loc;
|
|
91
|
-
smells.push({
|
|
92
|
-
type: 'security-xss',
|
|
93
|
-
severity: 'warning',
|
|
94
|
-
message: `Direct ${left.property.name} assignment can lead to XSS`,
|
|
95
|
-
file: filePath,
|
|
96
|
-
line: loc?.start.line || 0,
|
|
97
|
-
column: loc?.start.column || 0,
|
|
98
|
-
suggestion: 'Use textContent for plain text, or sanitize HTML with DOMPurify',
|
|
99
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Detect unsafe URLs (javascript:, data:)
|
|
107
|
-
component.path.traverse({
|
|
108
|
-
JSXAttribute(path) {
|
|
109
|
-
if (!t.isJSXIdentifier(path.node.name)) return;
|
|
110
|
-
|
|
111
|
-
const propName = path.node.name.name;
|
|
112
|
-
if (!['href', 'src', 'action'].includes(propName)) return;
|
|
113
|
-
|
|
114
|
-
const value = path.node.value;
|
|
115
|
-
if (t.isStringLiteral(value)) {
|
|
116
|
-
const url = value.value.toLowerCase().trim();
|
|
117
|
-
|
|
118
|
-
if (url.startsWith('javascript:')) {
|
|
119
|
-
const loc = path.node.loc;
|
|
120
|
-
smells.push({
|
|
121
|
-
type: 'security-xss',
|
|
122
|
-
severity: 'error',
|
|
123
|
-
message: `javascript: URLs are a security risk`,
|
|
124
|
-
file: filePath,
|
|
125
|
-
line: loc?.start.line || 0,
|
|
126
|
-
column: loc?.start.column || 0,
|
|
127
|
-
suggestion: 'Use onClick handlers instead of javascript: URLs',
|
|
128
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (url.startsWith('data:') && propName === 'href') {
|
|
133
|
-
const loc = path.node.loc;
|
|
134
|
-
smells.push({
|
|
135
|
-
type: 'security-xss',
|
|
136
|
-
severity: 'warning',
|
|
137
|
-
message: `data: URLs in href can be a security risk`,
|
|
138
|
-
file: filePath,
|
|
139
|
-
line: loc?.start.line || 0,
|
|
140
|
-
column: loc?.start.column || 0,
|
|
141
|
-
suggestion: 'Validate and sanitize data URLs, or use blob URLs instead',
|
|
142
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Detect potential exposed secrets
|
|
150
|
-
const secretPatterns = [
|
|
151
|
-
{ pattern: /['"](?:sk[-_]live|pk[-_]live|api[-_]?key|secret[-_]?key|access[-_]?token|auth[-_]?token)['"]\s*[:=]\s*['"][a-zA-Z0-9-_]{20,}/i, name: 'API key' },
|
|
152
|
-
{ pattern: /['"](?:ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{36,}['"]/i, name: 'GitHub token' },
|
|
153
|
-
{ pattern: /['"]AKIA[A-Z0-9]{16}['"]/i, name: 'AWS access key' },
|
|
154
|
-
{ pattern: /password\s*[:=]\s*['"][^'"]{8,}['"]/i, name: 'Hardcoded password' },
|
|
155
|
-
];
|
|
156
|
-
|
|
157
|
-
const lines = sourceCode.split('\n');
|
|
158
|
-
lines.forEach((line, index) => {
|
|
159
|
-
const lineNum = index + 1;
|
|
160
|
-
if (lineNum < component.startLine || lineNum > component.endLine) return;
|
|
161
|
-
|
|
162
|
-
secretPatterns.forEach(({ pattern, name }) => {
|
|
163
|
-
if (pattern.test(line)) {
|
|
164
|
-
smells.push({
|
|
165
|
-
type: 'security-secrets',
|
|
166
|
-
severity: 'error',
|
|
167
|
-
message: `Potential ${name} exposed in code`,
|
|
168
|
-
file: filePath,
|
|
169
|
-
line: lineNum,
|
|
170
|
-
column: 0,
|
|
171
|
-
suggestion: 'Move secrets to environment variables (.env) and never commit them to version control',
|
|
172
|
-
codeSnippet: getCodeSnippet(sourceCode, lineNum),
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return smells;
|
|
179
|
-
}
|
|
@@ -1,151 +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 TypeScript-specific code smells:
|
|
7
|
-
* - Overuse of 'any' type
|
|
8
|
-
* - Missing return type on functions
|
|
9
|
-
* - Non-null assertion operator (!)
|
|
10
|
-
* - Type assertions (as) that could be avoided
|
|
11
|
-
*/
|
|
12
|
-
export function detectTypescriptIssues(
|
|
13
|
-
component: ParsedComponent,
|
|
14
|
-
filePath: string,
|
|
15
|
-
sourceCode: string,
|
|
16
|
-
config: DetectorConfig = DEFAULT_CONFIG
|
|
17
|
-
): CodeSmell[] {
|
|
18
|
-
if (!config.checkTypescript) return [];
|
|
19
|
-
|
|
20
|
-
// Only run on TypeScript files
|
|
21
|
-
if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx')) return [];
|
|
22
|
-
|
|
23
|
-
const smells: CodeSmell[] = [];
|
|
24
|
-
|
|
25
|
-
// Detect 'any' type usage
|
|
26
|
-
component.path.traverse({
|
|
27
|
-
TSAnyKeyword(path) {
|
|
28
|
-
const loc = path.node.loc;
|
|
29
|
-
smells.push({
|
|
30
|
-
type: 'ts-any-usage',
|
|
31
|
-
severity: 'warning',
|
|
32
|
-
message: `Using "any" type in "${component.name}"`,
|
|
33
|
-
file: filePath,
|
|
34
|
-
line: loc?.start.line || 0,
|
|
35
|
-
column: loc?.start.column || 0,
|
|
36
|
-
suggestion: 'Use a specific type, "unknown", or create an interface',
|
|
37
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
38
|
-
});
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Detect functions without return type (only in complex functions)
|
|
43
|
-
component.path.traverse({
|
|
44
|
-
FunctionDeclaration(path) {
|
|
45
|
-
// Skip if function has explicit return type
|
|
46
|
-
if (path.node.returnType) return;
|
|
47
|
-
|
|
48
|
-
// Check if function body has return statements
|
|
49
|
-
let hasReturn = false;
|
|
50
|
-
path.traverse({
|
|
51
|
-
ReturnStatement(returnPath) {
|
|
52
|
-
if (returnPath.node.argument) {
|
|
53
|
-
hasReturn = true;
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
if (hasReturn) {
|
|
59
|
-
const loc = path.node.loc;
|
|
60
|
-
smells.push({
|
|
61
|
-
type: 'ts-missing-return-type',
|
|
62
|
-
severity: 'info',
|
|
63
|
-
message: `Function "${path.node.id?.name || 'anonymous'}" missing return type`,
|
|
64
|
-
file: filePath,
|
|
65
|
-
line: loc?.start.line || 0,
|
|
66
|
-
column: loc?.start.column || 0,
|
|
67
|
-
suggestion: 'Add explicit return type: function name(): ReturnType { ... }',
|
|
68
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
ArrowFunctionExpression(path) {
|
|
74
|
-
// Only check arrow functions assigned to variables with 5+ lines
|
|
75
|
-
if (!path.node.returnType) {
|
|
76
|
-
const body = path.node.body;
|
|
77
|
-
|
|
78
|
-
// Skip simple arrow functions (single expression)
|
|
79
|
-
if (!t.isBlockStatement(body)) return;
|
|
80
|
-
|
|
81
|
-
// Check complexity - only flag if function is substantial
|
|
82
|
-
const startLine = path.node.loc?.start.line || 0;
|
|
83
|
-
const endLine = path.node.loc?.end.line || 0;
|
|
84
|
-
|
|
85
|
-
if (endLine - startLine >= 5) {
|
|
86
|
-
// Check if parent is variable declarator (assigned to variable)
|
|
87
|
-
const parent = path.parent;
|
|
88
|
-
|
|
89
|
-
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
90
|
-
const loc = path.node.loc;
|
|
91
|
-
smells.push({
|
|
92
|
-
type: 'ts-missing-return-type',
|
|
93
|
-
severity: 'info',
|
|
94
|
-
message: `Arrow function "${parent.id.name}" missing return type`,
|
|
95
|
-
file: filePath,
|
|
96
|
-
line: loc?.start.line || 0,
|
|
97
|
-
column: loc?.start.column || 0,
|
|
98
|
-
suggestion: 'Add return type: const name = (): ReturnType => { ... }',
|
|
99
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Detect non-null assertion operator (!)
|
|
108
|
-
component.path.traverse({
|
|
109
|
-
TSNonNullExpression(path) {
|
|
110
|
-
const loc = path.node.loc;
|
|
111
|
-
smells.push({
|
|
112
|
-
type: 'ts-non-null-assertion',
|
|
113
|
-
severity: 'warning',
|
|
114
|
-
message: `Non-null assertion (!) bypasses type safety in "${component.name}"`,
|
|
115
|
-
file: filePath,
|
|
116
|
-
line: loc?.start.line || 0,
|
|
117
|
-
column: loc?.start.column || 0,
|
|
118
|
-
suggestion: 'Use optional chaining (?.) or proper null checks instead',
|
|
119
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
120
|
-
});
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Detect type assertions (as keyword) - these can hide type errors
|
|
125
|
-
component.path.traverse({
|
|
126
|
-
TSAsExpression(path) {
|
|
127
|
-
// Skip assertions to 'const' (used for const assertions)
|
|
128
|
-
if (t.isTSTypeReference(path.node.typeAnnotation)) {
|
|
129
|
-
const typeName = path.node.typeAnnotation.typeName;
|
|
130
|
-
if (t.isIdentifier(typeName) && typeName.name === 'const') return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Skip double assertions (as unknown as Type) - already flagged by TypeScript
|
|
134
|
-
if (t.isTSAsExpression(path.node.expression)) return;
|
|
135
|
-
|
|
136
|
-
const loc = path.node.loc;
|
|
137
|
-
smells.push({
|
|
138
|
-
type: 'ts-type-assertion',
|
|
139
|
-
severity: 'info',
|
|
140
|
-
message: `Type assertion (as) bypasses type checking in "${component.name}"`,
|
|
141
|
-
file: filePath,
|
|
142
|
-
line: loc?.start.line || 0,
|
|
143
|
-
column: loc?.start.column || 0,
|
|
144
|
-
suggestion: 'Consider using type guards or proper typing instead of assertions',
|
|
145
|
-
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
146
|
-
});
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return smells;
|
|
151
|
-
}
|