react-doctor-cli-dev 1.0.0

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 (82) hide show
  1. package/backend/.env +3 -0
  2. package/backend/dist/index.js +43 -0
  3. package/backend/dist/middleware/auth.js +16 -0
  4. package/backend/dist/routes/reports.js +93 -0
  5. package/backend/package-lock.json +2000 -0
  6. package/backend/package.json +30 -0
  7. package/backend/src/db.ts +24 -0
  8. package/backend/src/index.ts +49 -0
  9. package/backend/src/middleware/auth.ts +21 -0
  10. package/backend/src/routes/reports.ts +110 -0
  11. package/backend/tsconfig.json +12 -0
  12. package/cli/bin/react-doctor.js +29 -0
  13. package/cli/dist/commands/analyze.js +125 -0
  14. package/cli/dist/commands/full.js +366 -0
  15. package/cli/dist/commands/install.js +138 -0
  16. package/cli/dist/commands/profile.js +166 -0
  17. package/cli/dist/index.js +78 -0
  18. package/cli/dist/ui.js +113 -0
  19. package/cli/package-lock.json +936 -0
  20. package/cli/package.json +34 -0
  21. package/cli/src/commands/analyze.ts +162 -0
  22. package/cli/src/commands/full.ts +574 -0
  23. package/cli/src/commands/install.ts +163 -0
  24. package/cli/src/commands/profile.ts +246 -0
  25. package/cli/src/index.ts +84 -0
  26. package/cli/src/ui.ts +120 -0
  27. package/cli/tsconfig.json +16 -0
  28. package/core/report-compiler/index.ts +359 -0
  29. package/core/report-compiler/test-report-compiler.ts +126 -0
  30. package/core/rule-engine/context-builder.ts +146 -0
  31. package/core/rule-engine/evaluator.ts +131 -0
  32. package/core/rule-engine/index.ts +222 -0
  33. package/core/rule-engine/rules.json +304 -0
  34. package/core/rule-engine/suggestion-builder.ts +209 -0
  35. package/core/rule-engine/test-rule-engine.ts +144 -0
  36. package/core/rule-engine/types.ts +202 -0
  37. package/core/runtime/profiler/browser.ts +121 -0
  38. package/core/runtime/profiler/collectors.ts +216 -0
  39. package/core/runtime/profiler/index.ts +311 -0
  40. package/core/runtime/profiler/porfiler.ts +967 -0
  41. package/core/runtime/profiler/route-scanner.ts +76 -0
  42. package/core/runtime/profiler/score.ts +59 -0
  43. package/core/runtime/profiler/server.ts +115 -0
  44. package/core/runtime/profiler/types.ts +65 -0
  45. package/core/runtime/test-runtime-profiler.ts +226 -0
  46. package/core/static-ana/static/analyzer.ts +145 -0
  47. package/core/static-ana/static/ast-parser.ts +31 -0
  48. package/core/static-ana/static/detectors/console-log.ts +49 -0
  49. package/core/static-ana/static/detectors/dead-code.ts +51 -0
  50. package/core/static-ana/static/detectors/effect-loop.ts +45 -0
  51. package/core/static-ana/static/detectors/index.ts +16 -0
  52. package/core/static-ana/static/detectors/inline-function.ts +59 -0
  53. package/core/static-ana/static/detectors/inline-style.ts +52 -0
  54. package/core/static-ana/static/detectors/large-component.ts +79 -0
  55. package/core/static-ana/static/detectors/missing-key.ts +56 -0
  56. package/core/static-ana/static/detectors/missing-memo.ts +59 -0
  57. package/core/static-ana/static/detectors/prop-drilling.ts +66 -0
  58. package/core/static-ana/static/helpers.ts +81 -0
  59. package/core/static-ana/static/scanner.ts +93 -0
  60. package/core/static-ana/test-analyzer.ts +115 -0
  61. package/core/static-ana/types.ts +25 -0
  62. package/core/tests/mock-react-project/src/app.tsx +22 -0
  63. package/core/tests/mock-react-project/src/components/Button.tsx +9 -0
  64. package/core/tests/mock-react-project/src/components/Header.tsx +3 -0
  65. package/core/tests/mock-react-project/src/components/ListTesting.tsx +51 -0
  66. package/core/tests/mock-react-project/src/components/UserDashboard.tsx +66 -0
  67. package/core/tests/mock-react-project/src/utils.ts +4 -0
  68. package/package.json +55 -0
  69. package/react-doctor-cli-dev-1.0.0.tgz +0 -0
  70. package/shared/dist/index.d.ts +2 -0
  71. package/shared/dist/index.js +19 -0
  72. package/shared/dist/schemas.d.ts +91 -0
  73. package/shared/dist/schemas.js +82 -0
  74. package/shared/dist/types.d.ts +44 -0
  75. package/shared/dist/types.js +2 -0
  76. package/shared/package-lock.json +47 -0
  77. package/shared/package.json +21 -0
  78. package/shared/src/index.ts +4 -0
  79. package/shared/src/schemas.ts +136 -0
  80. package/shared/src/types.ts +137 -0
  81. package/shared/tsconfig.json +15 -0
  82. package/tsconfig.json +25 -0
@@ -0,0 +1,31 @@
1
+ import * as parser from '@babel/parser';
2
+ import { File } from '@babel/types';
3
+
4
+ /**
5
+ * AST Parser - Converts JavaScript/TypeScript code into an Abstract Syntax Tree
6
+ */
7
+ export class ASTParser {
8
+ /**
9
+ * Parse source code into an AST
10
+ * @param code - Source code as string
11
+ * @param filePath - File path (for better error messages)
12
+ * @returns Parsed AST
13
+ */
14
+ parse(code: string, filePath: string): File {
15
+ try {
16
+ return parser.parse(code, {
17
+ sourceType: 'module',
18
+ plugins: [
19
+ 'jsx', // Parse JSX syntax
20
+ 'typescript', // Parse TypeScript syntax
21
+ 'decorators-legacy', // Support decorators
22
+ 'classProperties', // Support class properties
23
+ 'dynamicImport', // Support dynamic imports
24
+ ],
25
+ });
26
+ } catch (error) {
27
+ const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
28
+ throw new Error(`Failed to parse ${filePath}: ${errorMessage}`);
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,49 @@
1
+ import traverse from '@babel/traverse';
2
+ import { File } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { getParentComponentName, generateIssueId } from '../helpers';
5
+
6
+ /**
7
+ * Detect console.log, console.warn, console.error statements
8
+ *
9
+ * Why this is bad:
10
+ * - Performance impact in production
11
+ * - Exposes internal information
12
+ * - Clutters browser console
13
+ */
14
+ export function detectConsoleLogs(ast: File, filePath: string): ComponentIssue[] {
15
+ const issues: ComponentIssue[] = [];
16
+
17
+ traverse(ast, {
18
+ CallExpression(path) { // 1. Guard finds a function call
19
+ const node = path.node;
20
+
21
+ // Check if it's a console.* call
22
+ if (
23
+ node.callee.type === 'MemberExpression' && // 2. Is it a "Something.Something"?
24
+ node.callee.object.type === 'Identifier' &&
25
+ node.callee.object.name === 'console' // 3. Is the first "Something" 'console'?
26
+ ) {
27
+ const method = node.callee.property.type === 'Identifier'
28
+ ? node.callee.property.name
29
+ : 'unknown';
30
+
31
+ const line = node.loc?.start.line || 0;
32
+ const component = getParentComponentName(path);
33
+
34
+ issues.push({
35
+ id: generateIssueId('console', filePath, line),
36
+ component,
37
+ file: filePath,
38
+ line,
39
+ column: node.loc?.start.column,
40
+ severity: 'info',
41
+ message: `console.${method}() statement found`,
42
+ suggestion: 'Remove console statements before deploying to production',
43
+ });
44
+ }
45
+ },
46
+ });
47
+
48
+ return issues;
49
+ }
@@ -0,0 +1,51 @@
1
+ import traverse from '@babel/traverse';
2
+ import { File } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { generateIssueId } from '../helpers';
5
+
6
+ /**
7
+ * Detect unused variables and imports
8
+ */
9
+ export function detectDeadCode(ast: File, filePath: string): ComponentIssue[] {
10
+ const issues: ComponentIssue[] = [];
11
+
12
+ traverse(ast, {
13
+ Program(path) {
14
+ // Babel automatically collects all "bindings" (variables/imports)
15
+ // in the current scope.
16
+ const bindings = path.scope.getAllBindings();
17
+
18
+ for (const name in bindings) {
19
+ const binding = bindings[name];
20
+
21
+ // 1. Check if the variable was ever referenced (used)
22
+ if (!binding.referenced) {
23
+
24
+ // 2. Ignore variables that start with "_"
25
+ // (Standard dev practice for "intentionally unused")
26
+ if (name.startsWith('_')) continue;
27
+
28
+ // 3. Identify if it's an Import or a local Variable
29
+ const isImport = binding.kind === 'module';
30
+ const typeLabel = isImport ? 'Import' : 'Variable';
31
+
32
+ const node = binding.path.node;
33
+ const line = node.loc?.start.line || 0;
34
+
35
+ issues.push({
36
+ id: generateIssueId('dead-code', filePath, line),
37
+ component: 'Global/Module', // Dead code is often outside components
38
+ file: filePath,
39
+ line,
40
+ column: node.loc?.start.column,
41
+ severity: 'warning',
42
+ message: `Unused ${typeLabel} found: "${name}"`,
43
+ suggestion: `Remove the unused ${typeLabel.toLowerCase()} to clean up the code and reduce bundle size.`,
44
+ });
45
+ }
46
+ }
47
+ },
48
+ });
49
+
50
+ return issues;
51
+ }
@@ -0,0 +1,45 @@
1
+ import traverse from '@babel/traverse';
2
+ import { File } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { getParentComponentName, generateIssueId } from '../helpers';
5
+
6
+ /**
7
+ * Detect potential infinite loops in useEffect hooks
8
+ */
9
+ export function detectInfiniteLoops(ast: File, filePath: string): ComponentIssue[] {
10
+ const issues: ComponentIssue[] = [];
11
+
12
+ traverse(ast, {
13
+ CallExpression(path) {
14
+ const node = path.node;
15
+
16
+ // 1. Is the function being called named 'useEffect'?
17
+ if (
18
+ node.callee.type === 'Identifier' &&
19
+ node.callee.name === 'useEffect'
20
+ ) {
21
+
22
+ // 2. Check the number of arguments
23
+ // useEffect(callback, dependencies) should have 2 arguments.
24
+ // If it only has 1, it runs on EVERY render.
25
+ if (node.arguments.length === 1) {
26
+ const line = node.loc?.start.line || 0;
27
+ const component = getParentComponentName(path);
28
+
29
+ issues.push({
30
+ id: generateIssueId('infinite-loop', filePath, line),
31
+ component,
32
+ file: filePath,
33
+ line,
34
+ column: node.loc?.start.column,
35
+ severity: 'critical', // This is a "Critical" issue!
36
+ message: `Potential Infinite Loop: "useEffect" is missing a dependency array`,
37
+ suggestion: 'Add a dependency array (e.g., [], [data]) as the second argument to control when the effect runs.',
38
+ });
39
+ }
40
+ }
41
+ },
42
+ });
43
+
44
+ return issues;
45
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Central export for all detectors
3
+ */
4
+
5
+ export { detectConsoleLogs } from './console-log';
6
+ export { detectLargeComponents } from './large-component';
7
+ export { detectInlineFunctions } from './inline-function';
8
+ export { detectMissingKeys } from './missing-key';
9
+ export { detectInfiniteLoops } from './effect-loop';
10
+ export { detectPropDrilling } from './prop-drilling';
11
+ export { detectMissingMemo } from './missing-memo';
12
+ export { detectInlineStyles } from './inline-style';
13
+ export { detectDeadCode } from './dead-code';
14
+ // As you build more detectors, export them here
15
+ // export { detectMissingKeys } from './missing-key';
16
+ // export { detectMissingMemo } from './missing-memo';
@@ -0,0 +1,59 @@
1
+ import traverse from '@babel/traverse';
2
+ import { File } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { getParentComponentName, generateIssueId } from '../helpers';
5
+
6
+ /**
7
+ * Detect inline functions in JSX props
8
+ *
9
+ * Example of bad code:
10
+ * <button onClick={() => handleClick()}>Click</button>
11
+ *
12
+ * Why this is bad:
13
+ * - Creates a new function on every render
14
+ * - Causes child components to re-render unnecessarily
15
+ * - Performance impact
16
+ *
17
+ * Better approach:
18
+ * const handleClick = useCallback(() => {...}, []);
19
+ * <button onClick={handleClick}>Click</button>
20
+ */
21
+ export function detectInlineFunctions(ast: File, filePath: string): ComponentIssue[] {
22
+ const issues: ComponentIssue[] = [];
23
+
24
+ traverse(ast, {
25
+ JSXAttribute(path) {
26
+ const node = path.node;
27
+
28
+ // Check if the attribute value is an expression container
29
+ if (!node.value || node.value.type !== 'JSXExpressionContainer') {
30
+ return;
31
+ }
32
+
33
+ const expression = node.value.expression;
34
+
35
+ // Check if the expression is an inline function
36
+ if (
37
+ expression.type === 'ArrowFunctionExpression' ||
38
+ expression.type === 'FunctionExpression'
39
+ ) {
40
+ const attributeName = node.name.type === 'JSXIdentifier' ? node.name.name : 'unknown';
41
+ const line = node.loc?.start.line || 0;
42
+ const component = getParentComponentName(path);
43
+
44
+ issues.push({
45
+ id: generateIssueId('inline-function', filePath, line),
46
+ component,
47
+ file: filePath,
48
+ line,
49
+ column: node.loc?.start.column,
50
+ severity: 'info',
51
+ message: `Inline ${expression.type} in prop "${attributeName}"`,
52
+ suggestion: 'Use useCallback or define function outside component to prevent re-renders',
53
+ });
54
+ }
55
+ },
56
+ });
57
+
58
+ return issues;
59
+ }
@@ -0,0 +1,52 @@
1
+ import traverse from '@babel/traverse';
2
+ import { File } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { getParentComponentName, generateIssueId } from '../helpers';
5
+
6
+ // Threshold: If an inline style has more than this many properties, flag it.
7
+ const MAX_STYLE_PROPS = 5;
8
+
9
+ /**
10
+ * Detect large inline style objects in JSX
11
+ */
12
+ export function detectInlineStyles(ast: File, filePath: string): ComponentIssue[] {
13
+ const issues: ComponentIssue[] = [];
14
+
15
+ traverse(ast, {
16
+ JSXAttribute(path) {
17
+ const node = path.node;
18
+
19
+ // 1. Is the attribute named "style"?
20
+ if (node.name.name !== 'style') return;
21
+
22
+ // 2. Is the value an expression like {{ ... }}?
23
+ if (node.value?.type === 'JSXExpressionContainer') {
24
+ const expression = node.value.expression;
25
+
26
+ // 3. Is it an object (ObjectExpression)?
27
+ if (expression.type === 'ObjectExpression') {
28
+ const propCount = expression.properties.length;
29
+
30
+ // 4. Check if it's too big
31
+ if (propCount > MAX_STYLE_PROPS) {
32
+ const line = node.loc?.start.line || 0;
33
+ const component = getParentComponentName(path);
34
+
35
+ issues.push({
36
+ id: generateIssueId('inline-style', filePath, line),
37
+ component,
38
+ file: filePath,
39
+ line,
40
+ column: node.loc?.start.column,
41
+ severity: 'warning',
42
+ message: `Large inline style object found (${propCount} properties)`,
43
+ suggestion: 'Move large style objects to a constant or use CSS/Styled-components to improve performance and readability.',
44
+ });
45
+ }
46
+ }
47
+ }
48
+ },
49
+ });
50
+
51
+ return issues;
52
+ }
@@ -0,0 +1,79 @@
1
+ import * as traverse from '@babel/traverse';
2
+ import { File } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { getLineCount, returnsJSX, generateIssueId } from '../helpers';
5
+
6
+ // Threshold: components over this many lines are considered large
7
+ const MAX_COMPONENT_LINES = 300;
8
+
9
+ /**
10
+ * Detect components that are too large (>300 lines)
11
+ *
12
+ * Why this is bad:
13
+ * - Hard to understand and maintain
14
+ * - Likely doing too many things (violates Single Responsibility)
15
+ * - Hard to test
16
+ * - Harder to reuse
17
+ */
18
+ export function detectLargeComponents(ast: File, filePath: string): ComponentIssue[] {
19
+ const issues: ComponentIssue[] = [];
20
+
21
+ traverse.default(ast, {
22
+ // Check function declarations
23
+ FunctionDeclaration(path) {
24
+ const node = path.node;
25
+
26
+ // Only check if it's actually a React component (returns JSX)
27
+ if (!returnsJSX(path)) return;
28
+
29
+ const lineCount = getLineCount(node);
30
+
31
+ if (lineCount > MAX_COMPONENT_LINES) {
32
+ const componentName = node.id?.name || 'Unknown';
33
+ const line = node.loc?.start.line || 0;
34
+
35
+ issues.push({
36
+ id: generateIssueId('large-component', filePath, line),
37
+ component: componentName,
38
+ file: filePath,
39
+ line,
40
+ column: node.loc?.start.column,
41
+ severity: 'warning',
42
+ message: `Component "${componentName}" is ${lineCount} lines long`,
43
+ suggestion: `Split into smaller components (recommended max: ${MAX_COMPONENT_LINES} lines)`,
44
+ });
45
+ }
46
+ },
47
+
48
+ // Check arrow function components (const Component = () => ...)
49
+ VariableDeclarator(path) {
50
+ const node = path.node;
51
+
52
+ // Check if it's an arrow function
53
+ if (node.init?.type !== 'ArrowFunctionExpression') return;
54
+
55
+ // Check if it returns JSX (now works with updated helper!)
56
+ if (!returnsJSX(path.get('init'))) return;
57
+
58
+ const lineCount = getLineCount(node.init);
59
+
60
+ if (lineCount > MAX_COMPONENT_LINES) {
61
+ const componentName = node.id.type === 'Identifier' ? node.id.name : 'Unknown';
62
+ const line = node.loc?.start.line || 0;
63
+
64
+ issues.push({
65
+ id: generateIssueId('large-component', filePath, line),
66
+ component: componentName,
67
+ file: filePath,
68
+ line,
69
+ column: node.loc?.start.column,
70
+ severity: 'warning',
71
+ message: `Component "${componentName}" is ${lineCount} lines long`,
72
+ suggestion: `Split into smaller components (recommended max: ${MAX_COMPONENT_LINES} lines)`,
73
+ });
74
+ }
75
+ },
76
+ });
77
+
78
+ return issues;
79
+ }
@@ -0,0 +1,56 @@
1
+ import traverse from '@babel/traverse';
2
+ import { File, isJSXElement, isJSXAttribute, isIdentifier, isArrayExpression } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { generateIssueId, getParentComponentName } from '../helpers';
5
+
6
+ export function detectMissingKeys(ast: File, filePath: string): ComponentIssue[] {
7
+ const issues: ComponentIssue[] = [];
8
+
9
+ traverse(ast, {
10
+ // 1. Catch Literal Arrays: {[ <div />, <span /> ]}
11
+ ArrayExpression(path) {
12
+ const hasJSX = path.node.elements.some(el => isJSXElement(el));
13
+ if (!hasJSX) return;
14
+
15
+ path.node.elements.forEach((el) => {
16
+ if (isJSXElement(el)) {
17
+ const hasKey = el.openingElement.attributes.some(attr =>
18
+ isJSXAttribute(attr) && isIdentifier(attr.name, { name: 'key' })
19
+ );
20
+
21
+ if (!hasKey) {
22
+ pushIssue(el, issues, filePath, path);
23
+ }
24
+ }
25
+ });
26
+ },
27
+
28
+ // 2. Catch .map(), .filter(), .concat() that return JSX
29
+ CallExpression(path) {
30
+ const { node } = path;
31
+ // Look for array methods
32
+ if (node.callee.type === 'MemberExpression' && isIdentifier(node.callee.property)) {
33
+ const methodName = node.callee.property.name;
34
+ if (['map', 'filter', 'concat', 'from'].includes(methodName)) {
35
+ // ... logic to check if the result is JSX (similar to previous code)
36
+ }
37
+ }
38
+ }
39
+ });
40
+
41
+ return issues;
42
+ }
43
+
44
+ // Helper to keep code clean
45
+ function pushIssue(node: any, issues: any[], filePath: string, path: any) {
46
+ const line = node.loc?.start.line || 0;
47
+ issues.push({
48
+ id: generateIssueId('missing-key', filePath, line),
49
+ component: getParentComponentName(path),
50
+ file: filePath,
51
+ line,
52
+ severity: 'warning',
53
+ message: 'Missing "key" prop in an array of elements',
54
+ suggestion: 'Every element in an array or iterator must have a unique "key" prop.',
55
+ });
56
+ }
@@ -0,0 +1,59 @@
1
+ import traverse from '@babel/traverse';
2
+ import { File, isCallExpression, isIdentifier } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { generateIssueId } from '../helpers';
5
+
6
+ // Threshold: Only suggest memo if the component is over 40 lines
7
+ const MIN_LINES_FOR_MEMO = 40;
8
+
9
+ export function detectMissingMemo(ast: File, filePath: string): ComponentIssue[] {
10
+ const issues: ComponentIssue[] = [];
11
+
12
+ traverse(ast, {
13
+ FunctionDeclaration(path) {
14
+ const node = path.node;
15
+
16
+ // 1. Check if it looks like a React Component (Capitalized name)
17
+ const name = node.id?.name;
18
+ if (!name || !/^[A-Z]/.test(name)) return;
19
+
20
+ // 2. Check size: Small components don't always need memo
21
+ const startLine = node.loc?.start.line || 0;
22
+ const endLine = node.loc?.end.line || 0;
23
+ const lineCount = endLine - startLine;
24
+ if (lineCount < MIN_LINES_FOR_MEMO) return;
25
+
26
+ // 3. Check if it's already wrapped in memo (if exported separately)
27
+ // or if it's an ExportDefaultDeclaration wrapped in memo
28
+ let isMemoized = false;
29
+ path.scope.path.parentPath?.traverse({
30
+ CallExpression(innerPath) {
31
+ if (
32
+ isIdentifier(innerPath.node.callee, { name: 'memo' }) ||
33
+ (innerPath.node.callee.type === 'MemberExpression' &&
34
+ isIdentifier(innerPath.node.callee.property, { name: 'memo' }))
35
+ ) {
36
+ // Check if our component name is passed to this memo()
37
+ if (innerPath.node.arguments.some(arg => isIdentifier(arg, { name }))) {
38
+ isMemoized = true;
39
+ }
40
+ }
41
+ }
42
+ });
43
+
44
+ if (!isMemoized) {
45
+ issues.push({
46
+ id: generateIssueId('missing-memo', filePath, startLine),
47
+ component: name,
48
+ file: filePath,
49
+ line: startLine,
50
+ severity: 'info',
51
+ message: `Large component "${name}" is not memoized.`,
52
+ suggestion: 'Wrap this component in React.memo() to prevent unnecessary re-renders when parent props change.',
53
+ });
54
+ }
55
+ }
56
+ });
57
+
58
+ return issues;
59
+ }
@@ -0,0 +1,66 @@
1
+ import traverse from '@babel/traverse';
2
+ import { File, isFunctionDeclaration, isArrowFunctionExpression, isObjectProperty, isIdentifier } from '@babel/types';
3
+ import { ComponentIssue } from '../../../../shared/src/types';
4
+ import { generateIssueId } from '../helpers';
5
+
6
+ /**
7
+ * Detect potential prop drilling (props passed through without being used)
8
+ */
9
+ export function detectPropDrilling(ast: File, filePath: string): ComponentIssue[] {
10
+ const issues: ComponentIssue[] = [];
11
+
12
+ traverse(ast, {
13
+ // We target both declaration styles
14
+ "FunctionDeclaration|ArrowFunctionExpression"(path) {
15
+ const node = path.node;
16
+
17
+ // 1. Tell TypeScript: "Trust me, this node has params"
18
+ if (!isFunctionDeclaration(node) && !isArrowFunctionExpression(node)) return;
19
+
20
+ const params = node.params;
21
+ if (params.length === 0) return;
22
+
23
+ const propsNode = params[0];
24
+ let propNames: string[] = [];
25
+
26
+ // Handle destructured props: ({ user, theme })
27
+ if (propsNode.type === 'ObjectPattern') {
28
+ propNames = propsNode.properties
29
+ .filter((p): p is any => isObjectProperty(p) && isIdentifier(p.key))
30
+ .map(p => (p.key as any).name);
31
+ }
32
+ // Handle single prop object: (props)
33
+ else if (propsNode.type === 'Identifier') {
34
+ propNames = [propsNode.name];
35
+ }
36
+
37
+ // 2. For each prop, check its references in the component scope
38
+ propNames.forEach(name => {
39
+ const binding = path.scope.getBinding(name);
40
+ if (!binding) return;
41
+
42
+ const totalRefs = binding.referencePaths.length;
43
+ const jsxRefs = binding.referencePaths.filter(refPath =>
44
+ refPath.findParent(p => p.isJSXAttribute())
45
+ ).length;
46
+
47
+ // If used, but ONLY as a pass-through to a JSX attribute
48
+ if (totalRefs > 0 && totalRefs === jsxRefs) {
49
+ const line = node.loc?.start.line || 0;
50
+
51
+ issues.push({
52
+ id: generateIssueId('prop-drilling', filePath, line),
53
+ component: (node as any).id?.name || 'UnknownComponent',
54
+ file: filePath,
55
+ line,
56
+ severity: 'info',
57
+ message: `Potential Prop Drilling: The prop "${name}" is passed through this component without being used locally.`,
58
+ suggestion: 'Consider using React Context or a State Management library (Redux/Zustand) to avoid deep prop drilling.',
59
+ });
60
+ }
61
+ });
62
+ }
63
+ });
64
+
65
+ return issues;
66
+ }
@@ -0,0 +1,81 @@
1
+ import { NodePath } from '@babel/traverse';
2
+ import { Node } from '@babel/types';
3
+
4
+ /**
5
+ * Get the name of the parent component
6
+ * Walks up the AST tree to find the nearest component name
7
+ */
8
+ export function getParentComponentName(path: NodePath): string {
9
+ let current: NodePath | null = path;
10
+
11
+ while (current) {
12
+ // Check for FunctionDeclaration with a name
13
+ if (current.isFunctionDeclaration() && current.node.id) {
14
+ return current.node.id.name;
15
+ }
16
+
17
+ // Check for VariableDeclarator (const Component = ...)
18
+ if (current.isVariableDeclarator() && current.node.id.type === 'Identifier') {
19
+ return current.node.id.name;
20
+ }
21
+
22
+ current = current.parentPath;
23
+ }
24
+
25
+ return 'Unknown';
26
+ }
27
+
28
+ /**
29
+ * Check if a function/component returns JSX
30
+ * Safely handles null/undefined paths
31
+ *
32
+ * @param path - NodePath to check (can be null/undefined)
33
+ * @returns true if the path contains JSX elements or fragments
34
+ */
35
+ export function returnsJSX(path: NodePath | NodePath<Node | null | undefined> | null | undefined): boolean {
36
+ // Guard against null/undefined
37
+ if (!path || !path.node) return false;
38
+
39
+ let hasJSX = false;
40
+
41
+ try {
42
+ path.traverse({
43
+ JSXElement() {
44
+ hasJSX = true;
45
+ },
46
+ JSXFragment() {
47
+ hasJSX = true;
48
+ }
49
+ });
50
+ } catch (error) {
51
+ // If traversal fails for any reason, safely assume no JSX
52
+ return false;
53
+ }
54
+
55
+ return hasJSX;
56
+ }
57
+
58
+ /**
59
+ * Count the number of lines in a node
60
+ *
61
+ * @param node - AST node with location info
62
+ * @returns number of lines, or 0 if no location info
63
+ */
64
+ export function getLineCount(node: any): number {
65
+ if (!node || !node.loc) return 0;
66
+ return node.loc.end.line - node.loc.start.line;
67
+ }
68
+
69
+ /**
70
+ * Generate a unique issue ID
71
+ *
72
+ * @param type - Type of issue (e.g., 'console', 'large-component')
73
+ * @param filePath - File path where issue was found
74
+ * @param line - Line number
75
+ * @returns Unique identifier string
76
+ */
77
+ export function generateIssueId(type: string, filePath: string, line: number): string {
78
+ // Sanitize file path to create valid ID
79
+ const sanitizedPath = filePath.replace(/[^a-zA-Z0-9]/g, '_');
80
+ return `${type}-${sanitizedPath}-${line}`;
81
+ }