react-code-smell-detector 1.0.0 → 1.1.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 (59) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +43 -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 +11 -0
  14. package/dist/detectors/index.d.ts.map +1 -1
  15. package/dist/detectors/index.js +12 -0
  16. package/dist/detectors/javascript.d.ts +11 -0
  17. package/dist/detectors/javascript.d.ts.map +1 -0
  18. package/dist/detectors/javascript.js +148 -0
  19. package/dist/detectors/magicValues.d.ts +7 -0
  20. package/dist/detectors/magicValues.d.ts.map +1 -0
  21. package/dist/detectors/magicValues.js +99 -0
  22. package/dist/detectors/missingKey.d.ts +7 -0
  23. package/dist/detectors/missingKey.d.ts.map +1 -0
  24. package/dist/detectors/missingKey.js +93 -0
  25. package/dist/detectors/nestedTernary.d.ts +7 -0
  26. package/dist/detectors/nestedTernary.d.ts.map +1 -0
  27. package/dist/detectors/nestedTernary.js +58 -0
  28. package/dist/detectors/nextjs.d.ts +11 -0
  29. package/dist/detectors/nextjs.d.ts.map +1 -0
  30. package/dist/detectors/nextjs.js +103 -0
  31. package/dist/detectors/nodejs.d.ts +11 -0
  32. package/dist/detectors/nodejs.d.ts.map +1 -0
  33. package/dist/detectors/nodejs.js +169 -0
  34. package/dist/detectors/reactNative.d.ts +10 -0
  35. package/dist/detectors/reactNative.d.ts.map +1 -0
  36. package/dist/detectors/reactNative.js +135 -0
  37. package/dist/detectors/typescript.d.ts +11 -0
  38. package/dist/detectors/typescript.d.ts.map +1 -0
  39. package/dist/detectors/typescript.js +135 -0
  40. package/dist/reporter.js +30 -0
  41. package/dist/types/index.d.ts +14 -1
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/dist/types/index.js +14 -0
  44. package/package.json +1 -1
  45. package/src/analyzer.ts +53 -0
  46. package/src/detectors/deadCode.ts +163 -0
  47. package/src/detectors/dependencyArray.ts +176 -0
  48. package/src/detectors/hooksRules.ts +101 -0
  49. package/src/detectors/index.ts +12 -0
  50. package/src/detectors/javascript.ts +169 -0
  51. package/src/detectors/magicValues.ts +114 -0
  52. package/src/detectors/missingKey.ts +105 -0
  53. package/src/detectors/nestedTernary.ts +75 -0
  54. package/src/detectors/nextjs.ts +124 -0
  55. package/src/detectors/nodejs.ts +199 -0
  56. package/src/detectors/reactNative.ts +154 -0
  57. package/src/detectors/typescript.ts +151 -0
  58. package/src/reporter.ts +30 -0
  59. package/src/types/index.ts +59 -1
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA0CtF;AA+ID,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAqBA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA0CtF;AAyLD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/analyzer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fg from 'fast-glob';
2
2
  import path from 'path';
3
3
  import { parseFile } from './parser/index.js';
4
- import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, } from './detectors/index.js';
4
+ import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, } from './detectors/index.js';
5
5
  import { DEFAULT_CONFIG, } from './types/index.js';
6
6
  export async function analyzeProject(options) {
7
7
  const { rootDir, include = ['**/*.tsx', '**/*.jsx'], exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'], config: userConfig = {}, } = options;
@@ -62,6 +62,18 @@ function analyzeFile(parseResult, filePath, config) {
62
62
  smells.push(...detectPropDrilling(component, filePath, sourceCode, config));
63
63
  smells.push(...detectLargeComponent(component, filePath, sourceCode, config));
64
64
  smells.push(...detectUnmemoizedCalculations(component, filePath, sourceCode, config));
65
+ smells.push(...detectMissingKeys(component, filePath, sourceCode, config));
66
+ smells.push(...detectHooksRulesViolations(component, filePath, sourceCode, config));
67
+ smells.push(...detectDependencyArrayIssues(component, filePath, sourceCode, config));
68
+ smells.push(...detectNestedTernaries(component, filePath, sourceCode, config));
69
+ smells.push(...detectDeadCode(component, filePath, sourceCode, config));
70
+ smells.push(...detectMagicValues(component, filePath, sourceCode, config));
71
+ // Framework-specific detectors
72
+ smells.push(...detectNextjsIssues(component, filePath, sourceCode, config, imports));
73
+ smells.push(...detectReactNativeIssues(component, filePath, sourceCode, config, imports));
74
+ smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
75
+ smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
76
+ smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
65
77
  });
66
78
  // Run cross-component analysis
67
79
  smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
@@ -82,6 +94,36 @@ function calculateSummary(files) {
82
94
  'state-in-loop': 0,
83
95
  'inline-function-prop': 0,
84
96
  'deep-nesting': 0,
97
+ 'missing-key': 0,
98
+ 'hooks-rules-violation': 0,
99
+ 'dependency-array-issue': 0,
100
+ 'nested-ternary': 0,
101
+ 'dead-code': 0,
102
+ 'magic-value': 0,
103
+ // Next.js
104
+ 'nextjs-client-server-boundary': 0,
105
+ 'nextjs-missing-metadata': 0,
106
+ 'nextjs-image-unoptimized': 0,
107
+ 'nextjs-router-misuse': 0,
108
+ // React Native
109
+ 'rn-inline-style': 0,
110
+ 'rn-missing-accessibility': 0,
111
+ 'rn-performance-issue': 0,
112
+ // Node.js
113
+ 'nodejs-callback-hell': 0,
114
+ 'nodejs-unhandled-promise': 0,
115
+ 'nodejs-sync-io': 0,
116
+ 'nodejs-missing-error-handling': 0,
117
+ // JavaScript
118
+ 'js-var-usage': 0,
119
+ 'js-loose-equality': 0,
120
+ 'js-implicit-coercion': 0,
121
+ 'js-global-pollution': 0,
122
+ // TypeScript
123
+ 'ts-any-usage': 0,
124
+ 'ts-missing-return-type': 0,
125
+ 'ts-non-null-assertion': 0,
126
+ 'ts-type-assertion': 0,
85
127
  };
86
128
  const smellsBySeverity = {
87
129
  error: 0,
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,14 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects potentially dead code: unused variables, imports, and functions
5
+ */
6
+ export declare function detectDeadCode(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
7
+ /**
8
+ * Detects unused imports at the file level
9
+ */
10
+ export declare function detectUnusedImports(imports: Map<string, {
11
+ source: string;
12
+ line: number;
13
+ }>, usedInFile: Set<string>, filePath: string, sourceCode: string): CodeSmell[];
14
+ //# sourceMappingURL=deadCode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deadCode.d.ts","sourceRoot":"","sources":["../../src/detectors/deadCode.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;GAEG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAsHb;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACtD,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,SAAS,EAAE,CAsBb"}
@@ -0,0 +1,141 @@
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 potentially dead code: unused variables, imports, and functions
6
+ */
7
+ export function detectDeadCode(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
8
+ if (!config.checkDeadCode)
9
+ return [];
10
+ const smells = [];
11
+ // Track declared and used identifiers within the component
12
+ const declared = new Map();
13
+ const used = new Set();
14
+ // Collect all declared variables in the component
15
+ component.path.traverse({
16
+ VariableDeclarator(path) {
17
+ if (t.isIdentifier(path.node.id)) {
18
+ const loc = path.node.loc;
19
+ declared.set(path.node.id.name, {
20
+ line: loc?.start.line || 0,
21
+ column: loc?.start.column || 0,
22
+ type: 'variable',
23
+ });
24
+ }
25
+ else if (t.isObjectPattern(path.node.id)) {
26
+ // Destructured variables
27
+ path.node.id.properties.forEach(prop => {
28
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
29
+ const loc = prop.loc;
30
+ declared.set(prop.value.name, {
31
+ line: loc?.start.line || 0,
32
+ column: loc?.start.column || 0,
33
+ type: 'variable',
34
+ });
35
+ }
36
+ else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
37
+ const loc = prop.loc;
38
+ declared.set(prop.argument.name, {
39
+ line: loc?.start.line || 0,
40
+ column: loc?.start.column || 0,
41
+ type: 'variable',
42
+ });
43
+ }
44
+ });
45
+ }
46
+ else if (t.isArrayPattern(path.node.id)) {
47
+ // Array destructured variables
48
+ path.node.id.elements.forEach(elem => {
49
+ if (t.isIdentifier(elem)) {
50
+ const loc = elem.loc;
51
+ declared.set(elem.name, {
52
+ line: loc?.start.line || 0,
53
+ column: loc?.start.column || 0,
54
+ type: 'variable',
55
+ });
56
+ }
57
+ });
58
+ }
59
+ },
60
+ FunctionDeclaration(path) {
61
+ if (path.node.id) {
62
+ const loc = path.node.loc;
63
+ declared.set(path.node.id.name, {
64
+ line: loc?.start.line || 0,
65
+ column: loc?.start.column || 0,
66
+ type: 'function',
67
+ });
68
+ }
69
+ },
70
+ });
71
+ // Collect all used identifiers
72
+ component.path.traverse({
73
+ Identifier(path) {
74
+ // Skip if it's a declaration
75
+ if (t.isVariableDeclarator(path.parent) && path.parent.id === path.node ||
76
+ t.isFunctionDeclaration(path.parent) && path.parent.id === path.node ||
77
+ t.isObjectProperty(path.parent) && path.parent.key === path.node ||
78
+ t.isImportSpecifier(path.parent) ||
79
+ t.isImportDefaultSpecifier(path.parent)) {
80
+ return;
81
+ }
82
+ // Skip property access (x.y - y is not a reference)
83
+ if (t.isMemberExpression(path.parent) && path.parent.property === path.node && !path.parent.computed) {
84
+ return;
85
+ }
86
+ used.add(path.node.name);
87
+ },
88
+ JSXIdentifier(path) {
89
+ // Components used in JSX
90
+ if (t.isJSXOpeningElement(path.parent) || t.isJSXClosingElement(path.parent)) {
91
+ used.add(path.node.name);
92
+ }
93
+ },
94
+ });
95
+ // Find unused declarations
96
+ declared.forEach((info, name) => {
97
+ // Skip hooks and common patterns
98
+ if (name.startsWith('_') || name.startsWith('set'))
99
+ return;
100
+ // Skip if it's the component name itself
101
+ if (name === component.name)
102
+ return;
103
+ if (!used.has(name)) {
104
+ smells.push({
105
+ type: 'dead-code',
106
+ severity: 'info',
107
+ message: `Unused ${info.type} "${name}" in "${component.name}"`,
108
+ file: filePath,
109
+ line: info.line,
110
+ column: info.column,
111
+ suggestion: `Remove the unused ${info.type} or use it in the component.`,
112
+ codeSnippet: getCodeSnippet(sourceCode, info.line),
113
+ });
114
+ }
115
+ });
116
+ return smells;
117
+ }
118
+ /**
119
+ * Detects unused imports at the file level
120
+ */
121
+ export function detectUnusedImports(imports, usedInFile, filePath, sourceCode) {
122
+ const smells = [];
123
+ imports.forEach((info, name) => {
124
+ // Skip React imports as they might be used implicitly
125
+ if (name === 'React' || info.source === 'react')
126
+ return;
127
+ if (!usedInFile.has(name)) {
128
+ smells.push({
129
+ type: 'dead-code',
130
+ severity: 'info',
131
+ message: `Unused import "${name}" from "${info.source}"`,
132
+ file: filePath,
133
+ line: info.line,
134
+ column: 0,
135
+ suggestion: `Remove the unused import: import { ${name} } from '${info.source}'`,
136
+ codeSnippet: getCodeSnippet(sourceCode, info.line),
137
+ });
138
+ }
139
+ });
140
+ return smells;
141
+ }
@@ -0,0 +1,7 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects potential issues with useEffect dependency arrays
5
+ */
6
+ export declare function detectDependencyArrayIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
7
+ //# sourceMappingURL=dependencyArray.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependencyArray.d.ts","sourceRoot":"","sources":["../../src/detectors/dependencyArray.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CA4Hb"}
@@ -0,0 +1,164 @@
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 potential issues with useEffect dependency arrays
6
+ */
7
+ export function detectDependencyArrayIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
8
+ if (!config.checkDependencyArrays)
9
+ return [];
10
+ const smells = [];
11
+ // Analyze each useEffect
12
+ component.hooks.useEffect.forEach(effectCall => {
13
+ const args = effectCall.arguments;
14
+ const callback = args[0];
15
+ const depsArray = args[1];
16
+ const loc = effectCall.loc;
17
+ const line = loc?.start.line || 0;
18
+ // Check 1: Missing dependency array entirely
19
+ if (!depsArray) {
20
+ smells.push({
21
+ type: 'dependency-array-issue',
22
+ severity: 'warning',
23
+ message: `useEffect without dependency array in "${component.name}" runs on every render`,
24
+ file: filePath,
25
+ line,
26
+ column: loc?.start.column || 0,
27
+ suggestion: 'Add a dependency array. Use [] for mount-only effect, or [dep1, dep2] for reactive dependencies.',
28
+ codeSnippet: getCodeSnippet(sourceCode, line),
29
+ });
30
+ return;
31
+ }
32
+ // Check 2: Empty dependency array with variables used inside
33
+ if (t.isArrayExpression(depsArray) && depsArray.elements.length === 0) {
34
+ // Collect variables used in the effect callback
35
+ const usedVariables = new Set();
36
+ if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
37
+ collectVariablesUsed(callback.body, usedVariables);
38
+ // Filter out common globals and React hooks
39
+ const globals = new Set(['console', 'window', 'document', 'setTimeout', 'setInterval',
40
+ 'clearTimeout', 'clearInterval', 'fetch', 'Promise', 'JSON', 'Math', 'Date', 'Array',
41
+ 'Object', 'String', 'Number', 'Boolean', 'Error', 'undefined', 'null', 'true', 'false']);
42
+ // Check if there are local variables that might need to be dependencies
43
+ const potentialMissing = [...usedVariables].filter(v => !globals.has(v) &&
44
+ !v.startsWith('set') && // Setters from useState are stable
45
+ v !== 'props' &&
46
+ v !== 'children');
47
+ // Only warn if there appear to be external variables used
48
+ if (potentialMissing.length > 3) {
49
+ smells.push({
50
+ type: 'dependency-array-issue',
51
+ severity: 'info',
52
+ message: `useEffect in "${component.name}" has empty deps array but uses external variables`,
53
+ file: filePath,
54
+ line,
55
+ column: loc?.start.column || 0,
56
+ suggestion: `Consider adding dependencies: [${potentialMissing.slice(0, 3).join(', ')}...]`,
57
+ codeSnippet: getCodeSnippet(sourceCode, line),
58
+ });
59
+ }
60
+ }
61
+ }
62
+ // Check 3: Dependency array with object/array literals
63
+ if (t.isArrayExpression(depsArray)) {
64
+ depsArray.elements.forEach((elem, index) => {
65
+ if (t.isObjectExpression(elem) || t.isArrayExpression(elem)) {
66
+ smells.push({
67
+ type: 'dependency-array-issue',
68
+ severity: 'warning',
69
+ message: `Object/array literal in useEffect dependency array in "${component.name}" causes infinite re-renders`,
70
+ file: filePath,
71
+ line,
72
+ column: loc?.start.column || 0,
73
+ suggestion: 'Move the object/array to a useMemo or extract to a constant outside the component.',
74
+ codeSnippet: getCodeSnippet(sourceCode, line),
75
+ });
76
+ }
77
+ // Check for inline functions
78
+ if (t.isArrowFunctionExpression(elem) || t.isFunctionExpression(elem)) {
79
+ smells.push({
80
+ type: 'dependency-array-issue',
81
+ severity: 'warning',
82
+ message: `Inline function in useEffect dependency array in "${component.name}" causes infinite re-renders`,
83
+ file: filePath,
84
+ line,
85
+ column: loc?.start.column || 0,
86
+ suggestion: 'Wrap the function in useCallback before using as a dependency.',
87
+ codeSnippet: getCodeSnippet(sourceCode, line),
88
+ });
89
+ }
90
+ });
91
+ }
92
+ });
93
+ // Also check useMemo and useCallback
94
+ [...component.hooks.useMemo, ...component.hooks.useCallback].forEach(hookCall => {
95
+ const depsArray = hookCall.arguments[1];
96
+ const loc = hookCall.loc;
97
+ const line = loc?.start.line || 0;
98
+ if (!depsArray) {
99
+ const hookName = t.isIdentifier(hookCall.callee)
100
+ ? (hookCall.callee.name)
101
+ : 'useMemo/useCallback';
102
+ smells.push({
103
+ type: 'dependency-array-issue',
104
+ severity: 'warning',
105
+ message: `${hookName} without dependency array in "${component.name}"`,
106
+ file: filePath,
107
+ line,
108
+ column: loc?.start.column || 0,
109
+ suggestion: 'Add a dependency array to specify when the value should be recalculated.',
110
+ codeSnippet: getCodeSnippet(sourceCode, line),
111
+ });
112
+ }
113
+ });
114
+ return smells;
115
+ }
116
+ /**
117
+ * Helper to collect variable names used in a code block
118
+ */
119
+ function collectVariablesUsed(node, variables) {
120
+ if (t.isIdentifier(node)) {
121
+ variables.add(node.name);
122
+ }
123
+ else if (t.isMemberExpression(node)) {
124
+ if (t.isIdentifier(node.object)) {
125
+ variables.add(node.object.name);
126
+ }
127
+ else {
128
+ collectVariablesUsed(node.object, variables);
129
+ }
130
+ }
131
+ else if (t.isBlockStatement(node)) {
132
+ node.body.forEach(stmt => collectVariablesUsed(stmt, variables));
133
+ }
134
+ else if (t.isExpressionStatement(node)) {
135
+ collectVariablesUsed(node.expression, variables);
136
+ }
137
+ else if (t.isCallExpression(node)) {
138
+ if (t.isIdentifier(node.callee)) {
139
+ variables.add(node.callee.name);
140
+ }
141
+ node.arguments.forEach(arg => {
142
+ if (t.isNode(arg))
143
+ collectVariablesUsed(arg, variables);
144
+ });
145
+ }
146
+ else if (t.isConditionalExpression(node)) {
147
+ collectVariablesUsed(node.test, variables);
148
+ collectVariablesUsed(node.consequent, variables);
149
+ collectVariablesUsed(node.alternate, variables);
150
+ }
151
+ else if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
152
+ collectVariablesUsed(node.left, variables);
153
+ collectVariablesUsed(node.right, variables);
154
+ }
155
+ else if (t.isReturnStatement(node) && node.argument) {
156
+ collectVariablesUsed(node.argument, variables);
157
+ }
158
+ else if (t.isIfStatement(node)) {
159
+ collectVariablesUsed(node.test, variables);
160
+ collectVariablesUsed(node.consequent, variables);
161
+ if (node.alternate)
162
+ collectVariablesUsed(node.alternate, variables);
163
+ }
164
+ }
@@ -0,0 +1,7 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects hooks used inside conditionals or loops (violates Rules of Hooks)
5
+ */
6
+ export declare function detectHooksRulesViolations(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
7
+ //# sourceMappingURL=hooksRules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooksRules.d.ts","sourceRoot":"","sources":["../../src/detectors/hooksRules.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,0BAA0B,CACxC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAuFb"}
@@ -0,0 +1,81 @@
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 hooks used inside conditionals or loops (violates Rules of Hooks)
6
+ */
7
+ export function detectHooksRulesViolations(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
8
+ if (!config.checkHooksRules)
9
+ return [];
10
+ const smells = [];
11
+ const hooks = [
12
+ 'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback',
13
+ 'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect',
14
+ 'useDebugValue', 'useDeferredValue', 'useTransition', 'useId',
15
+ ];
16
+ component.path.traverse({
17
+ CallExpression(path) {
18
+ // Check if it's a hook call
19
+ const callee = path.node.callee;
20
+ let hookName = null;
21
+ if (t.isIdentifier(callee) && hooks.includes(callee.name)) {
22
+ hookName = callee.name;
23
+ }
24
+ else if (t.isIdentifier(callee) && callee.name.startsWith('use') && callee.name[3]?.toUpperCase() === callee.name[3]) {
25
+ // Custom hooks (useXxx pattern)
26
+ hookName = callee.name;
27
+ }
28
+ if (!hookName)
29
+ return;
30
+ // Check if hook is inside a conditional or loop
31
+ let currentPath = path.parentPath;
32
+ while (currentPath) {
33
+ const node = currentPath.node;
34
+ // Check for conditionals
35
+ if (t.isIfStatement(node) || t.isConditionalExpression(node)) {
36
+ const loc = path.node.loc;
37
+ smells.push({
38
+ type: 'hooks-rules-violation',
39
+ severity: 'error',
40
+ message: `Hook "${hookName}" called inside a conditional in "${component.name}"`,
41
+ file: filePath,
42
+ line: loc?.start.line || 0,
43
+ column: loc?.start.column || 0,
44
+ suggestion: 'Hooks must be called at the top level of the component. Move the hook call outside the conditional.',
45
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
46
+ });
47
+ break;
48
+ }
49
+ // Check for loops
50
+ if (t.isForStatement(node) ||
51
+ t.isWhileStatement(node) ||
52
+ t.isDoWhileStatement(node) ||
53
+ t.isForInStatement(node) ||
54
+ t.isForOfStatement(node)) {
55
+ const loc = path.node.loc;
56
+ smells.push({
57
+ type: 'hooks-rules-violation',
58
+ severity: 'error',
59
+ message: `Hook "${hookName}" called inside a loop in "${component.name}"`,
60
+ file: filePath,
61
+ line: loc?.start.line || 0,
62
+ column: loc?.start.column || 0,
63
+ suggestion: 'Hooks must be called at the top level. Consider restructuring to avoid calling hooks in loops.',
64
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
65
+ });
66
+ break;
67
+ }
68
+ // Check for early return before hook (within same function)
69
+ // This would require more complex flow analysis
70
+ // Stop traversing when we reach the component function
71
+ if (t.isFunctionDeclaration(node) ||
72
+ t.isArrowFunctionExpression(node) ||
73
+ t.isFunctionExpression(node)) {
74
+ break;
75
+ }
76
+ currentPath = currentPath.parentPath;
77
+ }
78
+ },
79
+ });
80
+ return smells;
81
+ }
@@ -2,4 +2,15 @@ export { detectUseEffectOveruse } from './useEffect.js';
2
2
  export { detectPropDrilling, analyzePropDrillingDepth } from './propDrilling.js';
3
3
  export { detectLargeComponent } from './largeComponent.js';
4
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
+ export { detectNextjsIssues } from './nextjs.js';
12
+ export { detectReactNativeIssues } from './reactNative.js';
13
+ export { detectNodejsIssues } from './nodejs.js';
14
+ export { detectJavascriptIssues } from './javascript.js';
15
+ export { detectTypescriptIssues } from './typescript.js';
5
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -2,3 +2,15 @@ export { detectUseEffectOveruse } from './useEffect.js';
2
2
  export { detectPropDrilling, analyzePropDrillingDepth } from './propDrilling.js';
3
3
  export { detectLargeComponent } from './largeComponent.js';
4
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';
@@ -0,0 +1,11 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects vanilla JavaScript code smells:
5
+ * - var usage (should use let/const)
6
+ * - Loose equality (== instead of ===)
7
+ * - Implicit type coercion
8
+ * - Global variable pollution
9
+ */
10
+ export declare function detectJavascriptIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
11
+ //# sourceMappingURL=javascript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"javascript.d.ts","sourceRoot":"","sources":["../../src/detectors/javascript.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAwJb"}