react-code-smell-detector 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 (47) hide show
  1. package/README.md +179 -0
  2. package/dist/analyzer.d.ts +10 -0
  3. package/dist/analyzer.d.ts.map +1 -0
  4. package/dist/analyzer.js +169 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +113 -0
  8. package/dist/detectors/index.d.ts +5 -0
  9. package/dist/detectors/index.d.ts.map +1 -0
  10. package/dist/detectors/index.js +4 -0
  11. package/dist/detectors/largeComponent.d.ts +4 -0
  12. package/dist/detectors/largeComponent.d.ts.map +1 -0
  13. package/dist/detectors/largeComponent.js +51 -0
  14. package/dist/detectors/memoization.d.ts +4 -0
  15. package/dist/detectors/memoization.d.ts.map +1 -0
  16. package/dist/detectors/memoization.js +150 -0
  17. package/dist/detectors/propDrilling.d.ts +5 -0
  18. package/dist/detectors/propDrilling.d.ts.map +1 -0
  19. package/dist/detectors/propDrilling.js +82 -0
  20. package/dist/detectors/useEffect.d.ts +4 -0
  21. package/dist/detectors/useEffect.d.ts.map +1 -0
  22. package/dist/detectors/useEffect.js +101 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +4 -0
  26. package/dist/parser/index.d.ts +29 -0
  27. package/dist/parser/index.d.ts.map +1 -0
  28. package/dist/parser/index.js +151 -0
  29. package/dist/reporter.d.ts +8 -0
  30. package/dist/reporter.d.ts.map +1 -0
  31. package/dist/reporter.js +217 -0
  32. package/dist/types/index.d.ts +64 -0
  33. package/dist/types/index.d.ts.map +1 -0
  34. package/dist/types/index.js +7 -0
  35. package/package.json +45 -0
  36. package/src/analyzer.ts +216 -0
  37. package/src/cli.ts +125 -0
  38. package/src/detectors/index.ts +4 -0
  39. package/src/detectors/largeComponent.ts +63 -0
  40. package/src/detectors/memoization.ts +177 -0
  41. package/src/detectors/propDrilling.ts +103 -0
  42. package/src/detectors/useEffect.ts +117 -0
  43. package/src/index.ts +4 -0
  44. package/src/parser/index.ts +195 -0
  45. package/src/reporter.ts +248 -0
  46. package/src/types/index.ts +86 -0
  47. package/tsconfig.json +19 -0
@@ -0,0 +1,150 @@
1
+ import * as t from '@babel/types';
2
+ import { getCodeSnippet } from '../parser/index.js';
3
+ import { DEFAULT_CONFIG } from '../types/index.js';
4
+ export function detectUnmemoizedCalculations(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
5
+ if (!config.checkMemoization)
6
+ return [];
7
+ const smells = [];
8
+ // Look for expensive operations not wrapped in useMemo
9
+ component.path.traverse({
10
+ VariableDeclarator(varPath) {
11
+ const init = varPath.node.init;
12
+ if (!init)
13
+ return;
14
+ const loc = init.loc;
15
+ if (!loc)
16
+ return;
17
+ // Check if this is inside useMemo/useCallback
18
+ let isInsideHook = false;
19
+ let currentPath = varPath.parentPath;
20
+ while (currentPath) {
21
+ if (currentPath.isCallExpression() &&
22
+ t.isIdentifier(currentPath.node.callee) &&
23
+ ['useMemo', 'useCallback'].includes(currentPath.node.callee.name)) {
24
+ isInsideHook = true;
25
+ break;
26
+ }
27
+ currentPath = currentPath.parentPath;
28
+ }
29
+ if (isInsideHook)
30
+ return;
31
+ // Detect expensive operations
32
+ let isExpensive = false;
33
+ let reason = '';
34
+ // Check for .map(), .filter(), .reduce(), .sort() on arrays
35
+ if (t.isCallExpression(init) && t.isMemberExpression(init.callee)) {
36
+ const prop = init.callee.property;
37
+ if (t.isIdentifier(prop)) {
38
+ const expensiveMethods = ['map', 'filter', 'reduce', 'sort', 'find', 'findIndex', 'flatMap'];
39
+ if (expensiveMethods.includes(prop.name)) {
40
+ isExpensive = true;
41
+ reason = `.${prop.name}() creates a new array on every render`;
42
+ }
43
+ }
44
+ }
45
+ // Check for object/array literals with complex computations
46
+ if (t.isArrayExpression(init) && init.elements.length > 5) {
47
+ isExpensive = true;
48
+ reason = 'Large array literal recreated on every render';
49
+ }
50
+ if (t.isObjectExpression(init) && init.properties.length > 5) {
51
+ isExpensive = true;
52
+ reason = 'Large object literal recreated on every render';
53
+ }
54
+ // Check for JSON operations
55
+ if (t.isCallExpression(init) && t.isMemberExpression(init.callee)) {
56
+ const obj = init.callee.object;
57
+ const prop = init.callee.property;
58
+ if (t.isIdentifier(obj) &&
59
+ obj.name === 'JSON' &&
60
+ t.isIdentifier(prop) &&
61
+ ['parse', 'stringify'].includes(prop.name)) {
62
+ isExpensive = true;
63
+ reason = `JSON.${prop.name}() is expensive and runs on every render`;
64
+ }
65
+ }
66
+ // Check for spread operations creating new objects
67
+ if (t.isObjectExpression(init)) {
68
+ const hasSpread = init.properties.some(p => t.isSpreadElement(p));
69
+ if (hasSpread && init.properties.length > 3) {
70
+ isExpensive = true;
71
+ reason = 'Spread operation creates new object reference on every render';
72
+ }
73
+ }
74
+ if (isExpensive) {
75
+ const id = varPath.node.id;
76
+ const varName = t.isIdentifier(id) ? id.name : 'variable';
77
+ smells.push({
78
+ type: 'unmemoized-calculation',
79
+ severity: 'warning',
80
+ message: `Unmemoized calculation "${varName}" in "${component.name}": ${reason}`,
81
+ file: filePath,
82
+ line: loc.start.line,
83
+ column: loc.start.column,
84
+ suggestion: `Wrap in useMemo: const ${varName} = useMemo(() => ..., [dependencies])`,
85
+ codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
86
+ });
87
+ }
88
+ },
89
+ });
90
+ // Check for inline function props (common performance issue)
91
+ component.path.traverse({
92
+ JSXAttribute(attrPath) {
93
+ const value = attrPath.node.value;
94
+ if (!t.isJSXExpressionContainer(value))
95
+ return;
96
+ const expr = value.expression;
97
+ const loc = expr.loc;
98
+ if (!loc)
99
+ return;
100
+ // Check for inline arrow functions
101
+ if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
102
+ const attrName = t.isJSXIdentifier(attrPath.node.name)
103
+ ? attrPath.node.name.name
104
+ : 'unknown';
105
+ // Only warn for non-trivial handlers
106
+ if (attrName.startsWith('on') && t.isArrowFunctionExpression(expr)) {
107
+ const body = expr.body;
108
+ // Skip simple one-liner callbacks that just call a function
109
+ if (t.isCallExpression(body)) {
110
+ // These are usually fine: onClick={() => doSomething()}
111
+ return;
112
+ }
113
+ if (t.isBlockStatement(body) && body.body.length > 1) {
114
+ smells.push({
115
+ type: 'inline-function-prop',
116
+ severity: 'info',
117
+ message: `Inline function for "${attrName}" creates new reference on every render`,
118
+ file: filePath,
119
+ line: loc.start.line,
120
+ column: loc.start.column,
121
+ suggestion: 'Extract to useCallback or define outside render',
122
+ codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
123
+ });
124
+ }
125
+ }
126
+ }
127
+ // Check for inline objects/arrays passed as props
128
+ if (t.isObjectExpression(expr) || t.isArrayExpression(expr)) {
129
+ const attrName = t.isJSXIdentifier(attrPath.node.name)
130
+ ? attrPath.node.name.name
131
+ : 'unknown';
132
+ // style prop is a common pattern, only warn for complex ones
133
+ if (attrName === 'style' && t.isObjectExpression(expr) && expr.properties.length <= 3) {
134
+ return;
135
+ }
136
+ smells.push({
137
+ type: 'unmemoized-calculation',
138
+ severity: 'info',
139
+ message: `Inline ${t.isObjectExpression(expr) ? 'object' : 'array'} for "${attrName}" prop creates new reference on every render`,
140
+ file: filePath,
141
+ line: loc.start.line,
142
+ column: loc.start.column,
143
+ suggestion: 'Extract to a constant or useMemo',
144
+ codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
145
+ });
146
+ }
147
+ },
148
+ });
149
+ return smells;
150
+ }
@@ -0,0 +1,5 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ export declare function detectPropDrilling(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
4
+ export declare function analyzePropDrillingDepth(components: ParsedComponent[], filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
5
+ //# sourceMappingURL=propDrilling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"propDrilling.d.ts","sourceRoot":"","sources":["../../src/detectors/propDrilling.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAqDb;AAED,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,eAAe,EAAE,EAC7B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAkCb"}
@@ -0,0 +1,82 @@
1
+ import { getCodeSnippet } from '../parser/index.js';
2
+ import { DEFAULT_CONFIG } from '../types/index.js';
3
+ export function detectPropDrilling(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
4
+ const smells = [];
5
+ // Check for too many props
6
+ if (component.props.length > config.maxPropsCount) {
7
+ smells.push({
8
+ type: 'prop-drilling',
9
+ severity: 'warning',
10
+ message: `Component "${component.name}" receives ${component.props.length} props (max recommended: ${config.maxPropsCount})`,
11
+ file: filePath,
12
+ line: component.startLine,
13
+ column: 0,
14
+ suggestion: 'Consider using Context, a state management library, or component composition',
15
+ codeSnippet: getCodeSnippet(sourceCode, component.startLine),
16
+ });
17
+ }
18
+ // Check for props that are just passed through (prop drilling indicators)
19
+ const passThroughProps = component.props.filter(prop => {
20
+ // Common patterns that indicate prop drilling
21
+ const drillingPatterns = ['data', 'config', 'settings', 'options', 'state', 'handlers', 'callbacks'];
22
+ return drillingPatterns.some(pattern => prop.toLowerCase().includes(pattern));
23
+ });
24
+ if (passThroughProps.length >= 3) {
25
+ smells.push({
26
+ type: 'prop-drilling',
27
+ severity: 'info',
28
+ message: `Component "${component.name}" may be experiencing prop drilling: ${passThroughProps.join(', ')}`,
29
+ file: filePath,
30
+ line: component.startLine,
31
+ column: 0,
32
+ suggestion: 'Consider using React Context or a state management solution to avoid passing data through intermediate components',
33
+ codeSnippet: getCodeSnippet(sourceCode, component.startLine),
34
+ });
35
+ }
36
+ // Check for spread props (often indicates forwarding)
37
+ const spreadProps = component.props.filter(p => p.startsWith('...'));
38
+ if (spreadProps.length > 0 && component.props.length > 5) {
39
+ smells.push({
40
+ type: 'prop-drilling',
41
+ severity: 'info',
42
+ message: `Component "${component.name}" uses spread props with many other props - potential prop drilling`,
43
+ file: filePath,
44
+ line: component.startLine,
45
+ column: 0,
46
+ suggestion: 'Consider composing components differently or using Context for shared state',
47
+ codeSnippet: getCodeSnippet(sourceCode, component.startLine),
48
+ });
49
+ }
50
+ return smells;
51
+ }
52
+ export function analyzePropDrillingDepth(components, filePath, sourceCode, config = DEFAULT_CONFIG) {
53
+ const smells = [];
54
+ // Track props that appear in multiple components
55
+ const propUsage = new Map();
56
+ components.forEach(comp => {
57
+ comp.props.forEach(prop => {
58
+ if (!prop.startsWith('...')) {
59
+ const existing = propUsage.get(prop) || [];
60
+ existing.push(comp.name);
61
+ propUsage.set(prop, existing);
62
+ }
63
+ });
64
+ });
65
+ // Find props used in more than maxPropDrillingDepth components
66
+ propUsage.forEach((componentNames, propName) => {
67
+ if (componentNames.length > config.maxPropDrillingDepth) {
68
+ const firstComp = components.find(c => c.name === componentNames[0]);
69
+ smells.push({
70
+ type: 'prop-drilling',
71
+ severity: 'warning',
72
+ message: `Prop "${propName}" is passed through ${componentNames.length} components: ${componentNames.join(' → ')}`,
73
+ file: filePath,
74
+ line: firstComp?.startLine || 1,
75
+ column: 0,
76
+ suggestion: `Move "${propName}" to Context or a state management solution`,
77
+ codeSnippet: firstComp ? getCodeSnippet(sourceCode, firstComp.startLine) : undefined,
78
+ });
79
+ }
80
+ });
81
+ return smells;
82
+ }
@@ -0,0 +1,4 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ export declare function detectUseEffectOveruse(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
4
+ //# sourceMappingURL=useEffect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEffect.d.ts","sourceRoot":"","sources":["../../src/detectors/useEffect.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CA2Gb"}
@@ -0,0 +1,101 @@
1
+ import * as t from '@babel/types';
2
+ import { getCodeSnippet } from '../parser/index.js';
3
+ import { DEFAULT_CONFIG } from '../types/index.js';
4
+ export function detectUseEffectOveruse(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
5
+ const smells = [];
6
+ const { useEffect } = component.hooks;
7
+ // Check for too many useEffects
8
+ if (useEffect.length > config.maxUseEffectsPerComponent) {
9
+ smells.push({
10
+ type: 'useEffect-overuse',
11
+ severity: 'warning',
12
+ message: `Component "${component.name}" has ${useEffect.length} useEffect hooks (max recommended: ${config.maxUseEffectsPerComponent})`,
13
+ file: filePath,
14
+ line: component.startLine,
15
+ column: 0,
16
+ suggestion: 'Consider extracting logic into custom hooks or combining related effects',
17
+ codeSnippet: getCodeSnippet(sourceCode, component.startLine),
18
+ });
19
+ }
20
+ // Check each useEffect for issues
21
+ useEffect.forEach((effect, index) => {
22
+ const loc = effect.loc;
23
+ if (!loc)
24
+ return;
25
+ // Check for empty dependency array with async operations
26
+ const deps = effect.arguments[1];
27
+ const callback = effect.arguments[0];
28
+ if (t.isArrayExpression(deps) && deps.elements.length === 0) {
29
+ // Empty dependency array - check if it's justified
30
+ let hasAsyncOperation = false;
31
+ let hasStateUpdate = false;
32
+ if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
33
+ const body = callback.body;
34
+ const checkNode = (node) => {
35
+ if (t.isAwaitExpression(node))
36
+ hasAsyncOperation = true;
37
+ if (t.isCallExpression(node)) {
38
+ const callee = node.callee;
39
+ if (t.isIdentifier(callee) && callee.name.startsWith('set')) {
40
+ hasStateUpdate = true;
41
+ }
42
+ }
43
+ };
44
+ if (t.isBlockStatement(body)) {
45
+ body.body.forEach(stmt => {
46
+ t.traverseFast(stmt, checkNode);
47
+ });
48
+ }
49
+ }
50
+ if (hasAsyncOperation && hasStateUpdate) {
51
+ smells.push({
52
+ type: 'useEffect-overuse',
53
+ severity: 'info',
54
+ message: `useEffect #${index + 1} in "${component.name}" has empty deps but contains async state updates`,
55
+ file: filePath,
56
+ line: loc.start.line,
57
+ column: loc.start.column,
58
+ suggestion: 'Consider using React Query, SWR, or a custom hook for data fetching',
59
+ codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
60
+ });
61
+ }
62
+ }
63
+ // Check for missing cleanup
64
+ if (t.isArrowFunctionExpression(callback) || t.isFunctionExpression(callback)) {
65
+ let hasSubscription = false;
66
+ let hasCleanup = false;
67
+ const checkSubscription = (node) => {
68
+ if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
69
+ const prop = node.callee.property;
70
+ if (t.isIdentifier(prop)) {
71
+ if (['addEventListener', 'subscribe', 'on', 'setInterval', 'setTimeout'].includes(prop.name)) {
72
+ hasSubscription = true;
73
+ }
74
+ }
75
+ }
76
+ };
77
+ const body = callback.body;
78
+ if (t.isBlockStatement(body)) {
79
+ body.body.forEach(stmt => {
80
+ t.traverseFast(stmt, checkSubscription);
81
+ if (t.isReturnStatement(stmt) && stmt.argument) {
82
+ hasCleanup = true;
83
+ }
84
+ });
85
+ }
86
+ if (hasSubscription && !hasCleanup) {
87
+ smells.push({
88
+ type: 'useEffect-overuse',
89
+ severity: 'error',
90
+ message: `useEffect in "${component.name}" sets up subscription but has no cleanup function`,
91
+ file: filePath,
92
+ line: loc.start.line,
93
+ column: loc.start.column,
94
+ suggestion: 'Add a cleanup function to remove event listeners/subscriptions',
95
+ codeSnippet: getCodeSnippet(sourceCode, loc.start.line),
96
+ });
97
+ }
98
+ }
99
+ });
100
+ return smells;
101
+ }
@@ -0,0 +1,5 @@
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';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACpE,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { analyzeProject } from './analyzer.js';
2
+ export { reportResults } from './reporter.js';
3
+ export * from './types/index.js';
4
+ export { parseFile, parseCode } from './parser/index.js';
@@ -0,0 +1,29 @@
1
+ import { NodePath } from '@babel/traverse';
2
+ import * as t from '@babel/types';
3
+ export interface ParsedComponent {
4
+ name: string;
5
+ startLine: number;
6
+ endLine: number;
7
+ node: t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression;
8
+ path: NodePath;
9
+ hooks: {
10
+ useEffect: t.CallExpression[];
11
+ useState: t.CallExpression[];
12
+ useMemo: t.CallExpression[];
13
+ useCallback: t.CallExpression[];
14
+ useRef: t.CallExpression[];
15
+ custom: t.CallExpression[];
16
+ };
17
+ props: string[];
18
+ jsxDepth: number;
19
+ }
20
+ export interface ParseResult {
21
+ ast: t.File;
22
+ components: ParsedComponent[];
23
+ imports: string[];
24
+ sourceCode: string;
25
+ }
26
+ export declare function parseFile(filePath: string): Promise<ParseResult>;
27
+ export declare function parseCode(sourceCode: string, filePath?: string): ParseResult;
28
+ export declare function getCodeSnippet(sourceCode: string, line: number, context?: number): string;
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AACA,OAAkB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAMlC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,CAAC,CAAC,mBAAmB,GAAG,CAAC,CAAC,uBAAuB,GAAG,CAAC,CAAC,kBAAkB,CAAC;IAC/E,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE;QACL,SAAS,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;QAC9B,QAAQ,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;QAC5B,WAAW,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;QAChC,MAAM,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;QAC3B,MAAM,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;KAC5B,CAAC;IACF,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC;IACZ,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAGtE;AAED,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAkB,GAAG,WAAW,CA4CvF;AAmGD,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAU,GAAG,MAAM,CAa5F"}
@@ -0,0 +1,151 @@
1
+ import * as parser from '@babel/parser';
2
+ import _traverse from '@babel/traverse';
3
+ import * as t from '@babel/types';
4
+ import fs from 'fs/promises';
5
+ // Handle ESM/CJS interop
6
+ const traverse = _traverse.default || _traverse;
7
+ export async function parseFile(filePath) {
8
+ const sourceCode = await fs.readFile(filePath, 'utf-8');
9
+ return parseCode(sourceCode, filePath);
10
+ }
11
+ export function parseCode(sourceCode, filePath = 'unknown') {
12
+ const ast = parser.parse(sourceCode, {
13
+ sourceType: 'module',
14
+ plugins: [
15
+ 'jsx',
16
+ 'typescript',
17
+ 'decorators-legacy',
18
+ 'classProperties',
19
+ 'optionalChaining',
20
+ 'nullishCoalescingOperator',
21
+ ],
22
+ sourceFilename: filePath,
23
+ });
24
+ const components = [];
25
+ const imports = [];
26
+ traverse(ast, {
27
+ ImportDeclaration(path) {
28
+ imports.push(path.node.source.value);
29
+ },
30
+ FunctionDeclaration(path) {
31
+ if (isReactComponent(path.node.id?.name, path)) {
32
+ components.push(extractComponentInfo(path.node.id?.name || 'Anonymous', path));
33
+ }
34
+ },
35
+ VariableDeclarator(path) {
36
+ const init = path.node.init;
37
+ const id = path.node.id;
38
+ if (t.isIdentifier(id) &&
39
+ (t.isArrowFunctionExpression(init) || t.isFunctionExpression(init))) {
40
+ if (isReactComponent(id.name, path)) {
41
+ components.push(extractComponentInfo(id.name, path, init));
42
+ }
43
+ }
44
+ },
45
+ });
46
+ return { ast, components, imports, sourceCode };
47
+ }
48
+ function isReactComponent(name, path) {
49
+ if (!name)
50
+ return false;
51
+ // Component names start with uppercase
52
+ if (!/^[A-Z]/.test(name))
53
+ return false;
54
+ // Check if it returns JSX
55
+ let hasJSX = false;
56
+ path.traverse({
57
+ JSXElement() {
58
+ hasJSX = true;
59
+ },
60
+ JSXFragment() {
61
+ hasJSX = true;
62
+ },
63
+ });
64
+ return hasJSX;
65
+ }
66
+ function extractComponentInfo(name, path, node) {
67
+ const actualNode = node || path.node;
68
+ const loc = actualNode.loc;
69
+ const hooks = {
70
+ useEffect: [],
71
+ useState: [],
72
+ useMemo: [],
73
+ useCallback: [],
74
+ useRef: [],
75
+ custom: [],
76
+ };
77
+ const props = [];
78
+ let jsxDepth = 0;
79
+ // Extract hooks
80
+ path.traverse({
81
+ CallExpression(callPath) {
82
+ const callee = callPath.node.callee;
83
+ if (t.isIdentifier(callee)) {
84
+ const hookName = callee.name;
85
+ if (hookName === 'useEffect')
86
+ hooks.useEffect.push(callPath.node);
87
+ else if (hookName === 'useState')
88
+ hooks.useState.push(callPath.node);
89
+ else if (hookName === 'useMemo')
90
+ hooks.useMemo.push(callPath.node);
91
+ else if (hookName === 'useCallback')
92
+ hooks.useCallback.push(callPath.node);
93
+ else if (hookName === 'useRef')
94
+ hooks.useRef.push(callPath.node);
95
+ else if (hookName.startsWith('use'))
96
+ hooks.custom.push(callPath.node);
97
+ }
98
+ },
99
+ });
100
+ // Extract props
101
+ const params = t.isFunctionDeclaration(actualNode)
102
+ ? actualNode.params
103
+ : actualNode.params;
104
+ if (params.length > 0) {
105
+ const firstParam = params[0];
106
+ if (t.isObjectPattern(firstParam)) {
107
+ firstParam.properties.forEach(prop => {
108
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
109
+ props.push(prop.key.name);
110
+ }
111
+ else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
112
+ props.push(`...${prop.argument.name}`);
113
+ }
114
+ });
115
+ }
116
+ else if (t.isIdentifier(firstParam)) {
117
+ props.push(firstParam.name);
118
+ }
119
+ }
120
+ // Calculate JSX nesting depth
121
+ path.traverse({
122
+ JSXElement: {
123
+ enter() {
124
+ jsxDepth++;
125
+ },
126
+ },
127
+ });
128
+ return {
129
+ name,
130
+ startLine: loc?.start.line || 0,
131
+ endLine: loc?.end.line || 0,
132
+ node: actualNode,
133
+ path,
134
+ hooks,
135
+ props,
136
+ jsxDepth: Math.floor(jsxDepth / 2), // Approximate depth
137
+ };
138
+ }
139
+ export function getCodeSnippet(sourceCode, line, context = 2) {
140
+ const lines = sourceCode.split('\n');
141
+ const start = Math.max(0, line - context - 1);
142
+ const end = Math.min(lines.length, line + context);
143
+ return lines
144
+ .slice(start, end)
145
+ .map((l, i) => {
146
+ const lineNum = start + i + 1;
147
+ const marker = lineNum === line ? '>' : ' ';
148
+ return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;
149
+ })
150
+ .join('\n');
151
+ }
@@ -0,0 +1,8 @@
1
+ import { AnalysisResult } from './types/index.js';
2
+ export interface ReporterOptions {
3
+ format: 'console' | 'json' | 'markdown';
4
+ showCodeSnippets: boolean;
5
+ rootDir: string;
6
+ }
7
+ export declare function reportResults(result: AnalysisResult, options: ReporterOptions): string;
8
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAA4B,MAAM,kBAAkB,CAAC;AAG5E,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;IACxC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAUtF"}