react-code-smell-detector 1.1.1 → 1.3.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 +115 -11
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +65 -2
- package/dist/cli.js +134 -33
- package/dist/detectors/accessibility.d.ts +12 -0
- package/dist/detectors/accessibility.d.ts.map +1 -0
- package/dist/detectors/accessibility.js +191 -0
- 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/debug.d.ts +10 -0
- package/dist/detectors/debug.d.ts.map +1 -0
- package/dist/detectors/debug.js +87 -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 +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +8 -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/security.d.ts +12 -0
- package/dist/detectors/security.d.ts.map +1 -0
- package/dist/detectors/security.js +161 -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 +28 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +117 -0
- package/dist/htmlReporter.d.ts +6 -0
- package/dist/htmlReporter.d.ts.map +1 -0
- package/dist/htmlReporter.js +453 -0
- package/dist/reporter.js +26 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +13 -0
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +89 -0
- package/package.json +8 -2
- package/src/analyzer.ts +0 -269
- package/src/cli.ts +0 -125
- package/src/detectors/deadCode.ts +0 -163
- package/src/detectors/dependencyArray.ts +0 -176
- package/src/detectors/hooksRules.ts +0 -101
- package/src/detectors/index.ts +0 -16
- 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/typescript.ts +0 -151
- package/src/detectors/useEffect.ts +0 -117
- package/src/index.ts +0 -4
- package/src/parser/index.ts +0 -195
- package/src/reporter.ts +0 -278
- package/src/types/index.ts +0 -144
- package/tsconfig.json +0 -19
|
@@ -0,0 +1,191 @@
|
|
|
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 accessibility (a11y) issues:
|
|
6
|
+
* - Images without alt text
|
|
7
|
+
* - Form inputs without labels
|
|
8
|
+
* - Missing ARIA attributes on interactive elements
|
|
9
|
+
* - Click handlers without keyboard support
|
|
10
|
+
* - Improper heading hierarchy
|
|
11
|
+
*/
|
|
12
|
+
export function detectAccessibilityIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
|
|
13
|
+
if (!config.checkAccessibility)
|
|
14
|
+
return [];
|
|
15
|
+
const smells = [];
|
|
16
|
+
component.path.traverse({
|
|
17
|
+
JSXOpeningElement(path) {
|
|
18
|
+
if (!t.isJSXIdentifier(path.node.name))
|
|
19
|
+
return;
|
|
20
|
+
const elementName = path.node.name.name;
|
|
21
|
+
const attributes = path.node.attributes;
|
|
22
|
+
const loc = path.node.loc;
|
|
23
|
+
const line = loc?.start.line || 0;
|
|
24
|
+
// Helper to check if attribute exists
|
|
25
|
+
const hasAttr = (name) => {
|
|
26
|
+
return attributes.some(attr => {
|
|
27
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
28
|
+
return attr.name.name === name;
|
|
29
|
+
}
|
|
30
|
+
// Handle spread attributes - assume they might contain the attribute
|
|
31
|
+
return t.isJSXSpreadAttribute(attr);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
// Helper to get attribute value
|
|
35
|
+
const getAttrValue = (name) => {
|
|
36
|
+
for (const attr of attributes) {
|
|
37
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === name) {
|
|
38
|
+
if (t.isStringLiteral(attr.value)) {
|
|
39
|
+
return attr.value.value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
// Check for images without alt text
|
|
46
|
+
if (elementName === 'img') {
|
|
47
|
+
if (!hasAttr('alt')) {
|
|
48
|
+
smells.push({
|
|
49
|
+
type: 'a11y-missing-alt',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
message: `<img> missing alt attribute in "${component.name}"`,
|
|
52
|
+
file: filePath,
|
|
53
|
+
line,
|
|
54
|
+
column: loc?.start.column || 0,
|
|
55
|
+
suggestion: 'Add alt="description" for content images, or alt="" for decorative images',
|
|
56
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const altValue = getAttrValue('alt');
|
|
61
|
+
if (altValue !== null && altValue.toLowerCase().includes('image')) {
|
|
62
|
+
smells.push({
|
|
63
|
+
type: 'a11y-missing-alt',
|
|
64
|
+
severity: 'info',
|
|
65
|
+
message: `<img> alt text shouldn't contain "image" - it's redundant`,
|
|
66
|
+
file: filePath,
|
|
67
|
+
line,
|
|
68
|
+
column: loc?.start.column || 0,
|
|
69
|
+
suggestion: 'Describe what the image shows, not that it is an image',
|
|
70
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Check for form inputs without associated labels
|
|
76
|
+
if (elementName === 'input' || elementName === 'textarea' || elementName === 'select') {
|
|
77
|
+
const inputType = getAttrValue('type') || 'text';
|
|
78
|
+
// Skip hidden inputs
|
|
79
|
+
if (inputType === 'hidden')
|
|
80
|
+
return;
|
|
81
|
+
const hasLabel = hasAttr('aria-label') ||
|
|
82
|
+
hasAttr('aria-labelledby') ||
|
|
83
|
+
hasAttr('id'); // Assume id might be linked to a label
|
|
84
|
+
if (!hasLabel) {
|
|
85
|
+
smells.push({
|
|
86
|
+
type: 'a11y-missing-label',
|
|
87
|
+
severity: 'warning',
|
|
88
|
+
message: `<${elementName}> without accessible label in "${component.name}"`,
|
|
89
|
+
file: filePath,
|
|
90
|
+
line,
|
|
91
|
+
column: loc?.start.column || 0,
|
|
92
|
+
suggestion: 'Add aria-label, aria-labelledby, or associate with a <label> element',
|
|
93
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Check for interactive divs/spans without proper role and keyboard support
|
|
98
|
+
if (elementName === 'div' || elementName === 'span') {
|
|
99
|
+
const hasOnClick = hasAttr('onClick');
|
|
100
|
+
const hasRole = hasAttr('role');
|
|
101
|
+
const hasTabIndex = hasAttr('tabIndex');
|
|
102
|
+
const hasKeyboardHandler = hasAttr('onKeyDown') || hasAttr('onKeyUp') || hasAttr('onKeyPress');
|
|
103
|
+
if (hasOnClick) {
|
|
104
|
+
if (!hasRole) {
|
|
105
|
+
smells.push({
|
|
106
|
+
type: 'a11y-interactive-role',
|
|
107
|
+
severity: 'warning',
|
|
108
|
+
message: `Clickable <${elementName}> without role attribute in "${component.name}"`,
|
|
109
|
+
file: filePath,
|
|
110
|
+
line,
|
|
111
|
+
column: loc?.start.column || 0,
|
|
112
|
+
suggestion: 'Add role="button" or use a <button> element instead',
|
|
113
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (!hasTabIndex) {
|
|
117
|
+
smells.push({
|
|
118
|
+
type: 'a11y-interactive-role',
|
|
119
|
+
severity: 'warning',
|
|
120
|
+
message: `Clickable <${elementName}> not focusable in "${component.name}"`,
|
|
121
|
+
file: filePath,
|
|
122
|
+
line,
|
|
123
|
+
column: loc?.start.column || 0,
|
|
124
|
+
suggestion: 'Add tabIndex={0} to make the element focusable',
|
|
125
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (!hasKeyboardHandler) {
|
|
129
|
+
smells.push({
|
|
130
|
+
type: 'a11y-keyboard',
|
|
131
|
+
severity: 'info',
|
|
132
|
+
message: `Clickable <${elementName}> without keyboard handler in "${component.name}"`,
|
|
133
|
+
file: filePath,
|
|
134
|
+
line,
|
|
135
|
+
column: loc?.start.column || 0,
|
|
136
|
+
suggestion: 'Add onKeyDown to handle Enter/Space for keyboard users',
|
|
137
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Check for anchor tags without href (should be buttons)
|
|
143
|
+
if (elementName === 'a') {
|
|
144
|
+
if (!hasAttr('href')) {
|
|
145
|
+
smells.push({
|
|
146
|
+
type: 'a11y-semantic',
|
|
147
|
+
severity: 'warning',
|
|
148
|
+
message: `<a> without href should be a <button> in "${component.name}"`,
|
|
149
|
+
file: filePath,
|
|
150
|
+
line,
|
|
151
|
+
column: loc?.start.column || 0,
|
|
152
|
+
suggestion: 'Use <button> for actions and <a href="..."> for navigation',
|
|
153
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Check for proper button usage
|
|
158
|
+
if (elementName === 'button') {
|
|
159
|
+
if (!hasAttr('type')) {
|
|
160
|
+
smells.push({
|
|
161
|
+
type: 'a11y-semantic',
|
|
162
|
+
severity: 'info',
|
|
163
|
+
message: `<button> should have explicit type attribute`,
|
|
164
|
+
file: filePath,
|
|
165
|
+
line,
|
|
166
|
+
column: loc?.start.column || 0,
|
|
167
|
+
suggestion: 'Add type="button" or type="submit" to clarify button behavior',
|
|
168
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Check for icons that might need labels
|
|
173
|
+
if (elementName === 'svg' || elementName === 'Icon' || elementName.endsWith('Icon')) {
|
|
174
|
+
const hasAriaLabel = hasAttr('aria-label') || hasAttr('aria-hidden') || hasAttr('title');
|
|
175
|
+
if (!hasAriaLabel) {
|
|
176
|
+
smells.push({
|
|
177
|
+
type: 'a11y-missing-label',
|
|
178
|
+
severity: 'info',
|
|
179
|
+
message: `Icon/SVG may need aria-label or aria-hidden in "${component.name}"`,
|
|
180
|
+
file: filePath,
|
|
181
|
+
line,
|
|
182
|
+
column: loc?.start.column || 0,
|
|
183
|
+
suggestion: 'Add aria-label for meaningful icons, or aria-hidden="true" for decorative ones',
|
|
184
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
return smells;
|
|
191
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
export interface ComplexityMetrics {
|
|
4
|
+
cyclomaticComplexity: number;
|
|
5
|
+
cognitiveComplexity: number;
|
|
6
|
+
maxNestingDepth: number;
|
|
7
|
+
linesOfCode: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Detect code complexity issues in a component
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectComplexity(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
13
|
+
/**
|
|
14
|
+
* Calculate complexity metrics for a component
|
|
15
|
+
*/
|
|
16
|
+
export declare function calculateComplexityMetrics(component: ParsedComponent): ComplexityMetrics;
|
|
17
|
+
//# sourceMappingURL=complexity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"complexity.d.ts","sourceRoot":"","sources":["../../src/detectors/complexity.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAqCb;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,eAAe,GAAG,iBAAiB,CA6BxF"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect code complexity issues in a component
|
|
3
|
+
*/
|
|
4
|
+
export function detectComplexity(component, filePath, sourceCode, config) {
|
|
5
|
+
if (!config.checkComplexity)
|
|
6
|
+
return [];
|
|
7
|
+
const smells = [];
|
|
8
|
+
const metrics = calculateComplexityMetrics(component);
|
|
9
|
+
const thresholds = {
|
|
10
|
+
cyclomatic: config.maxCyclomaticComplexity || 10,
|
|
11
|
+
cognitive: config.maxCognitiveComplexity || 15,
|
|
12
|
+
nesting: config.maxNestingDepth || 4,
|
|
13
|
+
};
|
|
14
|
+
if (metrics.cyclomaticComplexity > thresholds.cyclomatic) {
|
|
15
|
+
smells.push({
|
|
16
|
+
type: 'high-cyclomatic-complexity',
|
|
17
|
+
severity: metrics.cyclomaticComplexity > thresholds.cyclomatic * 1.5 ? 'error' : 'warning',
|
|
18
|
+
message: `Component "${component.name}" has cyclomatic complexity of ${metrics.cyclomaticComplexity} (threshold: ${thresholds.cyclomatic})`,
|
|
19
|
+
file: filePath,
|
|
20
|
+
line: component.startLine,
|
|
21
|
+
column: 0,
|
|
22
|
+
suggestion: 'Break down complex logic into smaller functions.',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (metrics.cognitiveComplexity > thresholds.cognitive) {
|
|
26
|
+
smells.push({
|
|
27
|
+
type: 'high-cognitive-complexity',
|
|
28
|
+
severity: metrics.cognitiveComplexity > thresholds.cognitive * 1.5 ? 'error' : 'warning',
|
|
29
|
+
message: `Component "${component.name}" has cognitive complexity of ${metrics.cognitiveComplexity} (threshold: ${thresholds.cognitive})`,
|
|
30
|
+
file: filePath,
|
|
31
|
+
line: component.startLine,
|
|
32
|
+
column: 0,
|
|
33
|
+
suggestion: 'Simplify nested conditions and flatten control flow.',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return smells;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Calculate complexity metrics for a component
|
|
40
|
+
*/
|
|
41
|
+
export function calculateComplexityMetrics(component) {
|
|
42
|
+
let cyclomaticComplexity = 1;
|
|
43
|
+
let cognitiveComplexity = 0;
|
|
44
|
+
component.path.traverse({
|
|
45
|
+
IfStatement() { cyclomaticComplexity++; cognitiveComplexity++; },
|
|
46
|
+
ConditionalExpression() { cyclomaticComplexity++; cognitiveComplexity++; },
|
|
47
|
+
LogicalExpression(path) {
|
|
48
|
+
if (path.node.operator === '&&' || path.node.operator === '||') {
|
|
49
|
+
cyclomaticComplexity++;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
SwitchCase(path) {
|
|
53
|
+
if (path.node.test !== null)
|
|
54
|
+
cyclomaticComplexity++;
|
|
55
|
+
},
|
|
56
|
+
ForStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
57
|
+
ForInStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
58
|
+
ForOfStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
59
|
+
WhileStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
60
|
+
DoWhileStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
|
|
61
|
+
CatchClause() { cyclomaticComplexity++; cognitiveComplexity++; },
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
cyclomaticComplexity,
|
|
65
|
+
cognitiveComplexity,
|
|
66
|
+
maxNestingDepth: component.jsxDepth,
|
|
67
|
+
linesOfCode: component.endLine - component.startLine + 1,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects debug statements that should be removed:
|
|
5
|
+
* - console.log/warn/error/debug
|
|
6
|
+
* - debugger statements
|
|
7
|
+
* - TODO/FIXME/HACK comments (detected via source code)
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectDebugStatements(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
|
|
10
|
+
//# sourceMappingURL=debug.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../../src/detectors/debug.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAuFb"}
|
|
@@ -0,0 +1,87 @@
|
|
|
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 debug statements that should be removed:
|
|
6
|
+
* - console.log/warn/error/debug
|
|
7
|
+
* - debugger statements
|
|
8
|
+
* - TODO/FIXME/HACK comments (detected via source code)
|
|
9
|
+
*/
|
|
10
|
+
export function detectDebugStatements(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
|
|
11
|
+
if (!config.checkDebugStatements)
|
|
12
|
+
return [];
|
|
13
|
+
const smells = [];
|
|
14
|
+
const reportedLines = new Set();
|
|
15
|
+
// Detect console.* calls
|
|
16
|
+
component.path.traverse({
|
|
17
|
+
CallExpression(path) {
|
|
18
|
+
const { callee } = path.node;
|
|
19
|
+
if (t.isMemberExpression(callee) &&
|
|
20
|
+
t.isIdentifier(callee.object) &&
|
|
21
|
+
callee.object.name === 'console') {
|
|
22
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
23
|
+
const debugMethods = ['log', 'warn', 'error', 'debug', 'info', 'trace', 'dir', 'table'];
|
|
24
|
+
if (debugMethods.includes(method)) {
|
|
25
|
+
const loc = path.node.loc;
|
|
26
|
+
const line = loc?.start.line || 0;
|
|
27
|
+
if (!reportedLines.has(line)) {
|
|
28
|
+
reportedLines.add(line);
|
|
29
|
+
smells.push({
|
|
30
|
+
type: 'debug-statement',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
message: `console.${method}() should be removed before production`,
|
|
33
|
+
file: filePath,
|
|
34
|
+
line,
|
|
35
|
+
column: loc?.start.column || 0,
|
|
36
|
+
suggestion: 'Remove console statement or use a logging library with environment-based filtering',
|
|
37
|
+
codeSnippet: getCodeSnippet(sourceCode, line),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
// Detect debugger statements
|
|
44
|
+
DebuggerStatement(path) {
|
|
45
|
+
const loc = path.node.loc;
|
|
46
|
+
smells.push({
|
|
47
|
+
type: 'debug-statement',
|
|
48
|
+
severity: 'error',
|
|
49
|
+
message: 'debugger statement must be removed before production',
|
|
50
|
+
file: filePath,
|
|
51
|
+
line: loc?.start.line || 0,
|
|
52
|
+
column: loc?.start.column || 0,
|
|
53
|
+
suggestion: 'Remove the debugger statement',
|
|
54
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
// Detect TODO/FIXME/HACK comments in source
|
|
59
|
+
const todoPatterns = [
|
|
60
|
+
{ pattern: /\/\/\s*(TODO|FIXME|HACK|XXX|BUG)[:.\s]/gi, type: 'todo-comment' },
|
|
61
|
+
{ pattern: /\/\*\s*(TODO|FIXME|HACK|XXX|BUG)[:.\s]/gi, type: 'todo-comment' },
|
|
62
|
+
];
|
|
63
|
+
const lines = sourceCode.split('\n');
|
|
64
|
+
lines.forEach((line, index) => {
|
|
65
|
+
const lineNum = index + 1;
|
|
66
|
+
// Only check within component bounds
|
|
67
|
+
if (lineNum < component.startLine || lineNum > component.endLine)
|
|
68
|
+
return;
|
|
69
|
+
todoPatterns.forEach(({ pattern }) => {
|
|
70
|
+
const match = line.match(pattern);
|
|
71
|
+
if (match) {
|
|
72
|
+
const tag = match[0].replace(/[/\*\s:]/g, '').toUpperCase();
|
|
73
|
+
smells.push({
|
|
74
|
+
type: 'todo-comment',
|
|
75
|
+
severity: 'info',
|
|
76
|
+
message: `${tag} comment found in "${component.name}"`,
|
|
77
|
+
file: filePath,
|
|
78
|
+
line: lineNum,
|
|
79
|
+
column: 0,
|
|
80
|
+
suggestion: `Address the ${tag} or create a ticket to track it`,
|
|
81
|
+
codeSnippet: getCodeSnippet(sourceCode, lineNum),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
return smells;
|
|
87
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
2
|
+
export interface ImportInfo {
|
|
3
|
+
source: string;
|
|
4
|
+
specifiers: string[];
|
|
5
|
+
line: number;
|
|
6
|
+
isDefault: boolean;
|
|
7
|
+
isNamespace: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ImportAnalysisResult {
|
|
10
|
+
smells: CodeSmell[];
|
|
11
|
+
importGraph: Map<string, string[]>;
|
|
12
|
+
circularDeps: string[][];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Analyze imports across multiple files for issues
|
|
16
|
+
*/
|
|
17
|
+
export declare function analyzeImports(files: string[], rootDir: string, config: DetectorConfig): Promise<ImportAnalysisResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Standalone detection function matching other detector signatures
|
|
20
|
+
*/
|
|
21
|
+
export declare function detectImportIssues(component: any, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
22
|
+
//# sourceMappingURL=imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/detectors/imports.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAK9D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,oBAAoB,CAAC,CAkD/B;AA8KD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAGb"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import _traverse from '@babel/traverse';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
// Handle ESM/CJS interop
|
|
7
|
+
const traverse = typeof _traverse === 'function' ? _traverse : _traverse.default;
|
|
8
|
+
/**
|
|
9
|
+
* Analyze imports across multiple files for issues
|
|
10
|
+
*/
|
|
11
|
+
export async function analyzeImports(files, rootDir, config) {
|
|
12
|
+
const smells = [];
|
|
13
|
+
const importGraph = new Map();
|
|
14
|
+
// Build import graph
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
try {
|
|
17
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
18
|
+
const imports = extractImports(content, file);
|
|
19
|
+
const resolvedImports = [];
|
|
20
|
+
for (const imp of imports) {
|
|
21
|
+
// Skip external packages
|
|
22
|
+
if (!imp.source.startsWith('.') && !imp.source.startsWith('/')) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const resolvedPath = resolveImportPath(file, imp.source, rootDir);
|
|
26
|
+
if (resolvedPath) {
|
|
27
|
+
resolvedImports.push(resolvedPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
importGraph.set(file, resolvedImports);
|
|
31
|
+
// Detect import-related smells in this file
|
|
32
|
+
smells.push(...detectImportSmells(imports, file, content, config));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Skip files that can't be parsed
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Detect circular dependencies
|
|
39
|
+
const circularDeps = detectCircularDependencies(importGraph);
|
|
40
|
+
// Add circular dependency smells
|
|
41
|
+
for (const cycle of circularDeps) {
|
|
42
|
+
const cycleStr = cycle.map(f => path.basename(f)).join(' → ');
|
|
43
|
+
smells.push({
|
|
44
|
+
type: 'circular-dependency',
|
|
45
|
+
severity: 'warning',
|
|
46
|
+
message: `Circular dependency detected: ${cycleStr}`,
|
|
47
|
+
file: cycle[0],
|
|
48
|
+
line: 1,
|
|
49
|
+
column: 0,
|
|
50
|
+
suggestion: 'Refactor to break the cycle. Consider extracting shared logic to a separate module.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { smells, importGraph, circularDeps };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract all imports from a file
|
|
57
|
+
*/
|
|
58
|
+
function extractImports(sourceCode, filePath) {
|
|
59
|
+
const imports = [];
|
|
60
|
+
try {
|
|
61
|
+
const ast = parse(sourceCode, {
|
|
62
|
+
sourceType: 'module',
|
|
63
|
+
plugins: ['jsx', 'typescript'],
|
|
64
|
+
});
|
|
65
|
+
traverse(ast, {
|
|
66
|
+
ImportDeclaration(nodePath) {
|
|
67
|
+
const node = nodePath.node;
|
|
68
|
+
const specifiers = node.specifiers.map(spec => {
|
|
69
|
+
if (t.isImportDefaultSpecifier(spec))
|
|
70
|
+
return 'default';
|
|
71
|
+
if (t.isImportNamespaceSpecifier(spec))
|
|
72
|
+
return '*';
|
|
73
|
+
return spec.local.name;
|
|
74
|
+
});
|
|
75
|
+
imports.push({
|
|
76
|
+
source: node.source.value,
|
|
77
|
+
specifiers,
|
|
78
|
+
line: node.loc?.start.line || 0,
|
|
79
|
+
isDefault: node.specifiers.some(t.isImportDefaultSpecifier),
|
|
80
|
+
isNamespace: node.specifiers.some(t.isImportNamespaceSpecifier),
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Parse error, skip
|
|
87
|
+
}
|
|
88
|
+
return imports;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve an import path relative to the importing file
|
|
92
|
+
*/
|
|
93
|
+
function resolveImportPath(fromFile, importSource, rootDir) {
|
|
94
|
+
if (!importSource.startsWith('.')) {
|
|
95
|
+
return null; // External package
|
|
96
|
+
}
|
|
97
|
+
const dir = path.dirname(fromFile);
|
|
98
|
+
let resolved = path.resolve(dir, importSource);
|
|
99
|
+
// Try common extensions
|
|
100
|
+
const extensions = ['.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js'];
|
|
101
|
+
for (const ext of extensions) {
|
|
102
|
+
const tryPath = resolved + ext;
|
|
103
|
+
// We don't check existence here - just build the graph
|
|
104
|
+
if (!ext.includes('/')) {
|
|
105
|
+
return tryPath;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return resolved;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Detect circular dependencies in import graph using DFS
|
|
112
|
+
*/
|
|
113
|
+
function detectCircularDependencies(graph) {
|
|
114
|
+
const cycles = [];
|
|
115
|
+
const visited = new Set();
|
|
116
|
+
const recursionStack = new Set();
|
|
117
|
+
const path = [];
|
|
118
|
+
function dfs(node) {
|
|
119
|
+
visited.add(node);
|
|
120
|
+
recursionStack.add(node);
|
|
121
|
+
path.push(node);
|
|
122
|
+
const neighbors = graph.get(node) || [];
|
|
123
|
+
for (const neighbor of neighbors) {
|
|
124
|
+
if (!visited.has(neighbor)) {
|
|
125
|
+
dfs(neighbor);
|
|
126
|
+
}
|
|
127
|
+
else if (recursionStack.has(neighbor)) {
|
|
128
|
+
// Found a cycle
|
|
129
|
+
const cycleStart = path.indexOf(neighbor);
|
|
130
|
+
if (cycleStart !== -1) {
|
|
131
|
+
const cycle = path.slice(cycleStart).concat(neighbor);
|
|
132
|
+
// Avoid duplicate cycles
|
|
133
|
+
const cycleKey = [...cycle].sort().join('|');
|
|
134
|
+
if (!cycles.some(c => [...c].sort().join('|') === cycleKey)) {
|
|
135
|
+
cycles.push(cycle);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
path.pop();
|
|
141
|
+
recursionStack.delete(node);
|
|
142
|
+
}
|
|
143
|
+
for (const node of graph.keys()) {
|
|
144
|
+
if (!visited.has(node)) {
|
|
145
|
+
dfs(node);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return cycles;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Detect import-related code smells in a single file
|
|
152
|
+
*/
|
|
153
|
+
function detectImportSmells(imports, filePath, sourceCode, config) {
|
|
154
|
+
const smells = [];
|
|
155
|
+
// Check for barrel file imports (importing from index files)
|
|
156
|
+
for (const imp of imports) {
|
|
157
|
+
if (imp.source.endsWith('/index') || imp.source === '.') {
|
|
158
|
+
smells.push({
|
|
159
|
+
type: 'barrel-file-import',
|
|
160
|
+
severity: 'info',
|
|
161
|
+
message: `Barrel file import from "${imp.source}" may impact tree-shaking`,
|
|
162
|
+
file: filePath,
|
|
163
|
+
line: imp.line,
|
|
164
|
+
column: 0,
|
|
165
|
+
suggestion: 'Import directly from the source file instead of barrel/index files for better build optimization.',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Check for namespace imports (import * as)
|
|
170
|
+
for (const imp of imports) {
|
|
171
|
+
if (imp.isNamespace && !imp.source.startsWith('.')) {
|
|
172
|
+
smells.push({
|
|
173
|
+
type: 'namespace-import',
|
|
174
|
+
severity: 'info',
|
|
175
|
+
message: `Namespace import "* as" from "${imp.source}" may prevent tree-shaking`,
|
|
176
|
+
file: filePath,
|
|
177
|
+
line: imp.line,
|
|
178
|
+
column: 0,
|
|
179
|
+
suggestion: 'Import only the specific exports you need for better bundle size.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Check for too many imports from same source
|
|
184
|
+
const importCounts = new Map();
|
|
185
|
+
for (const imp of imports) {
|
|
186
|
+
const count = (importCounts.get(imp.source) || 0) + imp.specifiers.length;
|
|
187
|
+
importCounts.set(imp.source, count);
|
|
188
|
+
}
|
|
189
|
+
for (const [source, count] of importCounts) {
|
|
190
|
+
if (count > 10 && source.startsWith('.')) {
|
|
191
|
+
smells.push({
|
|
192
|
+
type: 'excessive-imports',
|
|
193
|
+
severity: 'warning',
|
|
194
|
+
message: `${count} imports from "${source}" suggests tight coupling`,
|
|
195
|
+
file: filePath,
|
|
196
|
+
line: imports.find(i => i.source === source)?.line || 1,
|
|
197
|
+
column: 0,
|
|
198
|
+
suggestion: 'Consider if this file has too many responsibilities or if modules should be reorganized.',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return smells;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Standalone detection function matching other detector signatures
|
|
206
|
+
*/
|
|
207
|
+
export function detectImportIssues(component, filePath, sourceCode, config) {
|
|
208
|
+
const imports = extractImports(sourceCode, filePath);
|
|
209
|
+
return detectImportSmells(imports, filePath, sourceCode, config);
|
|
210
|
+
}
|
|
@@ -13,4 +13,10 @@ export { detectReactNativeIssues } from './reactNative.js';
|
|
|
13
13
|
export { detectNodejsIssues } from './nodejs.js';
|
|
14
14
|
export { detectJavascriptIssues } from './javascript.js';
|
|
15
15
|
export { detectTypescriptIssues } from './typescript.js';
|
|
16
|
+
export { detectDebugStatements } from './debug.js';
|
|
17
|
+
export { detectSecurityIssues } from './security.js';
|
|
18
|
+
export { detectAccessibilityIssues } from './accessibility.js';
|
|
19
|
+
export { detectComplexity, calculateComplexityMetrics } from './complexity.js';
|
|
20
|
+
export { detectMemoryLeaks } from './memoryLeak.js';
|
|
21
|
+
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
16
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/detectors/index.js
CHANGED
|
@@ -14,3 +14,11 @@ export { detectReactNativeIssues } from './reactNative.js';
|
|
|
14
14
|
export { detectNodejsIssues } from './nodejs.js';
|
|
15
15
|
export { detectJavascriptIssues } from './javascript.js';
|
|
16
16
|
export { detectTypescriptIssues } from './typescript.js';
|
|
17
|
+
// Debug, Security, Accessibility
|
|
18
|
+
export { detectDebugStatements } from './debug.js';
|
|
19
|
+
export { detectSecurityIssues } from './security.js';
|
|
20
|
+
export { detectAccessibilityIssues } from './accessibility.js';
|
|
21
|
+
// Complexity, Memory Leaks, Imports
|
|
22
|
+
export { detectComplexity, calculateComplexityMetrics } from './complexity.js';
|
|
23
|
+
export { detectMemoryLeaks } from './memoryLeak.js';
|
|
24
|
+
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect potential memory leaks in React components
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectMemoryLeaks(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
7
|
+
//# sourceMappingURL=memoryLeak.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memoryLeak.d.ts","sourceRoot":"","sources":["../../src/detectors/memoryLeak.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAgEb"}
|