react-code-smell-detector 1.2.0 → 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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +18 -1
  5. package/dist/cli.js +93 -26
  6. package/dist/detectors/complexity.d.ts +17 -0
  7. package/dist/detectors/complexity.d.ts.map +1 -0
  8. package/dist/detectors/complexity.js +69 -0
  9. package/dist/detectors/imports.d.ts +22 -0
  10. package/dist/detectors/imports.d.ts.map +1 -0
  11. package/dist/detectors/imports.js +210 -0
  12. package/dist/detectors/index.d.ts +3 -0
  13. package/dist/detectors/index.d.ts.map +1 -1
  14. package/dist/detectors/index.js +4 -0
  15. package/dist/detectors/memoryLeak.d.ts +7 -0
  16. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  17. package/dist/detectors/memoryLeak.js +111 -0
  18. package/dist/fixer.d.ts +23 -0
  19. package/dist/fixer.d.ts.map +1 -0
  20. package/dist/fixer.js +133 -0
  21. package/dist/git.d.ts +28 -0
  22. package/dist/git.d.ts.map +1 -0
  23. package/dist/git.js +117 -0
  24. package/dist/reporter.js +13 -0
  25. package/dist/types/index.d.ts +7 -1
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/index.js +9 -0
  28. package/dist/watcher.d.ts +16 -0
  29. package/dist/watcher.d.ts.map +1 -0
  30. package/dist/watcher.js +89 -0
  31. package/package.json +8 -2
  32. package/src/analyzer.ts +0 -324
  33. package/src/cli.ts +0 -159
  34. package/src/detectors/accessibility.ts +0 -212
  35. package/src/detectors/deadCode.ts +0 -163
  36. package/src/detectors/debug.ts +0 -103
  37. package/src/detectors/dependencyArray.ts +0 -176
  38. package/src/detectors/hooksRules.ts +0 -101
  39. package/src/detectors/index.ts +0 -20
  40. package/src/detectors/javascript.ts +0 -169
  41. package/src/detectors/largeComponent.ts +0 -63
  42. package/src/detectors/magicValues.ts +0 -114
  43. package/src/detectors/memoization.ts +0 -177
  44. package/src/detectors/missingKey.ts +0 -105
  45. package/src/detectors/nestedTernary.ts +0 -75
  46. package/src/detectors/nextjs.ts +0 -124
  47. package/src/detectors/nodejs.ts +0 -199
  48. package/src/detectors/propDrilling.ts +0 -103
  49. package/src/detectors/reactNative.ts +0 -154
  50. package/src/detectors/security.ts +0 -179
  51. package/src/detectors/typescript.ts +0 -151
  52. package/src/detectors/useEffect.ts +0 -117
  53. package/src/htmlReporter.ts +0 -464
  54. package/src/index.ts +0 -4
  55. package/src/parser/index.ts +0 -195
  56. package/src/reporter.ts +0 -291
  57. package/src/types/index.ts +0 -165
  58. package/tsconfig.json +0 -19
@@ -1,75 +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 deeply nested ternary expressions (complex conditional rendering)
8
- */
9
- export function detectNestedTernaries(
10
- component: ParsedComponent,
11
- filePath: string,
12
- sourceCode: string,
13
- config: DetectorConfig = DEFAULT_CONFIG
14
- ): CodeSmell[] {
15
- const smells: CodeSmell[] = [];
16
- const maxDepth = config.maxTernaryDepth;
17
-
18
- component.path.traverse({
19
- ConditionalExpression(path) {
20
- // Only check the outermost ternary
21
- if (isNestedInTernary(path)) return;
22
-
23
- const depth = getTernaryDepth(path.node);
24
-
25
- if (depth > maxDepth) {
26
- const loc = path.node.loc;
27
- smells.push({
28
- type: 'nested-ternary',
29
- severity: depth > maxDepth + 1 ? 'warning' : 'info',
30
- message: `Nested ternary expression (depth: ${depth}) in "${component.name}"`,
31
- file: filePath,
32
- line: loc?.start.line || 0,
33
- column: loc?.start.column || 0,
34
- suggestion: `Refactor to use if/else, switch, or extract into separate components. Max recommended depth: ${maxDepth}`,
35
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
36
- });
37
- }
38
- },
39
- });
40
-
41
- return smells;
42
- }
43
-
44
- /**
45
- * Check if a ternary is nested inside another ternary
46
- */
47
- function isNestedInTernary(path: NodePath): boolean {
48
- let current: NodePath | null = path.parentPath;
49
-
50
- while (current) {
51
- if (t.isConditionalExpression(current.node)) {
52
- return true;
53
- }
54
- current = current.parentPath;
55
- }
56
-
57
- return false;
58
- }
59
-
60
- /**
61
- * Calculate the depth of nested ternary expressions
62
- */
63
- function getTernaryDepth(node: t.ConditionalExpression): number {
64
- let maxChildDepth = 0;
65
-
66
- if (t.isConditionalExpression(node.consequent)) {
67
- maxChildDepth = Math.max(maxChildDepth, getTernaryDepth(node.consequent));
68
- }
69
-
70
- if (t.isConditionalExpression(node.alternate)) {
71
- maxChildDepth = Math.max(maxChildDepth, getTernaryDepth(node.alternate));
72
- }
73
-
74
- return 1 + maxChildDepth;
75
- }
@@ -1,124 +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 Next.js-specific code smells:
7
- * - Missing 'use client' / 'use server' directives
8
- * - Unoptimized images (using <img> instead of next/image)
9
- * - Router misuse patterns
10
- * - Missing metadata exports
11
- */
12
- export function detectNextjsIssues(
13
- component: ParsedComponent,
14
- filePath: string,
15
- sourceCode: string,
16
- config: DetectorConfig = DEFAULT_CONFIG,
17
- imports: string[] = []
18
- ): CodeSmell[] {
19
- if (!config.checkNextjs) return [];
20
-
21
- // Only run on Next.js projects (check for next imports)
22
- const isNextProject = imports.some(imp =>
23
- imp.includes('next/') || imp.includes('next')
24
- );
25
-
26
- // Also check file path patterns
27
- const isAppRouter = filePath.includes('/app/') &&
28
- (filePath.endsWith('page.tsx') || filePath.endsWith('page.jsx') ||
29
- filePath.endsWith('layout.tsx') || filePath.endsWith('layout.jsx'));
30
-
31
- const smells: CodeSmell[] = [];
32
-
33
- // Check for unoptimized images (using <img> instead of next/image)
34
- component.path.traverse({
35
- JSXOpeningElement(path) {
36
- if (t.isJSXIdentifier(path.node.name) && path.node.name.name === 'img') {
37
- const loc = path.node.loc;
38
- smells.push({
39
- type: 'nextjs-image-unoptimized',
40
- severity: 'warning',
41
- message: `Using native <img> instead of next/image in "${component.name}"`,
42
- file: filePath,
43
- line: loc?.start.line || 0,
44
- column: loc?.start.column || 0,
45
- suggestion: 'Use next/image for automatic image optimization: import Image from "next/image"',
46
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
47
- });
48
- }
49
- },
50
- });
51
-
52
- // Check for client-side hooks in server components (App Router)
53
- if (isAppRouter && !sourceCode.includes("'use client'") && !sourceCode.includes('"use client"')) {
54
- const clientHooks = ['useState', 'useEffect', 'useContext', 'useReducer', 'useRef'];
55
- const usedClientHooks: string[] = [];
56
-
57
- component.path.traverse({
58
- CallExpression(path) {
59
- if (t.isIdentifier(path.node.callee) && clientHooks.includes(path.node.callee.name)) {
60
- usedClientHooks.push(path.node.callee.name);
61
- }
62
- },
63
- });
64
-
65
- if (usedClientHooks.length > 0) {
66
- const loc = component.node.loc;
67
- smells.push({
68
- type: 'nextjs-client-server-boundary',
69
- severity: 'error',
70
- message: `Client hooks (${usedClientHooks.join(', ')}) used without 'use client' directive in "${component.name}"`,
71
- file: filePath,
72
- line: loc?.start.line || 1,
73
- column: 0,
74
- suggestion: "Add 'use client' at the top of the file, or move client logic to a separate component",
75
- codeSnippet: getCodeSnippet(sourceCode, 1),
76
- });
77
- }
78
- }
79
-
80
- // Check for missing metadata in page/layout files
81
- if (isAppRouter && filePath.includes('page.')) {
82
- // This would require checking exports, which needs file-level analysis
83
- const hasMetadata = sourceCode.includes('export const metadata') ||
84
- sourceCode.includes('export function generateMetadata');
85
-
86
- if (!hasMetadata && component.name === 'default') {
87
- smells.push({
88
- type: 'nextjs-missing-metadata',
89
- severity: 'info',
90
- message: 'Page component missing metadata export',
91
- file: filePath,
92
- line: 1,
93
- column: 0,
94
- suggestion: 'Add metadata for SEO: export const metadata = { title: "...", description: "..." }',
95
- });
96
- }
97
- }
98
-
99
- // Check for router misuse (using window.location instead of next/router)
100
- component.path.traverse({
101
- MemberExpression(path) {
102
- if (
103
- t.isIdentifier(path.node.object) &&
104
- path.node.object.name === 'window' &&
105
- t.isIdentifier(path.node.property) &&
106
- path.node.property.name === 'location'
107
- ) {
108
- const loc = path.node.loc;
109
- smells.push({
110
- type: 'nextjs-router-misuse',
111
- severity: 'warning',
112
- message: `Using window.location instead of Next.js router in "${component.name}"`,
113
- file: filePath,
114
- line: loc?.start.line || 0,
115
- column: loc?.start.column || 0,
116
- suggestion: 'Use next/navigation: import { useRouter } from "next/navigation"',
117
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
118
- });
119
- }
120
- },
121
- });
122
-
123
- return smells;
124
- }
@@ -1,199 +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 Node.js-specific code smells:
8
- * - Callback hell (deeply nested callbacks)
9
- * - Unhandled promise rejections
10
- * - Synchronous I/O operations
11
- * - Missing error handling
12
- */
13
- export function detectNodejsIssues(
14
- component: ParsedComponent,
15
- filePath: string,
16
- sourceCode: string,
17
- config: DetectorConfig = DEFAULT_CONFIG,
18
- imports: string[] = []
19
- ): CodeSmell[] {
20
- if (!config.checkNodejs) return [];
21
-
22
- // Check if this looks like a Node.js file
23
- const isNodeFile = imports.some(imp =>
24
- imp.includes('fs') || imp.includes('path') || imp.includes('http') ||
25
- imp.includes('express') || imp.includes('child_process') ||
26
- imp.includes('crypto') || imp.includes('os') || imp.includes('stream')
27
- ) || filePath.includes('.server.') || filePath.includes('/api/');
28
-
29
- if (!isNodeFile) return [];
30
-
31
- const smells: CodeSmell[] = [];
32
-
33
- // Detect callback hell (nested callbacks > maxCallbackDepth)
34
- component.path.traverse({
35
- CallExpression(path) {
36
- const depth = getCallbackDepth(path);
37
-
38
- if (depth > config.maxCallbackDepth) {
39
- const loc = path.node.loc;
40
- smells.push({
41
- type: 'nodejs-callback-hell',
42
- severity: 'warning',
43
- message: `Callback hell detected (depth: ${depth}) in "${component.name}"`,
44
- file: filePath,
45
- line: loc?.start.line || 0,
46
- column: loc?.start.column || 0,
47
- suggestion: 'Refactor to async/await or use Promise.all() for parallel operations',
48
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
49
- });
50
- }
51
- },
52
- });
53
-
54
- // Detect unhandled promise rejections (Promise without .catch or try/catch)
55
- component.path.traverse({
56
- CallExpression(path) {
57
- // Check for .then() without .catch()
58
- if (t.isMemberExpression(path.node.callee) &&
59
- t.isIdentifier(path.node.callee.property) &&
60
- path.node.callee.property.name === 'then') {
61
-
62
- // Check if followed by .catch() in chain
63
- const parent = path.parent;
64
- let hasCatch = false;
65
-
66
- if (t.isMemberExpression(parent)) {
67
- const prop = (parent as t.MemberExpression).property;
68
- if (t.isIdentifier(prop) && prop.name === 'catch') {
69
- hasCatch = true;
70
- }
71
- }
72
-
73
- // Check if inside try block
74
- let current: NodePath | null = path;
75
- while (current) {
76
- if (t.isTryStatement(current.node)) {
77
- hasCatch = true;
78
- break;
79
- }
80
- current = current.parentPath;
81
- }
82
-
83
- if (!hasCatch) {
84
- const loc = path.node.loc;
85
- smells.push({
86
- type: 'nodejs-unhandled-promise',
87
- severity: 'warning',
88
- message: `.then() without .catch() in "${component.name}"`,
89
- file: filePath,
90
- line: loc?.start.line || 0,
91
- column: loc?.start.column || 0,
92
- suggestion: 'Add .catch() to handle rejections, or use try/catch with async/await',
93
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
94
- });
95
- }
96
- }
97
- },
98
- });
99
-
100
- // Detect synchronous file operations
101
- const syncMethods = ['readFileSync', 'writeFileSync', 'appendFileSync', 'readdirSync',
102
- 'statSync', 'mkdirSync', 'rmdirSync', 'unlinkSync', 'existsSync'];
103
-
104
- component.path.traverse({
105
- CallExpression(path) {
106
- const callee = path.node.callee;
107
-
108
- if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
109
- if (syncMethods.includes(callee.property.name)) {
110
- const loc = path.node.loc;
111
- smells.push({
112
- type: 'nodejs-sync-io',
113
- severity: 'warning',
114
- message: `Synchronous file operation "${callee.property.name}" blocks event loop`,
115
- file: filePath,
116
- line: loc?.start.line || 0,
117
- column: loc?.start.column || 0,
118
- suggestion: `Use async version: ${callee.property.name.replace('Sync', '')} with await or promises`,
119
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
120
- });
121
- }
122
- }
123
-
124
- // Direct function call
125
- if (t.isIdentifier(callee) && syncMethods.includes(callee.name)) {
126
- const loc = path.node.loc;
127
- smells.push({
128
- type: 'nodejs-sync-io',
129
- severity: 'warning',
130
- message: `Synchronous file operation "${callee.name}" blocks event loop`,
131
- file: filePath,
132
- line: loc?.start.line || 0,
133
- column: loc?.start.column || 0,
134
- suggestion: `Use async version: ${callee.name.replace('Sync', '')} with await or promises`,
135
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
136
- });
137
- }
138
- },
139
- });
140
-
141
- // Detect missing error handling in async functions
142
- component.path.traverse({
143
- AwaitExpression(path) {
144
- // Check if inside try block
145
- let insideTry = false;
146
- let current: NodePath | null = path;
147
-
148
- while (current) {
149
- if (t.isTryStatement(current.node)) {
150
- insideTry = true;
151
- break;
152
- }
153
- // Stop at function boundary
154
- if (t.isFunction(current.node)) break;
155
- current = current.parentPath;
156
- }
157
-
158
- if (!insideTry) {
159
- // Check if the parent function has error handling at call site
160
- // This is a simplified check - in practice you'd want more context
161
- const loc = path.node.loc;
162
- smells.push({
163
- type: 'nodejs-missing-error-handling',
164
- severity: 'info',
165
- message: `await without try/catch may cause unhandled rejections`,
166
- file: filePath,
167
- line: loc?.start.line || 0,
168
- column: loc?.start.column || 0,
169
- suggestion: 'Wrap await in try/catch or handle errors at the call site',
170
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
171
- });
172
- }
173
- },
174
- });
175
-
176
- return smells;
177
- }
178
-
179
- /**
180
- * Calculate the depth of nested callbacks
181
- */
182
- function getCallbackDepth(path: NodePath): number {
183
- let depth = 0;
184
- let current: NodePath | null = path;
185
-
186
- while (current) {
187
- const node = current.node;
188
-
189
- // Count function expressions that are arguments to calls
190
- if ((t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) &&
191
- t.isCallExpression(current.parent)) {
192
- depth++;
193
- }
194
-
195
- current = current.parentPath;
196
- }
197
-
198
- return depth;
199
- }
@@ -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
- }