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,111 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* Detect potential memory leaks in React components
|
|
4
|
+
*/
|
|
5
|
+
export function detectMemoryLeaks(component, filePath, sourceCode, config) {
|
|
6
|
+
if (!config.checkMemoryLeaks)
|
|
7
|
+
return [];
|
|
8
|
+
const smells = [];
|
|
9
|
+
// Check for setInterval without cleanup
|
|
10
|
+
component.path.traverse({
|
|
11
|
+
CallExpression(path) {
|
|
12
|
+
const node = path.node;
|
|
13
|
+
const { callee } = node;
|
|
14
|
+
if (t.isIdentifier(callee) && callee.name === 'setInterval') {
|
|
15
|
+
// Check if result is stored
|
|
16
|
+
const parent = path.parentPath;
|
|
17
|
+
if (!parent || !t.isVariableDeclarator(parent.node)) {
|
|
18
|
+
const loc = node.loc;
|
|
19
|
+
smells.push({
|
|
20
|
+
type: 'memory-leak-timer',
|
|
21
|
+
severity: 'error',
|
|
22
|
+
message: `setInterval without storing ID for cleanup in "${component.name}"`,
|
|
23
|
+
file: filePath,
|
|
24
|
+
line: loc?.start.line || 0,
|
|
25
|
+
column: loc?.start.column || 0,
|
|
26
|
+
suggestion: 'Store the interval ID and call clearInterval in cleanup.',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
// Check useEffect hooks for cleanup issues
|
|
33
|
+
for (const effect of component.hooks.useEffect) {
|
|
34
|
+
const hasCleanup = checkEffectHasCleanup(effect);
|
|
35
|
+
const hasSubscription = checkEffectHasSubscription(effect, component);
|
|
36
|
+
const hasEventListener = checkEffectHasEventListener(effect, component);
|
|
37
|
+
if (hasEventListener && !hasCleanup) {
|
|
38
|
+
const loc = effect.loc;
|
|
39
|
+
smells.push({
|
|
40
|
+
type: 'memory-leak-event-listener',
|
|
41
|
+
severity: 'error',
|
|
42
|
+
message: `useEffect adds event listener without cleanup in "${component.name}"`,
|
|
43
|
+
file: filePath,
|
|
44
|
+
line: loc?.start.line || 0,
|
|
45
|
+
column: 0,
|
|
46
|
+
suggestion: 'Return a cleanup function that calls removeEventListener.',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (hasSubscription && !hasCleanup) {
|
|
50
|
+
const loc = effect.loc;
|
|
51
|
+
smells.push({
|
|
52
|
+
type: 'memory-leak-subscription',
|
|
53
|
+
severity: 'error',
|
|
54
|
+
message: `useEffect creates subscription without cleanup in "${component.name}"`,
|
|
55
|
+
file: filePath,
|
|
56
|
+
line: loc?.start.line || 0,
|
|
57
|
+
column: 0,
|
|
58
|
+
suggestion: 'Return a cleanup function that unsubscribes.',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return smells;
|
|
63
|
+
}
|
|
64
|
+
function checkEffectHasCleanup(effect) {
|
|
65
|
+
const args = effect.arguments;
|
|
66
|
+
if (args.length === 0)
|
|
67
|
+
return false;
|
|
68
|
+
const callback = args[0];
|
|
69
|
+
if (!t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Check if callback body has a return statement
|
|
73
|
+
if (t.isBlockStatement(callback.body)) {
|
|
74
|
+
for (const stmt of callback.body.body) {
|
|
75
|
+
if (t.isReturnStatement(stmt) && stmt.argument) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function checkEffectHasEventListener(effect, component) {
|
|
83
|
+
let hasListener = false;
|
|
84
|
+
const args = effect.arguments;
|
|
85
|
+
if (args.length === 0)
|
|
86
|
+
return false;
|
|
87
|
+
const callback = args[0];
|
|
88
|
+
if (!t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (t.isBlockStatement(callback.body)) {
|
|
92
|
+
const sourceCode = JSON.stringify(callback.body);
|
|
93
|
+
hasListener = sourceCode.includes('addEventListener');
|
|
94
|
+
}
|
|
95
|
+
return hasListener;
|
|
96
|
+
}
|
|
97
|
+
function checkEffectHasSubscription(effect, component) {
|
|
98
|
+
let hasSubscription = false;
|
|
99
|
+
const args = effect.arguments;
|
|
100
|
+
if (args.length === 0)
|
|
101
|
+
return false;
|
|
102
|
+
const callback = args[0];
|
|
103
|
+
if (!t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (t.isBlockStatement(callback.body)) {
|
|
107
|
+
const sourceCode = JSON.stringify(callback.body);
|
|
108
|
+
hasSubscription = sourceCode.includes('subscribe') || sourceCode.includes('addListener');
|
|
109
|
+
}
|
|
110
|
+
return hasSubscription;
|
|
111
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects security vulnerabilities:
|
|
5
|
+
* - dangerouslySetInnerHTML usage
|
|
6
|
+
* - eval() and Function() constructor
|
|
7
|
+
* - innerHTML assignments
|
|
8
|
+
* - Unsafe URLs (javascript:, data:)
|
|
9
|
+
* - Exposed secrets/API keys
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectSecurityIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
|
|
12
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/detectors/security.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAiKb"}
|
|
@@ -0,0 +1,161 @@
|
|
|
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 security vulnerabilities:
|
|
6
|
+
* - dangerouslySetInnerHTML usage
|
|
7
|
+
* - eval() and Function() constructor
|
|
8
|
+
* - innerHTML assignments
|
|
9
|
+
* - Unsafe URLs (javascript:, data:)
|
|
10
|
+
* - Exposed secrets/API keys
|
|
11
|
+
*/
|
|
12
|
+
export function detectSecurityIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
|
|
13
|
+
if (!config.checkSecurity)
|
|
14
|
+
return [];
|
|
15
|
+
const smells = [];
|
|
16
|
+
// Detect dangerouslySetInnerHTML
|
|
17
|
+
component.path.traverse({
|
|
18
|
+
JSXAttribute(path) {
|
|
19
|
+
if (t.isJSXIdentifier(path.node.name) &&
|
|
20
|
+
path.node.name.name === 'dangerouslySetInnerHTML') {
|
|
21
|
+
const loc = path.node.loc;
|
|
22
|
+
smells.push({
|
|
23
|
+
type: 'security-xss',
|
|
24
|
+
severity: 'error',
|
|
25
|
+
message: `dangerouslySetInnerHTML is a security risk in "${component.name}"`,
|
|
26
|
+
file: filePath,
|
|
27
|
+
line: loc?.start.line || 0,
|
|
28
|
+
column: loc?.start.column || 0,
|
|
29
|
+
suggestion: 'Sanitize HTML with DOMPurify or use a safe alternative like converting to React elements',
|
|
30
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
// Detect eval() and Function() constructor
|
|
36
|
+
component.path.traverse({
|
|
37
|
+
CallExpression(path) {
|
|
38
|
+
const { callee } = path.node;
|
|
39
|
+
// eval()
|
|
40
|
+
if (t.isIdentifier(callee) && callee.name === 'eval') {
|
|
41
|
+
const loc = path.node.loc;
|
|
42
|
+
smells.push({
|
|
43
|
+
type: 'security-eval',
|
|
44
|
+
severity: 'error',
|
|
45
|
+
message: `eval() is a critical security risk in "${component.name}"`,
|
|
46
|
+
file: filePath,
|
|
47
|
+
line: loc?.start.line || 0,
|
|
48
|
+
column: loc?.start.column || 0,
|
|
49
|
+
suggestion: 'Never use eval(). Parse JSON with JSON.parse() or restructure logic to avoid dynamic code execution.',
|
|
50
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
// new Function()
|
|
55
|
+
NewExpression(path) {
|
|
56
|
+
const { callee } = path.node;
|
|
57
|
+
if (t.isIdentifier(callee) && callee.name === 'Function') {
|
|
58
|
+
const loc = path.node.loc;
|
|
59
|
+
smells.push({
|
|
60
|
+
type: 'security-eval',
|
|
61
|
+
severity: 'error',
|
|
62
|
+
message: `new Function() is equivalent to eval() and is a security risk`,
|
|
63
|
+
file: filePath,
|
|
64
|
+
line: loc?.start.line || 0,
|
|
65
|
+
column: loc?.start.column || 0,
|
|
66
|
+
suggestion: 'Avoid creating functions from strings. Restructure to use static function definitions.',
|
|
67
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
// Detect innerHTML assignments
|
|
73
|
+
component.path.traverse({
|
|
74
|
+
AssignmentExpression(path) {
|
|
75
|
+
const { left } = path.node;
|
|
76
|
+
if (t.isMemberExpression(left) && t.isIdentifier(left.property)) {
|
|
77
|
+
if (left.property.name === 'innerHTML' || left.property.name === 'outerHTML') {
|
|
78
|
+
const loc = path.node.loc;
|
|
79
|
+
smells.push({
|
|
80
|
+
type: 'security-xss',
|
|
81
|
+
severity: 'warning',
|
|
82
|
+
message: `Direct ${left.property.name} assignment can lead to XSS`,
|
|
83
|
+
file: filePath,
|
|
84
|
+
line: loc?.start.line || 0,
|
|
85
|
+
column: loc?.start.column || 0,
|
|
86
|
+
suggestion: 'Use textContent for plain text, or sanitize HTML with DOMPurify',
|
|
87
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
// Detect unsafe URLs (javascript:, data:)
|
|
94
|
+
component.path.traverse({
|
|
95
|
+
JSXAttribute(path) {
|
|
96
|
+
if (!t.isJSXIdentifier(path.node.name))
|
|
97
|
+
return;
|
|
98
|
+
const propName = path.node.name.name;
|
|
99
|
+
if (!['href', 'src', 'action'].includes(propName))
|
|
100
|
+
return;
|
|
101
|
+
const value = path.node.value;
|
|
102
|
+
if (t.isStringLiteral(value)) {
|
|
103
|
+
const url = value.value.toLowerCase().trim();
|
|
104
|
+
if (url.startsWith('javascript:')) {
|
|
105
|
+
const loc = path.node.loc;
|
|
106
|
+
smells.push({
|
|
107
|
+
type: 'security-xss',
|
|
108
|
+
severity: 'error',
|
|
109
|
+
message: `javascript: URLs are a security risk`,
|
|
110
|
+
file: filePath,
|
|
111
|
+
line: loc?.start.line || 0,
|
|
112
|
+
column: loc?.start.column || 0,
|
|
113
|
+
suggestion: 'Use onClick handlers instead of javascript: URLs',
|
|
114
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (url.startsWith('data:') && propName === 'href') {
|
|
118
|
+
const loc = path.node.loc;
|
|
119
|
+
smells.push({
|
|
120
|
+
type: 'security-xss',
|
|
121
|
+
severity: 'warning',
|
|
122
|
+
message: `data: URLs in href can be a security risk`,
|
|
123
|
+
file: filePath,
|
|
124
|
+
line: loc?.start.line || 0,
|
|
125
|
+
column: loc?.start.column || 0,
|
|
126
|
+
suggestion: 'Validate and sanitize data URLs, or use blob URLs instead',
|
|
127
|
+
codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
// Detect potential exposed secrets
|
|
134
|
+
const secretPatterns = [
|
|
135
|
+
{ pattern: /['"](?:sk[-_]live|pk[-_]live|api[-_]?key|secret[-_]?key|access[-_]?token|auth[-_]?token)['"]\s*[:=]\s*['"][a-zA-Z0-9-_]{20,}/i, name: 'API key' },
|
|
136
|
+
{ pattern: /['"](?:ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{36,}['"]/i, name: 'GitHub token' },
|
|
137
|
+
{ pattern: /['"]AKIA[A-Z0-9]{16}['"]/i, name: 'AWS access key' },
|
|
138
|
+
{ pattern: /password\s*[:=]\s*['"][^'"]{8,}['"]/i, name: 'Hardcoded password' },
|
|
139
|
+
];
|
|
140
|
+
const lines = sourceCode.split('\n');
|
|
141
|
+
lines.forEach((line, index) => {
|
|
142
|
+
const lineNum = index + 1;
|
|
143
|
+
if (lineNum < component.startLine || lineNum > component.endLine)
|
|
144
|
+
return;
|
|
145
|
+
secretPatterns.forEach(({ pattern, name }) => {
|
|
146
|
+
if (pattern.test(line)) {
|
|
147
|
+
smells.push({
|
|
148
|
+
type: 'security-secrets',
|
|
149
|
+
severity: 'error',
|
|
150
|
+
message: `Potential ${name} exposed in code`,
|
|
151
|
+
file: filePath,
|
|
152
|
+
line: lineNum,
|
|
153
|
+
column: 0,
|
|
154
|
+
suggestion: 'Move secrets to environment variables (.env) and never commit them to version control',
|
|
155
|
+
codeSnippet: getCodeSnippet(sourceCode, lineNum),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
return smells;
|
|
161
|
+
}
|
package/dist/fixer.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CodeSmell, SmellType } from './types/index.js';
|
|
2
|
+
export interface FixResult {
|
|
3
|
+
file: string;
|
|
4
|
+
fixedSmells: CodeSmell[];
|
|
5
|
+
skippedSmells: CodeSmell[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Check if a smell type can be auto-fixed
|
|
9
|
+
*/
|
|
10
|
+
export declare function isFixable(smell: CodeSmell): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Apply fixes to a file based on detected smells
|
|
13
|
+
*/
|
|
14
|
+
export declare function fixFile(filePath: string, smells: CodeSmell[]): Promise<FixResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Get list of fixable smell types
|
|
17
|
+
*/
|
|
18
|
+
export declare function getFixableTypes(): SmellType[];
|
|
19
|
+
/**
|
|
20
|
+
* Describe what fix will be applied for a smell type
|
|
21
|
+
*/
|
|
22
|
+
export declare function describeFixAction(type: SmellType): string;
|
|
23
|
+
//# sourceMappingURL=fixer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixer.d.ts","sourceRoot":"","sources":["../src/fixer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,SAAS,EAAE,CAAC;IACzB,aAAa,EAAE,SAAS,EAAE,CAAC;CAC5B;AAUD;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAEnD;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAuCvF;AAyED;;GAEG;AACH,wBAAgB,eAAe,IAAI,SAAS,EAAE,CAE7C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAazD"}
|
package/dist/fixer.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
// Smell types that can be auto-fixed
|
|
3
|
+
const FIXABLE_TYPES = [
|
|
4
|
+
'debug-statement',
|
|
5
|
+
'js-var-usage',
|
|
6
|
+
'js-loose-equality',
|
|
7
|
+
'a11y-missing-alt',
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* Check if a smell type can be auto-fixed
|
|
11
|
+
*/
|
|
12
|
+
export function isFixable(smell) {
|
|
13
|
+
return FIXABLE_TYPES.includes(smell.type);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Apply fixes to a file based on detected smells
|
|
17
|
+
*/
|
|
18
|
+
export async function fixFile(filePath, smells) {
|
|
19
|
+
const fixableSmells = smells.filter(isFixable);
|
|
20
|
+
const skippedSmells = smells.filter(s => !isFixable(s));
|
|
21
|
+
if (fixableSmells.length === 0) {
|
|
22
|
+
return { file: filePath, fixedSmells: [], skippedSmells };
|
|
23
|
+
}
|
|
24
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
25
|
+
const lines = content.split('\n');
|
|
26
|
+
const fixedSmells = [];
|
|
27
|
+
// Sort by line descending to fix from bottom up (preserves line numbers)
|
|
28
|
+
const sortedSmells = [...fixableSmells].sort((a, b) => b.line - a.line);
|
|
29
|
+
for (const smell of sortedSmells) {
|
|
30
|
+
const lineIndex = smell.line - 1;
|
|
31
|
+
if (lineIndex < 0 || lineIndex >= lines.length)
|
|
32
|
+
continue;
|
|
33
|
+
const originalLine = lines[lineIndex];
|
|
34
|
+
const fixedLine = applyFix(smell, originalLine);
|
|
35
|
+
if (fixedLine !== originalLine) {
|
|
36
|
+
if (fixedLine === null) {
|
|
37
|
+
// Remove the line entirely
|
|
38
|
+
lines.splice(lineIndex, 1);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
lines[lineIndex] = fixedLine;
|
|
42
|
+
}
|
|
43
|
+
fixedSmells.push(smell);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Write back if any fixes were applied
|
|
47
|
+
if (fixedSmells.length > 0) {
|
|
48
|
+
await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
return { file: filePath, fixedSmells, skippedSmells };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Apply a specific fix to a line of code
|
|
54
|
+
* Returns the fixed line, or null to remove the line
|
|
55
|
+
*/
|
|
56
|
+
function applyFix(smell, line) {
|
|
57
|
+
switch (smell.type) {
|
|
58
|
+
case 'debug-statement':
|
|
59
|
+
return fixDebugStatement(line);
|
|
60
|
+
case 'js-var-usage':
|
|
61
|
+
return fixVarUsage(line);
|
|
62
|
+
case 'js-loose-equality':
|
|
63
|
+
return fixLooseEquality(line);
|
|
64
|
+
case 'a11y-missing-alt':
|
|
65
|
+
return fixMissingAlt(line);
|
|
66
|
+
default:
|
|
67
|
+
return line;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Remove console.log, console.debug, debugger statements
|
|
72
|
+
*/
|
|
73
|
+
function fixDebugStatement(line) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
// If line is just a console statement or debugger, remove it
|
|
76
|
+
if (/^console\.(log|debug|info|warn|error|trace|dir)\s*\(.*\);?\s*$/.test(trimmed)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (/^debugger;?\s*$/.test(trimmed)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
// If console is part of a larger expression, comment it out
|
|
83
|
+
if (/console\.(log|debug|info|warn|error|trace|dir)\s*\(/.test(line)) {
|
|
84
|
+
return line.replace(/console\.(log|debug|info|warn|error|trace|dir)\s*\([^)]*\);?/g, '/* $& */');
|
|
85
|
+
}
|
|
86
|
+
return line;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Replace var with let or const
|
|
90
|
+
*/
|
|
91
|
+
function fixVarUsage(line) {
|
|
92
|
+
// Simple heuristic: if reassigned later, use let; otherwise const
|
|
93
|
+
// For auto-fix, default to let (safer)
|
|
94
|
+
return line.replace(/\bvar\s+/, 'let ');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Replace == with === and != with !==
|
|
98
|
+
*/
|
|
99
|
+
function fixLooseEquality(line) {
|
|
100
|
+
return line
|
|
101
|
+
.replace(/([^=!])={2}([^=])/g, '$1===$2')
|
|
102
|
+
.replace(/!={1}([^=])/g, '!==$1');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Add alt="" to img tags missing alt attribute
|
|
106
|
+
*/
|
|
107
|
+
function fixMissingAlt(line) {
|
|
108
|
+
// Add alt="" to <img> tags without alt
|
|
109
|
+
return line.replace(/<img\s+(?![^>]*\balt\b)([^>]*?)(\/?>)/gi, '<img $1alt="" $2');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get list of fixable smell types
|
|
113
|
+
*/
|
|
114
|
+
export function getFixableTypes() {
|
|
115
|
+
return [...FIXABLE_TYPES];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Describe what fix will be applied for a smell type
|
|
119
|
+
*/
|
|
120
|
+
export function describeFixAction(type) {
|
|
121
|
+
switch (type) {
|
|
122
|
+
case 'debug-statement':
|
|
123
|
+
return 'Remove console.log/debugger statements';
|
|
124
|
+
case 'js-var-usage':
|
|
125
|
+
return 'Replace var with let';
|
|
126
|
+
case 'js-loose-equality':
|
|
127
|
+
return 'Replace == with === and != with !==';
|
|
128
|
+
case 'a11y-missing-alt':
|
|
129
|
+
return 'Add alt="" to images';
|
|
130
|
+
default:
|
|
131
|
+
return 'Not auto-fixable';
|
|
132
|
+
}
|
|
133
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface GitInfo {
|
|
2
|
+
isGitRepo: boolean;
|
|
3
|
+
currentBranch?: string;
|
|
4
|
+
changedFiles: string[];
|
|
5
|
+
stagedFiles: string[];
|
|
6
|
+
untrackedFiles: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Get git information for a directory
|
|
10
|
+
*/
|
|
11
|
+
export declare function getGitInfo(rootDir: string): GitInfo;
|
|
12
|
+
/**
|
|
13
|
+
* Get files changed since a specific commit or branch
|
|
14
|
+
*/
|
|
15
|
+
export declare function getFilesSince(rootDir: string, ref: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Get files changed in the last N commits
|
|
18
|
+
*/
|
|
19
|
+
export declare function getFilesFromLastCommits(rootDir: string, count?: number): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Get all modified files (changed + staged + untracked)
|
|
22
|
+
*/
|
|
23
|
+
export declare function getAllModifiedFiles(rootDir: string): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Filter file list to only include React/JS/TS files
|
|
26
|
+
*/
|
|
27
|
+
export declare function filterReactFiles(files: string[]): string[];
|
|
28
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CA0DnD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAcpE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,MAAM,EAAE,CAcpF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAW7D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAG1D"}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Get git information for a directory
|
|
5
|
+
*/
|
|
6
|
+
export function getGitInfo(rootDir) {
|
|
7
|
+
try {
|
|
8
|
+
// Check if it's a git repo
|
|
9
|
+
execSync('git rev-parse --is-inside-work-tree', {
|
|
10
|
+
cwd: rootDir,
|
|
11
|
+
stdio: 'pipe',
|
|
12
|
+
});
|
|
13
|
+
const currentBranch = execSync('git branch --show-current', {
|
|
14
|
+
cwd: rootDir,
|
|
15
|
+
encoding: 'utf-8',
|
|
16
|
+
}).trim();
|
|
17
|
+
// Get changed files (modified but not staged)
|
|
18
|
+
const changedOutput = execSync('git diff --name-only', {
|
|
19
|
+
cwd: rootDir,
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
});
|
|
22
|
+
const changedFiles = changedOutput
|
|
23
|
+
.split('\n')
|
|
24
|
+
.filter(f => f.trim())
|
|
25
|
+
.map(f => path.resolve(rootDir, f));
|
|
26
|
+
// Get staged files
|
|
27
|
+
const stagedOutput = execSync('git diff --cached --name-only', {
|
|
28
|
+
cwd: rootDir,
|
|
29
|
+
encoding: 'utf-8',
|
|
30
|
+
});
|
|
31
|
+
const stagedFiles = stagedOutput
|
|
32
|
+
.split('\n')
|
|
33
|
+
.filter(f => f.trim())
|
|
34
|
+
.map(f => path.resolve(rootDir, f));
|
|
35
|
+
// Get untracked files
|
|
36
|
+
const untrackedOutput = execSync('git ls-files --others --exclude-standard', {
|
|
37
|
+
cwd: rootDir,
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
});
|
|
40
|
+
const untrackedFiles = untrackedOutput
|
|
41
|
+
.split('\n')
|
|
42
|
+
.filter(f => f.trim())
|
|
43
|
+
.map(f => path.resolve(rootDir, f));
|
|
44
|
+
return {
|
|
45
|
+
isGitRepo: true,
|
|
46
|
+
currentBranch,
|
|
47
|
+
changedFiles,
|
|
48
|
+
stagedFiles,
|
|
49
|
+
untrackedFiles,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {
|
|
54
|
+
isGitRepo: false,
|
|
55
|
+
changedFiles: [],
|
|
56
|
+
stagedFiles: [],
|
|
57
|
+
untrackedFiles: [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get files changed since a specific commit or branch
|
|
63
|
+
*/
|
|
64
|
+
export function getFilesSince(rootDir, ref) {
|
|
65
|
+
try {
|
|
66
|
+
const output = execSync(`git diff --name-only ${ref}`, {
|
|
67
|
+
cwd: rootDir,
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
});
|
|
70
|
+
return output
|
|
71
|
+
.split('\n')
|
|
72
|
+
.filter(f => f.trim())
|
|
73
|
+
.map(f => path.resolve(rootDir, f));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get files changed in the last N commits
|
|
81
|
+
*/
|
|
82
|
+
export function getFilesFromLastCommits(rootDir, count = 1) {
|
|
83
|
+
try {
|
|
84
|
+
const output = execSync(`git diff --name-only HEAD~${count}`, {
|
|
85
|
+
cwd: rootDir,
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
});
|
|
88
|
+
return output
|
|
89
|
+
.split('\n')
|
|
90
|
+
.filter(f => f.trim())
|
|
91
|
+
.map(f => path.resolve(rootDir, f));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get all modified files (changed + staged + untracked)
|
|
99
|
+
*/
|
|
100
|
+
export function getAllModifiedFiles(rootDir) {
|
|
101
|
+
const info = getGitInfo(rootDir);
|
|
102
|
+
if (!info.isGitRepo)
|
|
103
|
+
return [];
|
|
104
|
+
const allFiles = new Set([
|
|
105
|
+
...info.changedFiles,
|
|
106
|
+
...info.stagedFiles,
|
|
107
|
+
...info.untrackedFiles,
|
|
108
|
+
]);
|
|
109
|
+
return Array.from(allFiles);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Filter file list to only include React/JS/TS files
|
|
113
|
+
*/
|
|
114
|
+
export function filterReactFiles(files) {
|
|
115
|
+
const extensions = ['.tsx', '.jsx', '.ts', '.js'];
|
|
116
|
+
return files.filter(f => extensions.some(ext => f.endsWith(ext)));
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"htmlReporter.d.ts","sourceRoot":"","sources":["../src/htmlReporter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAiB,MAAM,kBAAkB,CAAC;AAGjE;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA8YlF"}
|