react-code-smell-detector 1.1.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -11
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +65 -2
  5. package/dist/cli.js +134 -33
  6. package/dist/detectors/accessibility.d.ts +12 -0
  7. package/dist/detectors/accessibility.d.ts.map +1 -0
  8. package/dist/detectors/accessibility.js +191 -0
  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/debug.d.ts +10 -0
  13. package/dist/detectors/debug.d.ts.map +1 -0
  14. package/dist/detectors/debug.js +87 -0
  15. package/dist/detectors/imports.d.ts +22 -0
  16. package/dist/detectors/imports.d.ts.map +1 -0
  17. package/dist/detectors/imports.js +210 -0
  18. package/dist/detectors/index.d.ts +6 -0
  19. package/dist/detectors/index.d.ts.map +1 -1
  20. package/dist/detectors/index.js +8 -0
  21. package/dist/detectors/memoryLeak.d.ts +7 -0
  22. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  23. package/dist/detectors/memoryLeak.js +111 -0
  24. package/dist/detectors/security.d.ts +12 -0
  25. package/dist/detectors/security.d.ts.map +1 -0
  26. package/dist/detectors/security.js +161 -0
  27. package/dist/fixer.d.ts +23 -0
  28. package/dist/fixer.d.ts.map +1 -0
  29. package/dist/fixer.js +133 -0
  30. package/dist/git.d.ts +28 -0
  31. package/dist/git.d.ts.map +1 -0
  32. package/dist/git.js +117 -0
  33. package/dist/htmlReporter.d.ts +6 -0
  34. package/dist/htmlReporter.d.ts.map +1 -0
  35. package/dist/htmlReporter.js +453 -0
  36. package/dist/reporter.js +26 -0
  37. package/dist/types/index.d.ts +10 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/index.js +13 -0
  40. package/dist/watcher.d.ts +16 -0
  41. package/dist/watcher.d.ts.map +1 -0
  42. package/dist/watcher.js +89 -0
  43. package/package.json +8 -2
  44. package/src/analyzer.ts +0 -269
  45. package/src/cli.ts +0 -125
  46. package/src/detectors/deadCode.ts +0 -163
  47. package/src/detectors/dependencyArray.ts +0 -176
  48. package/src/detectors/hooksRules.ts +0 -101
  49. package/src/detectors/index.ts +0 -16
  50. package/src/detectors/javascript.ts +0 -169
  51. package/src/detectors/largeComponent.ts +0 -63
  52. package/src/detectors/magicValues.ts +0 -114
  53. package/src/detectors/memoization.ts +0 -177
  54. package/src/detectors/missingKey.ts +0 -105
  55. package/src/detectors/nestedTernary.ts +0 -75
  56. package/src/detectors/nextjs.ts +0 -124
  57. package/src/detectors/nodejs.ts +0 -199
  58. package/src/detectors/propDrilling.ts +0 -103
  59. package/src/detectors/reactNative.ts +0 -154
  60. package/src/detectors/typescript.ts +0 -151
  61. package/src/detectors/useEffect.ts +0 -117
  62. package/src/index.ts +0 -4
  63. package/src/parser/index.ts +0 -195
  64. package/src/reporter.ts +0 -278
  65. package/src/types/index.ts +0 -144
  66. package/tsconfig.json +0 -19
@@ -1,103 +0,0 @@
1
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
2
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
3
-
4
- export function detectPropDrilling(
5
- component: ParsedComponent,
6
- filePath: string,
7
- sourceCode: string,
8
- config: DetectorConfig = DEFAULT_CONFIG
9
- ): CodeSmell[] {
10
- const smells: CodeSmell[] = [];
11
-
12
- // Check for too many props
13
- if (component.props.length > config.maxPropsCount) {
14
- smells.push({
15
- type: 'prop-drilling',
16
- severity: 'warning',
17
- message: `Component "${component.name}" receives ${component.props.length} props (max recommended: ${config.maxPropsCount})`,
18
- file: filePath,
19
- line: component.startLine,
20
- column: 0,
21
- suggestion: 'Consider using Context, a state management library, or component composition',
22
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
23
- });
24
- }
25
-
26
- // Check for props that are just passed through (prop drilling indicators)
27
- const passThroughProps = component.props.filter(prop => {
28
- // Common patterns that indicate prop drilling
29
- const drillingPatterns = ['data', 'config', 'settings', 'options', 'state', 'handlers', 'callbacks'];
30
- return drillingPatterns.some(pattern => prop.toLowerCase().includes(pattern));
31
- });
32
-
33
- if (passThroughProps.length >= 3) {
34
- smells.push({
35
- type: 'prop-drilling',
36
- severity: 'info',
37
- message: `Component "${component.name}" may be experiencing prop drilling: ${passThroughProps.join(', ')}`,
38
- file: filePath,
39
- line: component.startLine,
40
- column: 0,
41
- suggestion: 'Consider using React Context or a state management solution to avoid passing data through intermediate components',
42
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
43
- });
44
- }
45
-
46
- // Check for spread props (often indicates forwarding)
47
- const spreadProps = component.props.filter(p => p.startsWith('...'));
48
- if (spreadProps.length > 0 && component.props.length > 5) {
49
- smells.push({
50
- type: 'prop-drilling',
51
- severity: 'info',
52
- message: `Component "${component.name}" uses spread props with many other props - potential prop drilling`,
53
- file: filePath,
54
- line: component.startLine,
55
- column: 0,
56
- suggestion: 'Consider composing components differently or using Context for shared state',
57
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
58
- });
59
- }
60
-
61
- return smells;
62
- }
63
-
64
- export function analyzePropDrillingDepth(
65
- components: ParsedComponent[],
66
- filePath: string,
67
- sourceCode: string,
68
- config: DetectorConfig = DEFAULT_CONFIG
69
- ): CodeSmell[] {
70
- const smells: CodeSmell[] = [];
71
-
72
- // Track props that appear in multiple components
73
- const propUsage = new Map<string, string[]>();
74
-
75
- components.forEach(comp => {
76
- comp.props.forEach(prop => {
77
- if (!prop.startsWith('...')) {
78
- const existing = propUsage.get(prop) || [];
79
- existing.push(comp.name);
80
- propUsage.set(prop, existing);
81
- }
82
- });
83
- });
84
-
85
- // Find props used in more than maxPropDrillingDepth components
86
- propUsage.forEach((componentNames, propName) => {
87
- if (componentNames.length > config.maxPropDrillingDepth) {
88
- const firstComp = components.find(c => c.name === componentNames[0]);
89
- smells.push({
90
- type: 'prop-drilling',
91
- severity: 'warning',
92
- message: `Prop "${propName}" is passed through ${componentNames.length} components: ${componentNames.join(' → ')}`,
93
- file: filePath,
94
- line: firstComp?.startLine || 1,
95
- column: 0,
96
- suggestion: `Move "${propName}" to Context or a state management solution`,
97
- codeSnippet: firstComp ? getCodeSnippet(sourceCode, firstComp.startLine) : undefined,
98
- });
99
- }
100
- });
101
-
102
- return smells;
103
- }
@@ -1,154 +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 React Native-specific code smells:
8
- * - Inline styles instead of StyleSheet
9
- * - Missing accessibility props
10
- * - Performance anti-patterns
11
- */
12
- export function detectReactNativeIssues(
13
- component: ParsedComponent,
14
- filePath: string,
15
- sourceCode: string,
16
- config: DetectorConfig = DEFAULT_CONFIG,
17
- imports: string[] = []
18
- ): CodeSmell[] {
19
- if (!config.checkReactNative) return [];
20
-
21
- // Only run on React Native projects
22
- const isRNProject = imports.some(imp =>
23
- imp.includes('react-native') || imp.includes('@react-native')
24
- );
25
-
26
- if (!isRNProject) return [];
27
-
28
- const smells: CodeSmell[] = [];
29
- const inlineStyleLines = new Set<number>();
30
-
31
- // Check for inline styles instead of StyleSheet
32
- component.path.traverse({
33
- JSXAttribute(path) {
34
- if (!t.isJSXIdentifier(path.node.name)) return;
35
- if (path.node.name.name !== 'style') return;
36
-
37
- const value = path.node.value;
38
- if (!t.isJSXExpressionContainer(value)) return;
39
-
40
- // Check if it's an inline object (not a StyleSheet reference)
41
- if (t.isObjectExpression(value.expression)) {
42
- const loc = path.node.loc;
43
- const line = loc?.start.line || 0;
44
-
45
- if (!inlineStyleLines.has(line)) {
46
- inlineStyleLines.add(line);
47
- smells.push({
48
- type: 'rn-inline-style',
49
- severity: 'warning',
50
- message: `Inline style object in "${component.name}" - prefer StyleSheet.create()`,
51
- file: filePath,
52
- line,
53
- column: loc?.start.column || 0,
54
- suggestion: 'Use StyleSheet.create() for better performance: const styles = StyleSheet.create({ ... })',
55
- codeSnippet: getCodeSnippet(sourceCode, line),
56
- });
57
- }
58
- }
59
- },
60
- });
61
-
62
- // Check for missing accessibility props on interactive elements
63
- const interactiveComponents = ['TouchableOpacity', 'TouchableHighlight', 'Pressable', 'Button', 'TouchableWithoutFeedback'];
64
-
65
- component.path.traverse({
66
- JSXOpeningElement(path) {
67
- if (!t.isJSXIdentifier(path.node.name)) return;
68
-
69
- const componentName = path.node.name.name;
70
- if (!interactiveComponents.includes(componentName)) return;
71
-
72
- // Check for accessibility props
73
- const hasAccessibilityLabel = path.node.attributes.some(attr => {
74
- if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
75
- return ['accessibilityLabel', 'accessible', 'accessibilityRole', 'accessibilityHint'].includes(attr.name.name);
76
- }
77
- return false;
78
- });
79
-
80
- if (!hasAccessibilityLabel) {
81
- const loc = path.node.loc;
82
- smells.push({
83
- type: 'rn-missing-accessibility',
84
- severity: 'info',
85
- message: `${componentName} missing accessibility props in "${component.name}"`,
86
- file: filePath,
87
- line: loc?.start.line || 0,
88
- column: loc?.start.column || 0,
89
- suggestion: 'Add accessibilityLabel and accessibilityRole for screen readers',
90
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
91
- });
92
- }
93
- },
94
- });
95
-
96
- // Check for performance anti-patterns
97
- component.path.traverse({
98
- // Detect creating anonymous functions in render (for onPress, etc.)
99
- JSXAttribute(path) {
100
- if (!t.isJSXIdentifier(path.node.name)) return;
101
-
102
- const propName = path.node.name.name;
103
- if (!['onPress', 'onPressIn', 'onPressOut', 'onLongPress'].includes(propName)) return;
104
-
105
- const value = path.node.value;
106
- if (!t.isJSXExpressionContainer(value)) return;
107
-
108
- // Check for arrow functions or function expressions
109
- if (t.isArrowFunctionExpression(value.expression) || t.isFunctionExpression(value.expression)) {
110
- const loc = path.node.loc;
111
- smells.push({
112
- type: 'rn-performance-issue',
113
- severity: 'info',
114
- message: `Inline function for ${propName} in "${component.name}" creates new reference each render`,
115
- file: filePath,
116
- line: loc?.start.line || 0,
117
- column: loc?.start.column || 0,
118
- suggestion: 'Extract to useCallback or class method to prevent unnecessary re-renders',
119
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
120
- });
121
- }
122
- },
123
-
124
- // Detect array spreads in render that could be memoized
125
- SpreadElement(path) {
126
- const loc = path.node.loc;
127
- // Only flag if inside JSX or return statement
128
- let inJSX = false;
129
- let current: NodePath | null = path.parentPath;
130
- while (current) {
131
- if (t.isJSXElement(current.node) || t.isJSXFragment(current.node)) {
132
- inJSX = true;
133
- break;
134
- }
135
- current = current.parentPath;
136
- }
137
-
138
- if (inJSX && t.isArrayExpression(path.parent)) {
139
- smells.push({
140
- type: 'rn-performance-issue',
141
- severity: 'info',
142
- message: `Array spread in render may cause performance issues in "${component.name}"`,
143
- file: filePath,
144
- line: loc?.start.line || 0,
145
- column: loc?.start.column || 0,
146
- suggestion: 'Consider memoizing with useMemo if this array is passed as a prop',
147
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
148
- });
149
- }
150
- },
151
- });
152
-
153
- return smells;
154
- }
@@ -1,151 +0,0 @@
1
- import * as t from '@babel/types';
2
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
3
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
4
-
5
- /**
6
- * Detects TypeScript-specific code smells:
7
- * - Overuse of 'any' type
8
- * - Missing return type on functions
9
- * - Non-null assertion operator (!)
10
- * - Type assertions (as) that could be avoided
11
- */
12
- export function detectTypescriptIssues(
13
- component: ParsedComponent,
14
- filePath: string,
15
- sourceCode: string,
16
- config: DetectorConfig = DEFAULT_CONFIG
17
- ): CodeSmell[] {
18
- if (!config.checkTypescript) return [];
19
-
20
- // Only run on TypeScript files
21
- if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx')) return [];
22
-
23
- const smells: CodeSmell[] = [];
24
-
25
- // Detect 'any' type usage
26
- component.path.traverse({
27
- TSAnyKeyword(path) {
28
- const loc = path.node.loc;
29
- smells.push({
30
- type: 'ts-any-usage',
31
- severity: 'warning',
32
- message: `Using "any" type in "${component.name}"`,
33
- file: filePath,
34
- line: loc?.start.line || 0,
35
- column: loc?.start.column || 0,
36
- suggestion: 'Use a specific type, "unknown", or create an interface',
37
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
38
- });
39
- },
40
- });
41
-
42
- // Detect functions without return type (only in complex functions)
43
- component.path.traverse({
44
- FunctionDeclaration(path) {
45
- // Skip if function has explicit return type
46
- if (path.node.returnType) return;
47
-
48
- // Check if function body has return statements
49
- let hasReturn = false;
50
- path.traverse({
51
- ReturnStatement(returnPath) {
52
- if (returnPath.node.argument) {
53
- hasReturn = true;
54
- }
55
- },
56
- });
57
-
58
- if (hasReturn) {
59
- const loc = path.node.loc;
60
- smells.push({
61
- type: 'ts-missing-return-type',
62
- severity: 'info',
63
- message: `Function "${path.node.id?.name || 'anonymous'}" missing return type`,
64
- file: filePath,
65
- line: loc?.start.line || 0,
66
- column: loc?.start.column || 0,
67
- suggestion: 'Add explicit return type: function name(): ReturnType { ... }',
68
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
69
- });
70
- }
71
- },
72
-
73
- ArrowFunctionExpression(path) {
74
- // Only check arrow functions assigned to variables with 5+ lines
75
- if (!path.node.returnType) {
76
- const body = path.node.body;
77
-
78
- // Skip simple arrow functions (single expression)
79
- if (!t.isBlockStatement(body)) return;
80
-
81
- // Check complexity - only flag if function is substantial
82
- const startLine = path.node.loc?.start.line || 0;
83
- const endLine = path.node.loc?.end.line || 0;
84
-
85
- if (endLine - startLine >= 5) {
86
- // Check if parent is variable declarator (assigned to variable)
87
- const parent = path.parent;
88
-
89
- if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
90
- const loc = path.node.loc;
91
- smells.push({
92
- type: 'ts-missing-return-type',
93
- severity: 'info',
94
- message: `Arrow function "${parent.id.name}" missing return type`,
95
- file: filePath,
96
- line: loc?.start.line || 0,
97
- column: loc?.start.column || 0,
98
- suggestion: 'Add return type: const name = (): ReturnType => { ... }',
99
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
100
- });
101
- }
102
- }
103
- }
104
- },
105
- });
106
-
107
- // Detect non-null assertion operator (!)
108
- component.path.traverse({
109
- TSNonNullExpression(path) {
110
- const loc = path.node.loc;
111
- smells.push({
112
- type: 'ts-non-null-assertion',
113
- severity: 'warning',
114
- message: `Non-null assertion (!) bypasses type safety in "${component.name}"`,
115
- file: filePath,
116
- line: loc?.start.line || 0,
117
- column: loc?.start.column || 0,
118
- suggestion: 'Use optional chaining (?.) or proper null checks instead',
119
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
120
- });
121
- },
122
- });
123
-
124
- // Detect type assertions (as keyword) - these can hide type errors
125
- component.path.traverse({
126
- TSAsExpression(path) {
127
- // Skip assertions to 'const' (used for const assertions)
128
- if (t.isTSTypeReference(path.node.typeAnnotation)) {
129
- const typeName = path.node.typeAnnotation.typeName;
130
- if (t.isIdentifier(typeName) && typeName.name === 'const') return;
131
- }
132
-
133
- // Skip double assertions (as unknown as Type) - already flagged by TypeScript
134
- if (t.isTSAsExpression(path.node.expression)) return;
135
-
136
- const loc = path.node.loc;
137
- smells.push({
138
- type: 'ts-type-assertion',
139
- severity: 'info',
140
- message: `Type assertion (as) bypasses type checking in "${component.name}"`,
141
- file: filePath,
142
- line: loc?.start.line || 0,
143
- column: loc?.start.column || 0,
144
- suggestion: 'Consider using type guards or proper typing instead of assertions',
145
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
146
- });
147
- },
148
- });
149
-
150
- return smells;
151
- }
@@ -1,117 +0,0 @@
1
- import * as t from '@babel/types';
2
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
3
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
4
-
5
- export function detectUseEffectOveruse(
6
- component: ParsedComponent,
7
- filePath: string,
8
- sourceCode: string,
9
- config: DetectorConfig = DEFAULT_CONFIG
10
- ): CodeSmell[] {
11
- const smells: CodeSmell[] = [];
12
- const { useEffect } = component.hooks;
13
-
14
- // Check for too many useEffects
15
- if (useEffect.length > config.maxUseEffectsPerComponent) {
16
- smells.push({
17
- type: 'useEffect-overuse',
18
- severity: 'warning',
19
- message: `Component "${component.name}" has ${useEffect.length} useEffect hooks (max recommended: ${config.maxUseEffectsPerComponent})`,
20
- file: filePath,
21
- line: component.startLine,
22
- column: 0,
23
- suggestion: 'Consider extracting logic into custom hooks or combining related effects',
24
- codeSnippet: getCodeSnippet(sourceCode, component.startLine),
25
- });
26
- }
27
-
28
- // Check each useEffect for issues
29
- useEffect.forEach((effect, index) => {
30
- const loc = effect.loc;
31
- if (!loc) return;
32
-
33
- // Check for empty dependency array with async operations
34
- const deps = effect.arguments[1];
35
- const callback = effect.arguments[0];
36
-
37
- if (t.isArrayExpression(deps) && deps.elements.length === 0) {
38
- // Empty dependency array - check if it's justified
39
- let hasAsyncOperation = false;
40
- let hasStateUpdate = false;
41
-
42
- if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
43
- const body = callback.body;
44
- const checkNode = (node: t.Node) => {
45
- if (t.isAwaitExpression(node)) hasAsyncOperation = true;
46
- if (t.isCallExpression(node)) {
47
- const callee = node.callee;
48
- if (t.isIdentifier(callee) && callee.name.startsWith('set')) {
49
- hasStateUpdate = true;
50
- }
51
- }
52
- };
53
-
54
- if (t.isBlockStatement(body)) {
55
- body.body.forEach(stmt => {
56
- t.traverseFast(stmt, checkNode);
57
- });
58
- }
59
- }
60
-
61
- if (hasAsyncOperation && hasStateUpdate) {
62
- smells.push({
63
- type: 'useEffect-overuse',
64
- severity: 'info',
65
- message: `useEffect #${index + 1} in "${component.name}" has empty deps but contains async state updates`,
66
- file: filePath,
67
- line: loc.start.line,
68
- column: loc.start.column,
69
- suggestion: 'Consider using React Query, SWR, or a custom hook for data fetching',
70
- codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
71
- });
72
- }
73
- }
74
-
75
- // Check for missing cleanup
76
- if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
77
- let hasSubscription = false;
78
- let hasCleanup = false;
79
-
80
- const checkSubscription = (node: t.Node) => {
81
- if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
82
- const prop = node.callee.property;
83
- if (t.isIdentifier(prop)) {
84
- if (['addEventListener', 'subscribe', 'on', 'setInterval', 'setTimeout'].includes(prop.name)) {
85
- hasSubscription = true;
86
- }
87
- }
88
- }
89
- };
90
-
91
- const body = callback.body;
92
- if (t.isBlockStatement(body)) {
93
- body.body.forEach(stmt => {
94
- t.traverseFast(stmt, checkSubscription);
95
- if (t.isReturnStatement(stmt) && stmt.argument) {
96
- hasCleanup = true;
97
- }
98
- });
99
- }
100
-
101
- if (hasSubscription && !hasCleanup) {
102
- smells.push({
103
- type: 'useEffect-overuse',
104
- severity: 'error',
105
- message: `useEffect in "${component.name}" sets up subscription but has no cleanup function`,
106
- file: filePath,
107
- line: loc.start.line,
108
- column: loc.start.column,
109
- suggestion: 'Add a cleanup function to remove event listeners/subscriptions',
110
- codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
111
- });
112
- }
113
- }
114
- });
115
-
116
- return smells;
117
- }
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export { analyzeProject, type AnalyzerOptions } from './analyzer.js';
2
- export { reportResults, type ReporterOptions } from './reporter.js';
3
- export * from './types/index.js';
4
- export { parseFile, parseCode, type ParseResult, type ParsedComponent } from './parser/index.js';