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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -4
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +22 -1
  5. package/dist/baseline.d.ts +37 -0
  6. package/dist/baseline.d.ts.map +1 -0
  7. package/dist/baseline.js +112 -0
  8. package/dist/cli.js +125 -26
  9. package/dist/detectors/complexity.d.ts +17 -0
  10. package/dist/detectors/complexity.d.ts.map +1 -0
  11. package/dist/detectors/complexity.js +69 -0
  12. package/dist/detectors/imports.d.ts +22 -0
  13. package/dist/detectors/imports.d.ts.map +1 -0
  14. package/dist/detectors/imports.js +210 -0
  15. package/dist/detectors/index.d.ts +4 -0
  16. package/dist/detectors/index.d.ts.map +1 -1
  17. package/dist/detectors/index.js +5 -0
  18. package/dist/detectors/memoryLeak.d.ts +7 -0
  19. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  20. package/dist/detectors/memoryLeak.js +111 -0
  21. package/dist/detectors/unusedCode.d.ts +7 -0
  22. package/dist/detectors/unusedCode.d.ts.map +1 -0
  23. package/dist/detectors/unusedCode.js +78 -0
  24. package/dist/fixer.d.ts +23 -0
  25. package/dist/fixer.d.ts.map +1 -0
  26. package/dist/fixer.js +133 -0
  27. package/dist/git.d.ts +31 -0
  28. package/dist/git.d.ts.map +1 -0
  29. package/dist/git.js +137 -0
  30. package/dist/reporter.js +16 -0
  31. package/dist/types/index.d.ts +13 -1
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/index.js +18 -0
  34. package/dist/watcher.d.ts +16 -0
  35. package/dist/watcher.d.ts.map +1 -0
  36. package/dist/watcher.js +89 -0
  37. package/dist/webhooks.d.ts +20 -0
  38. package/dist/webhooks.d.ts.map +1 -0
  39. package/dist/webhooks.js +199 -0
  40. package/package.json +10 -2
  41. package/src/analyzer.ts +0 -324
  42. package/src/cli.ts +0 -159
  43. package/src/detectors/accessibility.ts +0 -212
  44. package/src/detectors/deadCode.ts +0 -163
  45. package/src/detectors/debug.ts +0 -103
  46. package/src/detectors/dependencyArray.ts +0 -176
  47. package/src/detectors/hooksRules.ts +0 -101
  48. package/src/detectors/index.ts +0 -20
  49. package/src/detectors/javascript.ts +0 -169
  50. package/src/detectors/largeComponent.ts +0 -63
  51. package/src/detectors/magicValues.ts +0 -114
  52. package/src/detectors/memoization.ts +0 -177
  53. package/src/detectors/missingKey.ts +0 -105
  54. package/src/detectors/nestedTernary.ts +0 -75
  55. package/src/detectors/nextjs.ts +0 -124
  56. package/src/detectors/nodejs.ts +0 -199
  57. package/src/detectors/propDrilling.ts +0 -103
  58. package/src/detectors/reactNative.ts +0 -154
  59. package/src/detectors/security.ts +0 -179
  60. package/src/detectors/typescript.ts +0 -151
  61. package/src/detectors/useEffect.ts +0 -117
  62. package/src/htmlReporter.ts +0 -464
  63. package/src/index.ts +0 -4
  64. package/src/parser/index.ts +0 -195
  65. package/src/reporter.ts +0 -291
  66. package/src/types/index.ts +0 -165
  67. package/tsconfig.json +0 -19
@@ -1,176 +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 potential issues with useEffect dependency arrays
7
- */
8
- export function detectDependencyArrayIssues(
9
- component: ParsedComponent,
10
- filePath: string,
11
- sourceCode: string,
12
- config: DetectorConfig = DEFAULT_CONFIG
13
- ): CodeSmell[] {
14
- if (!config.checkDependencyArrays) return [];
15
-
16
- const smells: CodeSmell[] = [];
17
-
18
- // Analyze each useEffect
19
- component.hooks.useEffect.forEach(effectCall => {
20
- const args = effectCall.arguments;
21
- const callback = args[0];
22
- const depsArray = args[1];
23
-
24
- const loc = effectCall.loc;
25
- const line = loc?.start.line || 0;
26
-
27
- // Check 1: Missing dependency array entirely
28
- if (!depsArray) {
29
- smells.push({
30
- type: 'dependency-array-issue',
31
- severity: 'warning',
32
- message: `useEffect without dependency array in "${component.name}" runs on every render`,
33
- file: filePath,
34
- line,
35
- column: loc?.start.column || 0,
36
- suggestion: 'Add a dependency array. Use [] for mount-only effect, or [dep1, dep2] for reactive dependencies.',
37
- codeSnippet: getCodeSnippet(sourceCode, line),
38
- });
39
- return;
40
- }
41
-
42
- // Check 2: Empty dependency array with variables used inside
43
- if (t.isArrayExpression(depsArray) && depsArray.elements.length === 0) {
44
- // Collect variables used in the effect callback
45
- const usedVariables = new Set<string>();
46
-
47
- if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
48
- collectVariablesUsed(callback.body, usedVariables);
49
-
50
- // Filter out common globals and React hooks
51
- const globals = new Set(['console', 'window', 'document', 'setTimeout', 'setInterval',
52
- 'clearTimeout', 'clearInterval', 'fetch', 'Promise', 'JSON', 'Math', 'Date', 'Array',
53
- 'Object', 'String', 'Number', 'Boolean', 'Error', 'undefined', 'null', 'true', 'false']);
54
-
55
- // Check if there are local variables that might need to be dependencies
56
- const potentialMissing = [...usedVariables].filter(v =>
57
- !globals.has(v) &&
58
- !v.startsWith('set') && // Setters from useState are stable
59
- v !== 'props' &&
60
- v !== 'children'
61
- );
62
-
63
- // Only warn if there appear to be external variables used
64
- if (potentialMissing.length > 3) {
65
- smells.push({
66
- type: 'dependency-array-issue',
67
- severity: 'info',
68
- message: `useEffect in "${component.name}" has empty deps array but uses external variables`,
69
- file: filePath,
70
- line,
71
- column: loc?.start.column || 0,
72
- suggestion: `Consider adding dependencies: [${potentialMissing.slice(0, 3).join(', ')}...]`,
73
- codeSnippet: getCodeSnippet(sourceCode, line),
74
- });
75
- }
76
- }
77
- }
78
-
79
- // Check 3: Dependency array with object/array literals
80
- if (t.isArrayExpression(depsArray)) {
81
- depsArray.elements.forEach((elem, index) => {
82
- if (t.isObjectExpression(elem) || t.isArrayExpression(elem)) {
83
- smells.push({
84
- type: 'dependency-array-issue',
85
- severity: 'warning',
86
- message: `Object/array literal in useEffect dependency array in "${component.name}" causes infinite re-renders`,
87
- file: filePath,
88
- line,
89
- column: loc?.start.column || 0,
90
- suggestion: 'Move the object/array to a useMemo or extract to a constant outside the component.',
91
- codeSnippet: getCodeSnippet(sourceCode, line),
92
- });
93
- }
94
-
95
- // Check for inline functions
96
- if (t.isArrowFunctionExpression(elem) || t.isFunctionExpression(elem)) {
97
- smells.push({
98
- type: 'dependency-array-issue',
99
- severity: 'warning',
100
- message: `Inline function in useEffect dependency array in "${component.name}" causes infinite re-renders`,
101
- file: filePath,
102
- line,
103
- column: loc?.start.column || 0,
104
- suggestion: 'Wrap the function in useCallback before using as a dependency.',
105
- codeSnippet: getCodeSnippet(sourceCode, line),
106
- });
107
- }
108
- });
109
- }
110
- });
111
-
112
- // Also check useMemo and useCallback
113
- [...component.hooks.useMemo, ...component.hooks.useCallback].forEach(hookCall => {
114
- const depsArray = hookCall.arguments[1];
115
- const loc = hookCall.loc;
116
- const line = loc?.start.line || 0;
117
-
118
- if (!depsArray) {
119
- const hookName = t.isIdentifier((hookCall.callee as t.Identifier))
120
- ? ((hookCall.callee as t.Identifier).name)
121
- : 'useMemo/useCallback';
122
-
123
- smells.push({
124
- type: 'dependency-array-issue',
125
- severity: 'warning',
126
- message: `${hookName} without dependency array in "${component.name}"`,
127
- file: filePath,
128
- line,
129
- column: loc?.start.column || 0,
130
- suggestion: 'Add a dependency array to specify when the value should be recalculated.',
131
- codeSnippet: getCodeSnippet(sourceCode, line),
132
- });
133
- }
134
- });
135
-
136
- return smells;
137
- }
138
-
139
- /**
140
- * Helper to collect variable names used in a code block
141
- */
142
- function collectVariablesUsed(node: t.Node, variables: Set<string>): void {
143
- if (t.isIdentifier(node)) {
144
- variables.add(node.name);
145
- } else if (t.isMemberExpression(node)) {
146
- if (t.isIdentifier(node.object)) {
147
- variables.add(node.object.name);
148
- } else {
149
- collectVariablesUsed(node.object, variables);
150
- }
151
- } else if (t.isBlockStatement(node)) {
152
- node.body.forEach(stmt => collectVariablesUsed(stmt, variables));
153
- } else if (t.isExpressionStatement(node)) {
154
- collectVariablesUsed(node.expression, variables);
155
- } else if (t.isCallExpression(node)) {
156
- if (t.isIdentifier(node.callee)) {
157
- variables.add(node.callee.name);
158
- }
159
- node.arguments.forEach(arg => {
160
- if (t.isNode(arg)) collectVariablesUsed(arg, variables);
161
- });
162
- } else if (t.isConditionalExpression(node)) {
163
- collectVariablesUsed(node.test, variables);
164
- collectVariablesUsed(node.consequent, variables);
165
- collectVariablesUsed(node.alternate, variables);
166
- } else if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
167
- collectVariablesUsed(node.left, variables);
168
- collectVariablesUsed(node.right, variables);
169
- } else if (t.isReturnStatement(node) && node.argument) {
170
- collectVariablesUsed(node.argument, variables);
171
- } else if (t.isIfStatement(node)) {
172
- collectVariablesUsed(node.test, variables);
173
- collectVariablesUsed(node.consequent, variables);
174
- if (node.alternate) collectVariablesUsed(node.alternate, variables);
175
- }
176
- }
@@ -1,101 +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 hooks used inside conditionals or loops (violates Rules of Hooks)
8
- */
9
- export function detectHooksRulesViolations(
10
- component: ParsedComponent,
11
- filePath: string,
12
- sourceCode: string,
13
- config: DetectorConfig = DEFAULT_CONFIG
14
- ): CodeSmell[] {
15
- if (!config.checkHooksRules) return [];
16
-
17
- const smells: CodeSmell[] = [];
18
- const hooks = [
19
- 'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback',
20
- 'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect',
21
- 'useDebugValue', 'useDeferredValue', 'useTransition', 'useId',
22
- ];
23
-
24
- component.path.traverse({
25
- CallExpression(path) {
26
- // Check if it's a hook call
27
- const callee = path.node.callee;
28
- let hookName: string | null = null;
29
-
30
- if (t.isIdentifier(callee) && hooks.includes(callee.name)) {
31
- hookName = callee.name;
32
- } else if (t.isIdentifier(callee) && callee.name.startsWith('use') && callee.name[3]?.toUpperCase() === callee.name[3]) {
33
- // Custom hooks (useXxx pattern)
34
- hookName = callee.name;
35
- }
36
-
37
- if (!hookName) return;
38
-
39
- // Check if hook is inside a conditional or loop
40
- let currentPath: NodePath | null = path.parentPath;
41
-
42
- while (currentPath) {
43
- const node = currentPath.node;
44
-
45
- // Check for conditionals
46
- if (t.isIfStatement(node) || t.isConditionalExpression(node)) {
47
- const loc = path.node.loc;
48
- smells.push({
49
- type: 'hooks-rules-violation',
50
- severity: 'error',
51
- message: `Hook "${hookName}" called inside a conditional in "${component.name}"`,
52
- file: filePath,
53
- line: loc?.start.line || 0,
54
- column: loc?.start.column || 0,
55
- suggestion: 'Hooks must be called at the top level of the component. Move the hook call outside the conditional.',
56
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
57
- });
58
- break;
59
- }
60
-
61
- // Check for loops
62
- if (
63
- t.isForStatement(node) ||
64
- t.isWhileStatement(node) ||
65
- t.isDoWhileStatement(node) ||
66
- t.isForInStatement(node) ||
67
- t.isForOfStatement(node)
68
- ) {
69
- const loc = path.node.loc;
70
- smells.push({
71
- type: 'hooks-rules-violation',
72
- severity: 'error',
73
- message: `Hook "${hookName}" called inside a loop in "${component.name}"`,
74
- file: filePath,
75
- line: loc?.start.line || 0,
76
- column: loc?.start.column || 0,
77
- suggestion: 'Hooks must be called at the top level. Consider restructuring to avoid calling hooks in loops.',
78
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
79
- });
80
- break;
81
- }
82
-
83
- // Check for early return before hook (within same function)
84
- // This would require more complex flow analysis
85
-
86
- // Stop traversing when we reach the component function
87
- if (
88
- t.isFunctionDeclaration(node) ||
89
- t.isArrowFunctionExpression(node) ||
90
- t.isFunctionExpression(node)
91
- ) {
92
- break;
93
- }
94
-
95
- currentPath = currentPath.parentPath;
96
- }
97
- },
98
- });
99
-
100
- return smells;
101
- }
@@ -1,20 +0,0 @@
1
- export { detectUseEffectOveruse } from './useEffect.js';
2
- export { detectPropDrilling, analyzePropDrillingDepth } from './propDrilling.js';
3
- export { detectLargeComponent } from './largeComponent.js';
4
- export { detectUnmemoizedCalculations } from './memoization.js';
5
- export { detectMissingKeys } from './missingKey.js';
6
- export { detectHooksRulesViolations } from './hooksRules.js';
7
- export { detectDependencyArrayIssues } from './dependencyArray.js';
8
- export { detectNestedTernaries } from './nestedTernary.js';
9
- export { detectDeadCode, detectUnusedImports } from './deadCode.js';
10
- export { detectMagicValues } from './magicValues.js';
11
- // Framework-specific detectors
12
- export { detectNextjsIssues } from './nextjs.js';
13
- export { detectReactNativeIssues } from './reactNative.js';
14
- export { detectNodejsIssues } from './nodejs.js';
15
- export { detectJavascriptIssues } from './javascript.js';
16
- export { detectTypescriptIssues } from './typescript.js';
17
- // Debug, Security, Accessibility
18
- export { detectDebugStatements } from './debug.js';
19
- export { detectSecurityIssues } from './security.js';
20
- export { detectAccessibilityIssues } from './accessibility.js';
@@ -1,169 +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 vanilla JavaScript code smells:
7
- * - var usage (should use let/const)
8
- * - Loose equality (== instead of ===)
9
- * - Implicit type coercion
10
- * - Global variable pollution
11
- */
12
- export function detectJavascriptIssues(
13
- component: ParsedComponent,
14
- filePath: string,
15
- sourceCode: string,
16
- config: DetectorConfig = DEFAULT_CONFIG
17
- ): CodeSmell[] {
18
- if (!config.checkJavascript) return [];
19
-
20
- const smells: CodeSmell[] = [];
21
-
22
- // Detect var usage (should use let/const)
23
- component.path.traverse({
24
- VariableDeclaration(path) {
25
- if (path.node.kind === 'var') {
26
- const loc = path.node.loc;
27
- smells.push({
28
- type: 'js-var-usage',
29
- severity: 'warning',
30
- message: `Using "var" instead of "let" or "const" in "${component.name}"`,
31
- file: filePath,
32
- line: loc?.start.line || 0,
33
- column: loc?.start.column || 0,
34
- suggestion: 'Use "const" for values that never change, "let" for reassignable variables',
35
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
36
- });
37
- }
38
- },
39
- });
40
-
41
- // Detect loose equality (== and != instead of === and !==)
42
- component.path.traverse({
43
- BinaryExpression(path) {
44
- const { operator, left, right } = path.node;
45
-
46
- // Skip if comparing with null/undefined (sometimes intentional)
47
- const isNullCheck =
48
- (t.isNullLiteral(left) || t.isNullLiteral(right)) ||
49
- (t.isIdentifier(left) && left.name === 'undefined') ||
50
- (t.isIdentifier(right) && right.name === 'undefined');
51
-
52
- if (operator === '==' && !isNullCheck) {
53
- const loc = path.node.loc;
54
- smells.push({
55
- type: 'js-loose-equality',
56
- severity: 'warning',
57
- message: `Using loose equality "==" in "${component.name}"`,
58
- file: filePath,
59
- line: loc?.start.line || 0,
60
- column: loc?.start.column || 0,
61
- suggestion: 'Use strict equality "===" to avoid type coercion bugs',
62
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
63
- });
64
- }
65
-
66
- if (operator === '!=' && !isNullCheck) {
67
- const loc = path.node.loc;
68
- smells.push({
69
- type: 'js-loose-equality',
70
- severity: 'warning',
71
- message: `Using loose inequality "!=" in "${component.name}"`,
72
- file: filePath,
73
- line: loc?.start.line || 0,
74
- column: loc?.start.column || 0,
75
- suggestion: 'Use strict inequality "!==" to avoid type coercion bugs',
76
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
77
- });
78
- }
79
- },
80
- });
81
-
82
- // Detect implicit type coercion patterns
83
- component.path.traverse({
84
- // +string to convert to number
85
- UnaryExpression(path) {
86
- if (path.node.operator === '+' && t.isIdentifier(path.node.argument)) {
87
- const loc = path.node.loc;
88
- smells.push({
89
- type: 'js-implicit-coercion',
90
- severity: 'info',
91
- message: `Implicit number coercion with unary + in "${component.name}"`,
92
- file: filePath,
93
- line: loc?.start.line || 0,
94
- column: loc?.start.column || 0,
95
- suggestion: 'Use explicit conversion: Number(value) or parseInt(value, 10)',
96
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
97
- });
98
- }
99
- },
100
-
101
- // !!value for boolean coercion - detected via UnaryExpression
102
- // LogicalExpression handles &&, ||, ?? - skip for !! detection
103
-
104
- // String concatenation with + that mixes types
105
- BinaryExpression(path) {
106
- if (path.node.operator === '+') {
107
- const { left, right } = path.node;
108
- const leftIsString = t.isStringLiteral(left) || t.isTemplateLiteral(left);
109
- const rightIsString = t.isStringLiteral(right) || t.isTemplateLiteral(right);
110
- const leftIsNumber = t.isNumericLiteral(left);
111
- const rightIsNumber = t.isNumericLiteral(right);
112
-
113
- // Mixed string + number concatenation
114
- if ((leftIsString && rightIsNumber) || (leftIsNumber && rightIsString)) {
115
- const loc = path.node.loc;
116
- smells.push({
117
- type: 'js-implicit-coercion',
118
- severity: 'info',
119
- message: `Implicit string coercion in "${component.name}"`,
120
- file: filePath,
121
- line: loc?.start.line || 0,
122
- column: loc?.start.column || 0,
123
- suggestion: 'Use template literal for clarity: `${value}` or String(value)',
124
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
125
- });
126
- }
127
- }
128
- },
129
- });
130
-
131
- // Detect potential global pollution (assignments without declaration)
132
- component.path.traverse({
133
- AssignmentExpression(path) {
134
- const { left } = path.node;
135
-
136
- if (t.isIdentifier(left)) {
137
- // Check if this identifier is declared in scope
138
- const binding = path.scope.getBinding(left.name);
139
-
140
- if (!binding) {
141
- // Check if it's a well-known global
142
- const knownGlobals = [
143
- 'window', 'document', 'console', 'localStorage', 'sessionStorage',
144
- 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
145
- 'fetch', 'XMLHttpRequest', 'WebSocket', 'module', 'exports',
146
- 'require', 'process', 'global', '__dirname', '__filename',
147
- 'Buffer', 'Promise', 'Map', 'Set', 'Symbol',
148
- ];
149
-
150
- if (!knownGlobals.includes(left.name)) {
151
- const loc = path.node.loc;
152
- smells.push({
153
- type: 'js-global-pollution',
154
- severity: 'error',
155
- message: `Implicit global variable "${left.name}" in "${component.name}"`,
156
- file: filePath,
157
- line: loc?.start.line || 0,
158
- column: loc?.start.column || 0,
159
- suggestion: `Declare the variable: const ${left.name} = ... or let ${left.name} = ...`,
160
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
161
- });
162
- }
163
- }
164
- }
165
- },
166
- });
167
-
168
- return smells;
169
- }
@@ -1,63 +0,0 @@
1
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
2
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
3
-
4
- export function detectLargeComponent(
5
- component: ParsedComponent,
6
- filePath: string,
7
- sourceCode: string,
8
- config: DetectorConfig = DEFAULT_CONFIG
9
- ): CodeSmell[] {
10
- const smells: CodeSmell[] = [];
11
- const lineCount = component.endLine - component.startLine + 1;
12
-
13
- if (lineCount > config.maxComponentLines) {
14
- smells.push({
15
- type: 'large-component',
16
- severity: 'warning',
17
- message: `Component "${component.name}" has ${lineCount} lines (max recommended: ${config.maxComponentLines})`,
18
- file: filePath,
19
- line: component.startLine,
20
- column: 0,
21
- suggestion: 'Break this component into smaller, more focused components',
22
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
23
- });
24
- }
25
-
26
- // Check for complex JSX nesting
27
- if (component.jsxDepth > 10) {
28
- smells.push({
29
- type: 'deep-nesting',
30
- severity: 'info',
31
- message: `Component "${component.name}" has deeply nested JSX (depth: ~${component.jsxDepth})`,
32
- file: filePath,
33
- line: component.startLine,
34
- column: 0,
35
- suggestion: 'Extract nested elements into separate components for better readability',
36
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
37
- });
38
- }
39
-
40
- // Check for too many hooks (complexity indicator)
41
- const totalHooks =
42
- component.hooks.useEffect.length +
43
- component.hooks.useState.length +
44
- component.hooks.useMemo.length +
45
- component.hooks.useCallback.length +
46
- component.hooks.useRef.length +
47
- component.hooks.custom.length;
48
-
49
- if (totalHooks > 10) {
50
- smells.push({
51
- type: 'large-component',
52
- severity: 'warning',
53
- message: `Component "${component.name}" uses ${totalHooks} hooks - high complexity`,
54
- file: filePath,
55
- line: component.startLine,
56
- column: 0,
57
- suggestion: 'Extract related logic into custom hooks',
58
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
59
- });
60
- }
61
-
62
- return smells;
63
- }
@@ -1,114 +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 magic numbers and strings that should be constants
7
- */
8
- export function detectMagicValues(
9
- component: ParsedComponent,
10
- filePath: string,
11
- sourceCode: string,
12
- config: DetectorConfig = DEFAULT_CONFIG
13
- ): CodeSmell[] {
14
- if (!config.checkMagicValues) return [];
15
-
16
- const smells: CodeSmell[] = [];
17
- const threshold = config.magicNumberThreshold;
18
-
19
- // Track locations to avoid duplicate reports
20
- const reportedLines = new Set<number>();
21
-
22
- component.path.traverse({
23
- NumericLiteral(path) {
24
- const value = path.node.value;
25
- const loc = path.node.loc;
26
- const line = loc?.start.line || 0;
27
-
28
- // Skip common acceptable values
29
- if (value === 0 || value === 1 || value === -1 || value === 2 || value === 100) return;
30
-
31
- // Skip array indices and small numbers
32
- if (value < threshold && value > -threshold) return;
33
-
34
- // Skip if inside an object key position (e.g., style objects)
35
- if (t.isObjectProperty(path.parent) && path.parent.key === path.node) return;
36
-
37
- // Skip if it's a port number or similar config
38
- if (value === 3000 || value === 8080 || value === 80 || value === 443) return;
39
-
40
- // Skip if already reported this line
41
- if (reportedLines.has(line)) return;
42
- reportedLines.add(line);
43
-
44
- // Check context - skip if it's in a variable declaration that names the constant
45
- if (t.isVariableDeclarator(path.parent)) {
46
- const varName = t.isIdentifier(path.parent.id) ? path.parent.id.name : '';
47
- // If variable name is UPPER_CASE, it's already a constant
48
- if (varName === varName.toUpperCase() && varName.includes('_')) return;
49
- }
50
-
51
- smells.push({
52
- type: 'magic-value',
53
- severity: 'info',
54
- message: `Magic number ${value} in "${component.name}"`,
55
- file: filePath,
56
- line,
57
- column: loc?.start.column || 0,
58
- suggestion: `Extract to a named constant: const DESCRIPTIVE_NAME = ${value}`,
59
- codeSnippet: getCodeSnippet(sourceCode, line),
60
- });
61
- },
62
-
63
- StringLiteral(path) {
64
- const value = path.node.value;
65
- const loc = path.node.loc;
66
- const line = loc?.start.line || 0;
67
-
68
- // Skip short strings, empty strings, common values
69
- if (value.length < 10) return;
70
-
71
- // Skip if it looks like a URL, path, or import
72
- if (value.startsWith('http') || value.startsWith('/') || value.startsWith('.')) return;
73
-
74
- // Skip if it's an import source
75
- if (t.isImportDeclaration(path.parent)) return;
76
-
77
- // Skip JSX text content (usually intentional)
78
- if (t.isJSXText(path.parent) || t.isJSXExpressionContainer(path.parent)) return;
79
-
80
- // Skip object property keys
81
- if (t.isObjectProperty(path.parent) && path.parent.key === path.node) return;
82
-
83
- // Skip if already reported this line
84
- if (reportedLines.has(line)) return;
85
-
86
- // Skip common patterns
87
- if (
88
- value.includes('className') ||
89
- value.includes('px') ||
90
- value.includes('rem') ||
91
- value.includes('%')
92
- ) return;
93
-
94
- // Check if it looks like a user-facing message or error
95
- const looksLikeMessage = /^[A-Z][a-z]+.*[.!?]?$/.test(value) || value.includes(' ');
96
-
97
- if (looksLikeMessage && value.length > 30) {
98
- reportedLines.add(line);
99
- smells.push({
100
- type: 'magic-value',
101
- severity: 'info',
102
- message: `Hardcoded string in "${component.name}" should be in constants or i18n`,
103
- file: filePath,
104
- line,
105
- column: loc?.start.column || 0,
106
- suggestion: `Extract to a constants file or use i18n: "${value.substring(0, 30)}..."`,
107
- codeSnippet: getCodeSnippet(sourceCode, line),
108
- });
109
- }
110
- },
111
- });
112
-
113
- return smells;
114
- }