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
@@ -0,0 +1,169 @@
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 Node.js-specific code smells:
6
+ * - Callback hell (deeply nested callbacks)
7
+ * - Unhandled promise rejections
8
+ * - Synchronous I/O operations
9
+ * - Missing error handling
10
+ */
11
+ export function detectNodejsIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG, imports = []) {
12
+ if (!config.checkNodejs)
13
+ return [];
14
+ // Check if this looks like a Node.js file
15
+ const isNodeFile = imports.some(imp => imp.includes('fs') || imp.includes('path') || imp.includes('http') ||
16
+ imp.includes('express') || imp.includes('child_process') ||
17
+ imp.includes('crypto') || imp.includes('os') || imp.includes('stream')) || filePath.includes('.server.') || filePath.includes('/api/');
18
+ if (!isNodeFile)
19
+ return [];
20
+ const smells = [];
21
+ // Detect callback hell (nested callbacks > maxCallbackDepth)
22
+ component.path.traverse({
23
+ CallExpression(path) {
24
+ const depth = getCallbackDepth(path);
25
+ if (depth > config.maxCallbackDepth) {
26
+ const loc = path.node.loc;
27
+ smells.push({
28
+ type: 'nodejs-callback-hell',
29
+ severity: 'warning',
30
+ message: `Callback hell detected (depth: ${depth}) in "${component.name}"`,
31
+ file: filePath,
32
+ line: loc?.start.line || 0,
33
+ column: loc?.start.column || 0,
34
+ suggestion: 'Refactor to async/await or use Promise.all() for parallel operations',
35
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
36
+ });
37
+ }
38
+ },
39
+ });
40
+ // Detect unhandled promise rejections (Promise without .catch or try/catch)
41
+ component.path.traverse({
42
+ CallExpression(path) {
43
+ // Check for .then() without .catch()
44
+ if (t.isMemberExpression(path.node.callee) &&
45
+ t.isIdentifier(path.node.callee.property) &&
46
+ path.node.callee.property.name === 'then') {
47
+ // Check if followed by .catch() in chain
48
+ const parent = path.parent;
49
+ let hasCatch = false;
50
+ if (t.isMemberExpression(parent)) {
51
+ const prop = parent.property;
52
+ if (t.isIdentifier(prop) && prop.name === 'catch') {
53
+ hasCatch = true;
54
+ }
55
+ }
56
+ // Check if inside try block
57
+ let current = path;
58
+ while (current) {
59
+ if (t.isTryStatement(current.node)) {
60
+ hasCatch = true;
61
+ break;
62
+ }
63
+ current = current.parentPath;
64
+ }
65
+ if (!hasCatch) {
66
+ const loc = path.node.loc;
67
+ smells.push({
68
+ type: 'nodejs-unhandled-promise',
69
+ severity: 'warning',
70
+ message: `.then() without .catch() in "${component.name}"`,
71
+ file: filePath,
72
+ line: loc?.start.line || 0,
73
+ column: loc?.start.column || 0,
74
+ suggestion: 'Add .catch() to handle rejections, or use try/catch with async/await',
75
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
76
+ });
77
+ }
78
+ }
79
+ },
80
+ });
81
+ // Detect synchronous file operations
82
+ const syncMethods = ['readFileSync', 'writeFileSync', 'appendFileSync', 'readdirSync',
83
+ 'statSync', 'mkdirSync', 'rmdirSync', 'unlinkSync', 'existsSync'];
84
+ component.path.traverse({
85
+ CallExpression(path) {
86
+ const callee = path.node.callee;
87
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
88
+ if (syncMethods.includes(callee.property.name)) {
89
+ const loc = path.node.loc;
90
+ smells.push({
91
+ type: 'nodejs-sync-io',
92
+ severity: 'warning',
93
+ message: `Synchronous file operation "${callee.property.name}" blocks event loop`,
94
+ file: filePath,
95
+ line: loc?.start.line || 0,
96
+ column: loc?.start.column || 0,
97
+ suggestion: `Use async version: ${callee.property.name.replace('Sync', '')} with await or promises`,
98
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
99
+ });
100
+ }
101
+ }
102
+ // Direct function call
103
+ if (t.isIdentifier(callee) && syncMethods.includes(callee.name)) {
104
+ const loc = path.node.loc;
105
+ smells.push({
106
+ type: 'nodejs-sync-io',
107
+ severity: 'warning',
108
+ message: `Synchronous file operation "${callee.name}" blocks event loop`,
109
+ file: filePath,
110
+ line: loc?.start.line || 0,
111
+ column: loc?.start.column || 0,
112
+ suggestion: `Use async version: ${callee.name.replace('Sync', '')} with await or promises`,
113
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
114
+ });
115
+ }
116
+ },
117
+ });
118
+ // Detect missing error handling in async functions
119
+ component.path.traverse({
120
+ AwaitExpression(path) {
121
+ // Check if inside try block
122
+ let insideTry = false;
123
+ let current = path;
124
+ while (current) {
125
+ if (t.isTryStatement(current.node)) {
126
+ insideTry = true;
127
+ break;
128
+ }
129
+ // Stop at function boundary
130
+ if (t.isFunction(current.node))
131
+ break;
132
+ current = current.parentPath;
133
+ }
134
+ if (!insideTry) {
135
+ // Check if the parent function has error handling at call site
136
+ // This is a simplified check - in practice you'd want more context
137
+ const loc = path.node.loc;
138
+ smells.push({
139
+ type: 'nodejs-missing-error-handling',
140
+ severity: 'info',
141
+ message: `await without try/catch may cause unhandled rejections`,
142
+ file: filePath,
143
+ line: loc?.start.line || 0,
144
+ column: loc?.start.column || 0,
145
+ suggestion: 'Wrap await in try/catch or handle errors at the call site',
146
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
147
+ });
148
+ }
149
+ },
150
+ });
151
+ return smells;
152
+ }
153
+ /**
154
+ * Calculate the depth of nested callbacks
155
+ */
156
+ function getCallbackDepth(path) {
157
+ let depth = 0;
158
+ let current = path;
159
+ while (current) {
160
+ const node = current.node;
161
+ // Count function expressions that are arguments to calls
162
+ if ((t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) &&
163
+ t.isCallExpression(current.parent)) {
164
+ depth++;
165
+ }
166
+ current = current.parentPath;
167
+ }
168
+ return depth;
169
+ }
@@ -0,0 +1,10 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects React Native-specific code smells:
5
+ * - Inline styles instead of StyleSheet
6
+ * - Missing accessibility props
7
+ * - Performance anti-patterns
8
+ */
9
+ export declare function detectReactNativeIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig, imports?: string[]): CodeSmell[];
10
+ //# sourceMappingURL=reactNative.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactNative.d.ts","sourceRoot":"","sources":["../../src/detectors/reactNative.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,EACvC,OAAO,GAAE,MAAM,EAAO,GACrB,SAAS,EAAE,CAwIb"}
@@ -0,0 +1,135 @@
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 React Native-specific code smells:
6
+ * - Inline styles instead of StyleSheet
7
+ * - Missing accessibility props
8
+ * - Performance anti-patterns
9
+ */
10
+ export function detectReactNativeIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG, imports = []) {
11
+ if (!config.checkReactNative)
12
+ return [];
13
+ // Only run on React Native projects
14
+ const isRNProject = imports.some(imp => imp.includes('react-native') || imp.includes('@react-native'));
15
+ if (!isRNProject)
16
+ return [];
17
+ const smells = [];
18
+ const inlineStyleLines = new Set();
19
+ // Check for inline styles instead of StyleSheet
20
+ component.path.traverse({
21
+ JSXAttribute(path) {
22
+ if (!t.isJSXIdentifier(path.node.name))
23
+ return;
24
+ if (path.node.name.name !== 'style')
25
+ return;
26
+ const value = path.node.value;
27
+ if (!t.isJSXExpressionContainer(value))
28
+ return;
29
+ // Check if it's an inline object (not a StyleSheet reference)
30
+ if (t.isObjectExpression(value.expression)) {
31
+ const loc = path.node.loc;
32
+ const line = loc?.start.line || 0;
33
+ if (!inlineStyleLines.has(line)) {
34
+ inlineStyleLines.add(line);
35
+ smells.push({
36
+ type: 'rn-inline-style',
37
+ severity: 'warning',
38
+ message: `Inline style object in "${component.name}" - prefer StyleSheet.create()`,
39
+ file: filePath,
40
+ line,
41
+ column: loc?.start.column || 0,
42
+ suggestion: 'Use StyleSheet.create() for better performance: const styles = StyleSheet.create({ ... })',
43
+ codeSnippet: getCodeSnippet(sourceCode, line),
44
+ });
45
+ }
46
+ }
47
+ },
48
+ });
49
+ // Check for missing accessibility props on interactive elements
50
+ const interactiveComponents = ['TouchableOpacity', 'TouchableHighlight', 'Pressable', 'Button', 'TouchableWithoutFeedback'];
51
+ component.path.traverse({
52
+ JSXOpeningElement(path) {
53
+ if (!t.isJSXIdentifier(path.node.name))
54
+ return;
55
+ const componentName = path.node.name.name;
56
+ if (!interactiveComponents.includes(componentName))
57
+ return;
58
+ // Check for accessibility props
59
+ const hasAccessibilityLabel = path.node.attributes.some(attr => {
60
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
61
+ return ['accessibilityLabel', 'accessible', 'accessibilityRole', 'accessibilityHint'].includes(attr.name.name);
62
+ }
63
+ return false;
64
+ });
65
+ if (!hasAccessibilityLabel) {
66
+ const loc = path.node.loc;
67
+ smells.push({
68
+ type: 'rn-missing-accessibility',
69
+ severity: 'info',
70
+ message: `${componentName} missing accessibility props in "${component.name}"`,
71
+ file: filePath,
72
+ line: loc?.start.line || 0,
73
+ column: loc?.start.column || 0,
74
+ suggestion: 'Add accessibilityLabel and accessibilityRole for screen readers',
75
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
76
+ });
77
+ }
78
+ },
79
+ });
80
+ // Check for performance anti-patterns
81
+ component.path.traverse({
82
+ // Detect creating anonymous functions in render (for onPress, etc.)
83
+ JSXAttribute(path) {
84
+ if (!t.isJSXIdentifier(path.node.name))
85
+ return;
86
+ const propName = path.node.name.name;
87
+ if (!['onPress', 'onPressIn', 'onPressOut', 'onLongPress'].includes(propName))
88
+ return;
89
+ const value = path.node.value;
90
+ if (!t.isJSXExpressionContainer(value))
91
+ return;
92
+ // Check for arrow functions or function expressions
93
+ if (t.isArrowFunctionExpression(value.expression) || t.isFunctionExpression(value.expression)) {
94
+ const loc = path.node.loc;
95
+ smells.push({
96
+ type: 'rn-performance-issue',
97
+ severity: 'info',
98
+ message: `Inline function for ${propName} in "${component.name}" creates new reference each render`,
99
+ file: filePath,
100
+ line: loc?.start.line || 0,
101
+ column: loc?.start.column || 0,
102
+ suggestion: 'Extract to useCallback or class method to prevent unnecessary re-renders',
103
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
104
+ });
105
+ }
106
+ },
107
+ // Detect array spreads in render that could be memoized
108
+ SpreadElement(path) {
109
+ const loc = path.node.loc;
110
+ // Only flag if inside JSX or return statement
111
+ let inJSX = false;
112
+ let current = path.parentPath;
113
+ while (current) {
114
+ if (t.isJSXElement(current.node) || t.isJSXFragment(current.node)) {
115
+ inJSX = true;
116
+ break;
117
+ }
118
+ current = current.parentPath;
119
+ }
120
+ if (inJSX && t.isArrayExpression(path.parent)) {
121
+ smells.push({
122
+ type: 'rn-performance-issue',
123
+ severity: 'info',
124
+ message: `Array spread in render may cause performance issues in "${component.name}"`,
125
+ file: filePath,
126
+ line: loc?.start.line || 0,
127
+ column: loc?.start.column || 0,
128
+ suggestion: 'Consider memoizing with useMemo if this array is passed as a prop',
129
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
130
+ });
131
+ }
132
+ },
133
+ });
134
+ return smells;
135
+ }
@@ -0,0 +1,11 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects TypeScript-specific code smells:
5
+ * - Overuse of 'any' type
6
+ * - Missing return type on functions
7
+ * - Non-null assertion operator (!)
8
+ * - Type assertions (as) that could be avoided
9
+ */
10
+ export declare function detectTypescriptIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
11
+ //# sourceMappingURL=typescript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typescript.d.ts","sourceRoot":"","sources":["../../src/detectors/typescript.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,CAsIb"}
@@ -0,0 +1,135 @@
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 TypeScript-specific code smells:
6
+ * - Overuse of 'any' type
7
+ * - Missing return type on functions
8
+ * - Non-null assertion operator (!)
9
+ * - Type assertions (as) that could be avoided
10
+ */
11
+ export function detectTypescriptIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
12
+ if (!config.checkTypescript)
13
+ return [];
14
+ // Only run on TypeScript files
15
+ if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx'))
16
+ return [];
17
+ const smells = [];
18
+ // Detect 'any' type usage
19
+ component.path.traverse({
20
+ TSAnyKeyword(path) {
21
+ const loc = path.node.loc;
22
+ smells.push({
23
+ type: 'ts-any-usage',
24
+ severity: 'warning',
25
+ message: `Using "any" type in "${component.name}"`,
26
+ file: filePath,
27
+ line: loc?.start.line || 0,
28
+ column: loc?.start.column || 0,
29
+ suggestion: 'Use a specific type, "unknown", or create an interface',
30
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
31
+ });
32
+ },
33
+ });
34
+ // Detect functions without return type (only in complex functions)
35
+ component.path.traverse({
36
+ FunctionDeclaration(path) {
37
+ // Skip if function has explicit return type
38
+ if (path.node.returnType)
39
+ return;
40
+ // Check if function body has return statements
41
+ let hasReturn = false;
42
+ path.traverse({
43
+ ReturnStatement(returnPath) {
44
+ if (returnPath.node.argument) {
45
+ hasReturn = true;
46
+ }
47
+ },
48
+ });
49
+ if (hasReturn) {
50
+ const loc = path.node.loc;
51
+ smells.push({
52
+ type: 'ts-missing-return-type',
53
+ severity: 'info',
54
+ message: `Function "${path.node.id?.name || 'anonymous'}" missing return type`,
55
+ file: filePath,
56
+ line: loc?.start.line || 0,
57
+ column: loc?.start.column || 0,
58
+ suggestion: 'Add explicit return type: function name(): ReturnType { ... }',
59
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
60
+ });
61
+ }
62
+ },
63
+ ArrowFunctionExpression(path) {
64
+ // Only check arrow functions assigned to variables with 5+ lines
65
+ if (!path.node.returnType) {
66
+ const body = path.node.body;
67
+ // Skip simple arrow functions (single expression)
68
+ if (!t.isBlockStatement(body))
69
+ return;
70
+ // Check complexity - only flag if function is substantial
71
+ const startLine = path.node.loc?.start.line || 0;
72
+ const endLine = path.node.loc?.end.line || 0;
73
+ if (endLine - startLine >= 5) {
74
+ // Check if parent is variable declarator (assigned to variable)
75
+ const parent = path.parent;
76
+ if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
77
+ const loc = path.node.loc;
78
+ smells.push({
79
+ type: 'ts-missing-return-type',
80
+ severity: 'info',
81
+ message: `Arrow function "${parent.id.name}" missing return type`,
82
+ file: filePath,
83
+ line: loc?.start.line || 0,
84
+ column: loc?.start.column || 0,
85
+ suggestion: 'Add return type: const name = (): ReturnType => { ... }',
86
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
87
+ });
88
+ }
89
+ }
90
+ }
91
+ },
92
+ });
93
+ // Detect non-null assertion operator (!)
94
+ component.path.traverse({
95
+ TSNonNullExpression(path) {
96
+ const loc = path.node.loc;
97
+ smells.push({
98
+ type: 'ts-non-null-assertion',
99
+ severity: 'warning',
100
+ message: `Non-null assertion (!) bypasses type safety in "${component.name}"`,
101
+ file: filePath,
102
+ line: loc?.start.line || 0,
103
+ column: loc?.start.column || 0,
104
+ suggestion: 'Use optional chaining (?.) or proper null checks instead',
105
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
106
+ });
107
+ },
108
+ });
109
+ // Detect type assertions (as keyword) - these can hide type errors
110
+ component.path.traverse({
111
+ TSAsExpression(path) {
112
+ // Skip assertions to 'const' (used for const assertions)
113
+ if (t.isTSTypeReference(path.node.typeAnnotation)) {
114
+ const typeName = path.node.typeAnnotation.typeName;
115
+ if (t.isIdentifier(typeName) && typeName.name === 'const')
116
+ return;
117
+ }
118
+ // Skip double assertions (as unknown as Type) - already flagged by TypeScript
119
+ if (t.isTSAsExpression(path.node.expression))
120
+ return;
121
+ const loc = path.node.loc;
122
+ smells.push({
123
+ type: 'ts-type-assertion',
124
+ severity: 'info',
125
+ message: `Type assertion (as) bypasses type checking in "${component.name}"`,
126
+ file: filePath,
127
+ line: loc?.start.line || 0,
128
+ column: loc?.start.column || 0,
129
+ suggestion: 'Consider using type guards or proper typing instead of assertions',
130
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
131
+ });
132
+ },
133
+ });
134
+ return smells;
135
+ }
package/dist/reporter.js CHANGED
@@ -212,6 +212,36 @@ 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',
221
+ // Next.js
222
+ 'nextjs-client-server-boundary': '▲ Next.js Client/Server Boundary',
223
+ 'nextjs-missing-metadata': '▲ Next.js Missing Metadata',
224
+ 'nextjs-image-unoptimized': '▲ Next.js Unoptimized Image',
225
+ 'nextjs-router-misuse': '▲ Next.js Router Misuse',
226
+ // React Native
227
+ 'rn-inline-style': '📱 RN Inline Style',
228
+ 'rn-missing-accessibility': '📱 RN Missing Accessibility',
229
+ 'rn-performance-issue': '📱 RN Performance Issue',
230
+ // Node.js
231
+ 'nodejs-callback-hell': '🟢 Node.js Callback Hell',
232
+ 'nodejs-unhandled-promise': '🟢 Node.js Unhandled Promise',
233
+ 'nodejs-sync-io': '🟢 Node.js Sync I/O',
234
+ 'nodejs-missing-error-handling': '🟢 Node.js Missing Error Handling',
235
+ // JavaScript
236
+ 'js-var-usage': '📜 JS var Usage',
237
+ 'js-loose-equality': '📜 JS Loose Equality',
238
+ 'js-implicit-coercion': '📜 JS Implicit Coercion',
239
+ 'js-global-pollution': '📜 JS Global Pollution',
240
+ // TypeScript
241
+ 'ts-any-usage': '🔷 TS any Usage',
242
+ 'ts-missing-return-type': '🔷 TS Missing Return Type',
243
+ 'ts-non-null-assertion': '🔷 TS Non-null Assertion',
244
+ 'ts-type-assertion': '🔷 TS Type Assertion',
215
245
  };
216
246
  return labels[type] || type;
217
247
  }
@@ -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' | 'nextjs-client-server-boundary' | 'nextjs-missing-metadata' | 'nextjs-image-unoptimized' | 'nextjs-router-misuse' | 'rn-inline-style' | 'rn-missing-accessibility' | 'rn-performance-issue' | 'nodejs-callback-hell' | 'nodejs-unhandled-promise' | 'nodejs-sync-io' | 'nodejs-missing-error-handling' | 'js-var-usage' | 'js-loose-equality' | 'js-implicit-coercion' | 'js-global-pollution' | 'ts-any-usage' | 'ts-missing-return-type' | 'ts-non-null-assertion' | 'ts-type-assertion';
3
3
  export interface CodeSmell {
4
4
  type: SmellType;
5
5
  severity: SmellSeverity;
@@ -59,6 +59,19 @@ 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;
69
+ checkNextjs: boolean;
70
+ checkReactNative: boolean;
71
+ checkNodejs: boolean;
72
+ checkJavascript: boolean;
73
+ checkTypescript: boolean;
74
+ maxCallbackDepth: number;
62
75
  }
63
76
  export declare const DEFAULT_CONFIG: DetectorConfig;
64
77
  //# 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":"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"}
@@ -4,4 +4,18 @@ 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,
14
+ // Framework detection - auto-enabled based on project
15
+ checkNextjs: true,
16
+ checkReactNative: true,
17
+ checkNodejs: true,
18
+ checkJavascript: true,
19
+ checkTypescript: true,
20
+ maxCallbackDepth: 3,
7
21
  };
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.1.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,17 @@ import {
7
7
  analyzePropDrillingDepth,
8
8
  detectLargeComponent,
9
9
  detectUnmemoizedCalculations,
10
+ detectMissingKeys,
11
+ detectHooksRulesViolations,
12
+ detectDependencyArrayIssues,
13
+ detectNestedTernaries,
14
+ detectDeadCode,
15
+ detectMagicValues,
16
+ detectNextjsIssues,
17
+ detectReactNativeIssues,
18
+ detectNodejsIssues,
19
+ detectJavascriptIssues,
20
+ detectTypescriptIssues,
10
21
  } from './detectors/index.js';
11
22
  import {
12
23
  AnalysisResult,
@@ -100,6 +111,18 @@ function analyzeFile(parseResult: ParseResult, filePath: string, config: Detecto
100
111
  smells.push(...detectPropDrilling(component, filePath, sourceCode, config));
101
112
  smells.push(...detectLargeComponent(component, filePath, sourceCode, config));
102
113
  smells.push(...detectUnmemoizedCalculations(component, filePath, sourceCode, config));
114
+ smells.push(...detectMissingKeys(component, filePath, sourceCode, config));
115
+ smells.push(...detectHooksRulesViolations(component, filePath, sourceCode, config));
116
+ smells.push(...detectDependencyArrayIssues(component, filePath, sourceCode, config));
117
+ smells.push(...detectNestedTernaries(component, filePath, sourceCode, config));
118
+ smells.push(...detectDeadCode(component, filePath, sourceCode, config));
119
+ smells.push(...detectMagicValues(component, filePath, sourceCode, config));
120
+ // Framework-specific detectors
121
+ smells.push(...detectNextjsIssues(component, filePath, sourceCode, config, imports));
122
+ smells.push(...detectReactNativeIssues(component, filePath, sourceCode, config, imports));
123
+ smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
124
+ smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
125
+ smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
103
126
  });
104
127
 
105
128
  // Run cross-component analysis
@@ -123,6 +146,36 @@ function calculateSummary(files: FileAnalysis[]): AnalysisSummary {
123
146
  'state-in-loop': 0,
124
147
  'inline-function-prop': 0,
125
148
  'deep-nesting': 0,
149
+ 'missing-key': 0,
150
+ 'hooks-rules-violation': 0,
151
+ 'dependency-array-issue': 0,
152
+ 'nested-ternary': 0,
153
+ 'dead-code': 0,
154
+ 'magic-value': 0,
155
+ // Next.js
156
+ 'nextjs-client-server-boundary': 0,
157
+ 'nextjs-missing-metadata': 0,
158
+ 'nextjs-image-unoptimized': 0,
159
+ 'nextjs-router-misuse': 0,
160
+ // React Native
161
+ 'rn-inline-style': 0,
162
+ 'rn-missing-accessibility': 0,
163
+ 'rn-performance-issue': 0,
164
+ // Node.js
165
+ 'nodejs-callback-hell': 0,
166
+ 'nodejs-unhandled-promise': 0,
167
+ 'nodejs-sync-io': 0,
168
+ 'nodejs-missing-error-handling': 0,
169
+ // JavaScript
170
+ 'js-var-usage': 0,
171
+ 'js-loose-equality': 0,
172
+ 'js-implicit-coercion': 0,
173
+ 'js-global-pollution': 0,
174
+ // TypeScript
175
+ 'ts-any-usage': 0,
176
+ 'ts-missing-return-type': 0,
177
+ 'ts-non-null-assertion': 0,
178
+ 'ts-type-assertion': 0,
126
179
  };
127
180
 
128
181
  const smellsBySeverity: Record<SmellSeverity, number> = {