react-code-smell-detector 1.2.0 → 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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +18 -1
  5. package/dist/cli.js +93 -26
  6. package/dist/detectors/complexity.d.ts +17 -0
  7. package/dist/detectors/complexity.d.ts.map +1 -0
  8. package/dist/detectors/complexity.js +69 -0
  9. package/dist/detectors/imports.d.ts +22 -0
  10. package/dist/detectors/imports.d.ts.map +1 -0
  11. package/dist/detectors/imports.js +210 -0
  12. package/dist/detectors/index.d.ts +3 -0
  13. package/dist/detectors/index.d.ts.map +1 -1
  14. package/dist/detectors/index.js +4 -0
  15. package/dist/detectors/memoryLeak.d.ts +7 -0
  16. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  17. package/dist/detectors/memoryLeak.js +111 -0
  18. package/dist/fixer.d.ts +23 -0
  19. package/dist/fixer.d.ts.map +1 -0
  20. package/dist/fixer.js +133 -0
  21. package/dist/git.d.ts +28 -0
  22. package/dist/git.d.ts.map +1 -0
  23. package/dist/git.js +117 -0
  24. package/dist/reporter.js +13 -0
  25. package/dist/types/index.d.ts +7 -1
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/index.js +9 -0
  28. package/dist/watcher.d.ts +16 -0
  29. package/dist/watcher.d.ts.map +1 -0
  30. package/dist/watcher.js +89 -0
  31. package/package.json +8 -2
  32. package/src/analyzer.ts +0 -324
  33. package/src/cli.ts +0 -159
  34. package/src/detectors/accessibility.ts +0 -212
  35. package/src/detectors/deadCode.ts +0 -163
  36. package/src/detectors/debug.ts +0 -103
  37. package/src/detectors/dependencyArray.ts +0 -176
  38. package/src/detectors/hooksRules.ts +0 -101
  39. package/src/detectors/index.ts +0 -20
  40. package/src/detectors/javascript.ts +0 -169
  41. package/src/detectors/largeComponent.ts +0 -63
  42. package/src/detectors/magicValues.ts +0 -114
  43. package/src/detectors/memoization.ts +0 -177
  44. package/src/detectors/missingKey.ts +0 -105
  45. package/src/detectors/nestedTernary.ts +0 -75
  46. package/src/detectors/nextjs.ts +0 -124
  47. package/src/detectors/nodejs.ts +0 -199
  48. package/src/detectors/propDrilling.ts +0 -103
  49. package/src/detectors/reactNative.ts +0 -154
  50. package/src/detectors/security.ts +0 -179
  51. package/src/detectors/typescript.ts +0 -151
  52. package/src/detectors/useEffect.ts +0 -117
  53. package/src/htmlReporter.ts +0 -464
  54. package/src/index.ts +0 -4
  55. package/src/parser/index.ts +0 -195
  56. package/src/reporter.ts +0 -291
  57. package/src/types/index.ts +0 -165
  58. package/tsconfig.json +0 -19
@@ -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
- }
@@ -1,117 +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
- export function detectUseEffectOveruse(
6
- component: ParsedComponent,
7
- filePath: string,
8
- sourceCode: string,
9
- config: DetectorConfig = DEFAULT_CONFIG
10
- ): CodeSmell[] {
11
- const smells: CodeSmell[] = [];
12
- const { useEffect } = component.hooks;
13
-
14
- // Check for too many useEffects
15
- if (useEffect.length > config.maxUseEffectsPerComponent) {
16
- smells.push({
17
- type: 'useEffect-overuse',
18
- severity: 'warning',
19
- message: `Component "${component.name}" has ${useEffect.length} useEffect hooks (max recommended: ${config.maxUseEffectsPerComponent})`,
20
- file: filePath,
21
- line: component.startLine,
22
- column: 0,
23
- suggestion: 'Consider extracting logic into custom hooks or combining related effects',
24
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
25
- });
26
- }
27
-
28
- // Check each useEffect for issues
29
- useEffect.forEach((effect, index) => {
30
- const loc = effect.loc;
31
- if (!loc) return;
32
-
33
- // Check for empty dependency array with async operations
34
- const deps = effect.arguments[1];
35
- const callback = effect.arguments[0];
36
-
37
- if (t.isArrayExpression(deps) && deps.elements.length === 0) {
38
- // Empty dependency array - check if it's justified
39
- let hasAsyncOperation = false;
40
- let hasStateUpdate = false;
41
-
42
- if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
43
- const body = callback.body;
44
- const checkNode = (node: t.Node) => {
45
- if (t.isAwaitExpression(node)) hasAsyncOperation = true;
46
- if (t.isCallExpression(node)) {
47
- const callee = node.callee;
48
- if (t.isIdentifier(callee) && callee.name.startsWith('set')) {
49
- hasStateUpdate = true;
50
- }
51
- }
52
- };
53
-
54
- if (t.isBlockStatement(body)) {
55
- body.body.forEach(stmt => {
56
- t.traverseFast(stmt, checkNode);
57
- });
58
- }
59
- }
60
-
61
- if (hasAsyncOperation && hasStateUpdate) {
62
- smells.push({
63
- type: 'useEffect-overuse',
64
- severity: 'info',
65
- message: `useEffect #${index + 1} in "${component.name}" has empty deps but contains async state updates`,
66
- file: filePath,
67
- line: loc.start.line,
68
- column: loc.start.column,
69
- suggestion: 'Consider using React Query, SWR, or a custom hook for data fetching',
70
- codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
71
- });
72
- }
73
- }
74
-
75
- // Check for missing cleanup
76
- if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
77
- let hasSubscription = false;
78
- let hasCleanup = false;
79
-
80
- const checkSubscription = (node: t.Node) => {
81
- if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
82
- const prop = node.callee.property;
83
- if (t.isIdentifier(prop)) {
84
- if (['addEventListener', 'subscribe', 'on', 'setInterval', 'setTimeout'].includes(prop.name)) {
85
- hasSubscription = true;
86
- }
87
- }
88
- }
89
- };
90
-
91
- const body = callback.body;
92
- if (t.isBlockStatement(body)) {
93
- body.body.forEach(stmt => {
94
- t.traverseFast(stmt, checkSubscription);
95
- if (t.isReturnStatement(stmt) && stmt.argument) {
96
- hasCleanup = true;
97
- }
98
- });
99
- }
100
-
101
- if (hasSubscription && !hasCleanup) {
102
- smells.push({
103
- type: 'useEffect-overuse',
104
- severity: 'error',
105
- message: `useEffect in "${component.name}" sets up subscription but has no cleanup function`,
106
- file: filePath,
107
- line: loc.start.line,
108
- column: loc.start.column,
109
- suggestion: 'Add a cleanup function to remove event listeners/subscriptions',
110
- codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
111
- });
112
- }
113
- }
114
- });
115
-
116
- return smells;
117
- }