react-code-smell-detector 1.0.0 → 1.0.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 (39) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +13 -1
  3. package/dist/cli.js +0 -0
  4. package/dist/detectors/deadCode.d.ts +14 -0
  5. package/dist/detectors/deadCode.d.ts.map +1 -0
  6. package/dist/detectors/deadCode.js +141 -0
  7. package/dist/detectors/dependencyArray.d.ts +7 -0
  8. package/dist/detectors/dependencyArray.d.ts.map +1 -0
  9. package/dist/detectors/dependencyArray.js +164 -0
  10. package/dist/detectors/hooksRules.d.ts +7 -0
  11. package/dist/detectors/hooksRules.d.ts.map +1 -0
  12. package/dist/detectors/hooksRules.js +81 -0
  13. package/dist/detectors/index.d.ts +6 -0
  14. package/dist/detectors/index.d.ts.map +1 -1
  15. package/dist/detectors/index.js +6 -0
  16. package/dist/detectors/magicValues.d.ts +7 -0
  17. package/dist/detectors/magicValues.d.ts.map +1 -0
  18. package/dist/detectors/magicValues.js +99 -0
  19. package/dist/detectors/missingKey.d.ts +7 -0
  20. package/dist/detectors/missingKey.d.ts.map +1 -0
  21. package/dist/detectors/missingKey.js +93 -0
  22. package/dist/detectors/nestedTernary.d.ts +7 -0
  23. package/dist/detectors/nestedTernary.d.ts.map +1 -0
  24. package/dist/detectors/nestedTernary.js +58 -0
  25. package/dist/reporter.js +6 -0
  26. package/dist/types/index.d.ts +8 -1
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/index.js +7 -0
  29. package/package.json +1 -1
  30. package/src/analyzer.ts +18 -0
  31. package/src/detectors/deadCode.ts +163 -0
  32. package/src/detectors/dependencyArray.ts +176 -0
  33. package/src/detectors/hooksRules.ts +101 -0
  34. package/src/detectors/index.ts +6 -0
  35. package/src/detectors/magicValues.ts +114 -0
  36. package/src/detectors/missingKey.ts +105 -0
  37. package/src/detectors/nestedTernary.ts +75 -0
  38. package/src/reporter.ts +6 -0
  39. package/src/types/index.ts +21 -1
@@ -0,0 +1,93 @@
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 .map() calls that render JSX without a key prop
6
+ */
7
+ export function detectMissingKeys(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
8
+ if (!config.checkMissingKeys)
9
+ return [];
10
+ const smells = [];
11
+ component.path.traverse({
12
+ CallExpression(path) {
13
+ // Check if it's a .map() call
14
+ if (!t.isMemberExpression(path.node.callee))
15
+ return;
16
+ const prop = path.node.callee.property;
17
+ if (!t.isIdentifier(prop) || prop.name !== 'map')
18
+ return;
19
+ // Check if the callback returns JSX
20
+ const callback = path.node.arguments[0];
21
+ if (!callback)
22
+ return;
23
+ let callbackBody = null;
24
+ if (t.isArrowFunctionExpression(callback)) {
25
+ callbackBody = callback.body;
26
+ }
27
+ else if (t.isFunctionExpression(callback)) {
28
+ callbackBody = callback.body;
29
+ }
30
+ if (!callbackBody)
31
+ return;
32
+ // Find JSX elements in the callback
33
+ let hasJSX = false;
34
+ let hasKey = false;
35
+ let jsxLine = path.node.loc?.start.line || 0;
36
+ const checkForKey = (node) => {
37
+ if (t.isJSXElement(node)) {
38
+ hasJSX = true;
39
+ jsxLine = node.loc?.start.line || jsxLine;
40
+ // Check if the opening element has a key prop
41
+ const openingElement = node.openingElement;
42
+ const keyAttr = openingElement.attributes.find(attr => {
43
+ if (t.isJSXAttribute(attr)) {
44
+ return t.isJSXIdentifier(attr.name) && attr.name.name === 'key';
45
+ }
46
+ return false;
47
+ });
48
+ if (keyAttr) {
49
+ hasKey = true;
50
+ }
51
+ }
52
+ else if (t.isJSXFragment(node)) {
53
+ hasJSX = true;
54
+ // Fragments can't have keys directly, so check children
55
+ }
56
+ };
57
+ // Traverse the callback body to find JSX
58
+ if (t.isJSXElement(callbackBody) || t.isJSXFragment(callbackBody)) {
59
+ checkForKey(callbackBody);
60
+ }
61
+ else if (t.isBlockStatement(callbackBody)) {
62
+ path.traverse({
63
+ ReturnStatement(returnPath) {
64
+ if (returnPath.node.argument) {
65
+ checkForKey(returnPath.node.argument);
66
+ }
67
+ },
68
+ JSXElement(jsxPath) {
69
+ // Only check top-level JSX in the map callback
70
+ if (jsxPath.parent === callbackBody ||
71
+ (t.isReturnStatement(jsxPath.parent) && jsxPath.parent.argument === jsxPath.node)) {
72
+ checkForKey(jsxPath.node);
73
+ }
74
+ },
75
+ });
76
+ }
77
+ if (hasJSX && !hasKey) {
78
+ const loc = path.node.loc;
79
+ smells.push({
80
+ type: 'missing-key',
81
+ severity: 'error',
82
+ message: `Missing "key" prop in .map() that renders JSX in "${component.name}"`,
83
+ file: filePath,
84
+ line: jsxLine || loc?.start.line || 0,
85
+ column: loc?.start.column || 0,
86
+ suggestion: 'Add a unique "key" prop to the root element: <Element key={item.id}>',
87
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
88
+ });
89
+ }
90
+ },
91
+ });
92
+ return smells;
93
+ }
@@ -0,0 +1,7 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects deeply nested ternary expressions (complex conditional rendering)
5
+ */
6
+ export declare function detectNestedTernaries(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
7
+ //# sourceMappingURL=nestedTernary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nestedTernary.d.ts","sourceRoot":"","sources":["../../src/detectors/nestedTernary.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CA4Bb"}
@@ -0,0 +1,58 @@
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 deeply nested ternary expressions (complex conditional rendering)
6
+ */
7
+ export function detectNestedTernaries(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
8
+ const smells = [];
9
+ const maxDepth = config.maxTernaryDepth;
10
+ component.path.traverse({
11
+ ConditionalExpression(path) {
12
+ // Only check the outermost ternary
13
+ if (isNestedInTernary(path))
14
+ return;
15
+ const depth = getTernaryDepth(path.node);
16
+ if (depth > maxDepth) {
17
+ const loc = path.node.loc;
18
+ smells.push({
19
+ type: 'nested-ternary',
20
+ severity: depth > maxDepth + 1 ? 'warning' : 'info',
21
+ message: `Nested ternary expression (depth: ${depth}) in "${component.name}"`,
22
+ file: filePath,
23
+ line: loc?.start.line || 0,
24
+ column: loc?.start.column || 0,
25
+ suggestion: `Refactor to use if/else, switch, or extract into separate components. Max recommended depth: ${maxDepth}`,
26
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
27
+ });
28
+ }
29
+ },
30
+ });
31
+ return smells;
32
+ }
33
+ /**
34
+ * Check if a ternary is nested inside another ternary
35
+ */
36
+ function isNestedInTernary(path) {
37
+ let current = path.parentPath;
38
+ while (current) {
39
+ if (t.isConditionalExpression(current.node)) {
40
+ return true;
41
+ }
42
+ current = current.parentPath;
43
+ }
44
+ return false;
45
+ }
46
+ /**
47
+ * Calculate the depth of nested ternary expressions
48
+ */
49
+ function getTernaryDepth(node) {
50
+ let maxChildDepth = 0;
51
+ if (t.isConditionalExpression(node.consequent)) {
52
+ maxChildDepth = Math.max(maxChildDepth, getTernaryDepth(node.consequent));
53
+ }
54
+ if (t.isConditionalExpression(node.alternate)) {
55
+ maxChildDepth = Math.max(maxChildDepth, getTernaryDepth(node.alternate));
56
+ }
57
+ return 1 + maxChildDepth;
58
+ }
package/dist/reporter.js CHANGED
@@ -212,6 +212,12 @@ function formatSmellType(type) {
212
212
  'state-in-loop': '🔄 State in Loop',
213
213
  'inline-function-prop': '📎 Inline Function Prop',
214
214
  'deep-nesting': '📊 Deep Nesting',
215
+ 'missing-key': '🔑 Missing Key',
216
+ 'hooks-rules-violation': '⚠️ Hooks Rules Violation',
217
+ 'dependency-array-issue': '📋 Dependency Array Issue',
218
+ 'nested-ternary': '❓ Nested Ternary',
219
+ 'dead-code': '💀 Dead Code',
220
+ 'magic-value': '🔢 Magic Value',
215
221
  };
216
222
  return labels[type] || type;
217
223
  }
@@ -1,5 +1,5 @@
1
1
  export type SmellSeverity = 'error' | 'warning' | 'info';
2
- export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting';
2
+ export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting' | 'missing-key' | 'hooks-rules-violation' | 'dependency-array-issue' | 'nested-ternary' | 'dead-code' | 'magic-value';
3
3
  export interface CodeSmell {
4
4
  type: SmellType;
5
5
  severity: SmellSeverity;
@@ -59,6 +59,13 @@ export interface DetectorConfig {
59
59
  maxComponentLines: number;
60
60
  maxPropsCount: number;
61
61
  checkMemoization: boolean;
62
+ checkMissingKeys: boolean;
63
+ checkHooksRules: boolean;
64
+ checkDependencyArrays: boolean;
65
+ maxTernaryDepth: number;
66
+ checkDeadCode: boolean;
67
+ checkMagicValues: boolean;
68
+ magicNumberThreshold: number;
62
69
  }
63
70
  export declare const DEFAULT_CONFIG: DetectorConfig;
64
71
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,MAAM,MAAM,SAAS,GACjB,mBAAmB,GACnB,eAAe,GACf,iBAAiB,GACjB,wBAAwB,GACxB,oBAAoB,GACpB,eAAe,GACf,sBAAsB,GACtB,cAAc,CAAC;AAEnB,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,kBAAkB,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACxC,gBAAgB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACnC,SAAS,EAAE;QACT,cAAc,EAAE,MAAM,CAAC;QACvB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,yBAAyB,EAAE,MAAM,CAAC;IAClC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,eAAO,MAAM,cAAc,EAAE,cAM5B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,MAAM,MAAM,SAAS,GACjB,mBAAmB,GACnB,eAAe,GACf,iBAAiB,GACjB,wBAAwB,GACxB,oBAAoB,GACpB,eAAe,GACf,sBAAsB,GACtB,cAAc,GACd,aAAa,GACb,uBAAuB,GACvB,wBAAwB,GACxB,gBAAgB,GAChB,WAAW,GACX,aAAa,CAAC;AAElB,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,kBAAkB,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACxC,gBAAgB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACnC,SAAS,EAAE;QACT,cAAc,EAAE,MAAM,CAAC;QACvB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,yBAAyB,EAAE,MAAM,CAAC;IAClC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,cAAc,EAAE,cAa5B,CAAC"}
@@ -4,4 +4,11 @@ export const DEFAULT_CONFIG = {
4
4
  maxComponentLines: 300,
5
5
  maxPropsCount: 7,
6
6
  checkMemoization: true,
7
+ checkMissingKeys: true,
8
+ checkHooksRules: true,
9
+ checkDependencyArrays: true,
10
+ maxTernaryDepth: 2,
11
+ checkDeadCode: true,
12
+ checkMagicValues: true,
13
+ magicNumberThreshold: 10,
7
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-code-smell-detector",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Detect code smells in React projects - useEffect overuse, prop drilling, large components, and more",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/analyzer.ts CHANGED
@@ -7,6 +7,12 @@ import {
7
7
  analyzePropDrillingDepth,
8
8
  detectLargeComponent,
9
9
  detectUnmemoizedCalculations,
10
+ detectMissingKeys,
11
+ detectHooksRulesViolations,
12
+ detectDependencyArrayIssues,
13
+ detectNestedTernaries,
14
+ detectDeadCode,
15
+ detectMagicValues,
10
16
  } from './detectors/index.js';
11
17
  import {
12
18
  AnalysisResult,
@@ -100,6 +106,12 @@ function analyzeFile(parseResult: ParseResult, filePath: string, config: Detecto
100
106
  smells.push(...detectPropDrilling(component, filePath, sourceCode, config));
101
107
  smells.push(...detectLargeComponent(component, filePath, sourceCode, config));
102
108
  smells.push(...detectUnmemoizedCalculations(component, filePath, sourceCode, config));
109
+ smells.push(...detectMissingKeys(component, filePath, sourceCode, config));
110
+ smells.push(...detectHooksRulesViolations(component, filePath, sourceCode, config));
111
+ smells.push(...detectDependencyArrayIssues(component, filePath, sourceCode, config));
112
+ smells.push(...detectNestedTernaries(component, filePath, sourceCode, config));
113
+ smells.push(...detectDeadCode(component, filePath, sourceCode, config));
114
+ smells.push(...detectMagicValues(component, filePath, sourceCode, config));
103
115
  });
104
116
 
105
117
  // Run cross-component analysis
@@ -123,6 +135,12 @@ function calculateSummary(files: FileAnalysis[]): AnalysisSummary {
123
135
  'state-in-loop': 0,
124
136
  'inline-function-prop': 0,
125
137
  'deep-nesting': 0,
138
+ 'missing-key': 0,
139
+ 'hooks-rules-violation': 0,
140
+ 'dependency-array-issue': 0,
141
+ 'nested-ternary': 0,
142
+ 'dead-code': 0,
143
+ 'magic-value': 0,
126
144
  };
127
145
 
128
146
  const smellsBySeverity: Record<SmellSeverity, number> = {
@@ -0,0 +1,163 @@
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 potentially dead code: unused variables, imports, and functions
7
+ */
8
+ export function detectDeadCode(
9
+ component: ParsedComponent,
10
+ filePath: string,
11
+ sourceCode: string,
12
+ config: DetectorConfig = DEFAULT_CONFIG
13
+ ): CodeSmell[] {
14
+ if (!config.checkDeadCode) return [];
15
+
16
+ const smells: CodeSmell[] = [];
17
+
18
+ // Track declared and used identifiers within the component
19
+ const declared = new Map<string, { line: number; column: number; type: string }>();
20
+ const used = new Set<string>();
21
+
22
+ // Collect all declared variables in the component
23
+ component.path.traverse({
24
+ VariableDeclarator(path) {
25
+ if (t.isIdentifier(path.node.id)) {
26
+ const loc = path.node.loc;
27
+ declared.set(path.node.id.name, {
28
+ line: loc?.start.line || 0,
29
+ column: loc?.start.column || 0,
30
+ type: 'variable',
31
+ });
32
+ } else if (t.isObjectPattern(path.node.id)) {
33
+ // Destructured variables
34
+ path.node.id.properties.forEach(prop => {
35
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
36
+ const loc = prop.loc;
37
+ declared.set(prop.value.name, {
38
+ line: loc?.start.line || 0,
39
+ column: loc?.start.column || 0,
40
+ type: 'variable',
41
+ });
42
+ } else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
43
+ const loc = prop.loc;
44
+ declared.set(prop.argument.name, {
45
+ line: loc?.start.line || 0,
46
+ column: loc?.start.column || 0,
47
+ type: 'variable',
48
+ });
49
+ }
50
+ });
51
+ } else if (t.isArrayPattern(path.node.id)) {
52
+ // Array destructured variables
53
+ path.node.id.elements.forEach(elem => {
54
+ if (t.isIdentifier(elem)) {
55
+ const loc = elem.loc;
56
+ declared.set(elem.name, {
57
+ line: loc?.start.line || 0,
58
+ column: loc?.start.column || 0,
59
+ type: 'variable',
60
+ });
61
+ }
62
+ });
63
+ }
64
+ },
65
+
66
+ FunctionDeclaration(path) {
67
+ if (path.node.id) {
68
+ const loc = path.node.loc;
69
+ declared.set(path.node.id.name, {
70
+ line: loc?.start.line || 0,
71
+ column: loc?.start.column || 0,
72
+ type: 'function',
73
+ });
74
+ }
75
+ },
76
+ });
77
+
78
+ // Collect all used identifiers
79
+ component.path.traverse({
80
+ Identifier(path) {
81
+ // Skip if it's a declaration
82
+ if (
83
+ t.isVariableDeclarator(path.parent) && path.parent.id === path.node ||
84
+ t.isFunctionDeclaration(path.parent) && path.parent.id === path.node ||
85
+ t.isObjectProperty(path.parent) && path.parent.key === path.node ||
86
+ t.isImportSpecifier(path.parent) ||
87
+ t.isImportDefaultSpecifier(path.parent)
88
+ ) {
89
+ return;
90
+ }
91
+
92
+ // Skip property access (x.y - y is not a reference)
93
+ if (t.isMemberExpression(path.parent) && path.parent.property === path.node && !path.parent.computed) {
94
+ return;
95
+ }
96
+
97
+ used.add(path.node.name);
98
+ },
99
+
100
+ JSXIdentifier(path) {
101
+ // Components used in JSX
102
+ if (t.isJSXOpeningElement(path.parent) || t.isJSXClosingElement(path.parent)) {
103
+ used.add(path.node.name);
104
+ }
105
+ },
106
+ });
107
+
108
+ // Find unused declarations
109
+ declared.forEach((info, name) => {
110
+ // Skip hooks and common patterns
111
+ if (name.startsWith('_') || name.startsWith('set')) return;
112
+
113
+ // Skip if it's the component name itself
114
+ if (name === component.name) return;
115
+
116
+ if (!used.has(name)) {
117
+ smells.push({
118
+ type: 'dead-code',
119
+ severity: 'info',
120
+ message: `Unused ${info.type} "${name}" in "${component.name}"`,
121
+ file: filePath,
122
+ line: info.line,
123
+ column: info.column,
124
+ suggestion: `Remove the unused ${info.type} or use it in the component.`,
125
+ codeSnippet: getCodeSnippet(sourceCode, info.line),
126
+ });
127
+ }
128
+ });
129
+
130
+ return smells;
131
+ }
132
+
133
+ /**
134
+ * Detects unused imports at the file level
135
+ */
136
+ export function detectUnusedImports(
137
+ imports: Map<string, { source: string; line: number }>,
138
+ usedInFile: Set<string>,
139
+ filePath: string,
140
+ sourceCode: string
141
+ ): CodeSmell[] {
142
+ const smells: CodeSmell[] = [];
143
+
144
+ imports.forEach((info, name) => {
145
+ // Skip React imports as they might be used implicitly
146
+ if (name === 'React' || info.source === 'react') return;
147
+
148
+ if (!usedInFile.has(name)) {
149
+ smells.push({
150
+ type: 'dead-code',
151
+ severity: 'info',
152
+ message: `Unused import "${name}" from "${info.source}"`,
153
+ file: filePath,
154
+ line: info.line,
155
+ column: 0,
156
+ suggestion: `Remove the unused import: import { ${name} } from '${info.source}'`,
157
+ codeSnippet: getCodeSnippet(sourceCode, info.line),
158
+ });
159
+ }
160
+ });
161
+
162
+ return smells;
163
+ }
@@ -0,0 +1,176 @@
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
+ }