UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

869 lines (740 loc) 25.1 kB
const { parse } = require('@babel/parser'); const traverse = require('@babel/traverse').default; const t = require('@babel/types'); const path = require('path'); /** * Component Relationship Analyzer * Analyzes component dependencies, prop flow, and architectural patterns */ class ComponentRelationshipAnalyzer { constructor() { this.components = new Map(); this.dependencies = new Map(); this.propFlow = new Map(); this.hooks = new Map(); this.contexts = new Map(); } /** * Analyze component relationships and dependencies */ analyzeRelationships(ast, filePath, context = {}) { const analysis = { components: [], imports: [], exports: [], propFlow: [], hooks: [], contexts: [], patterns: [] }; let currentComponent = null; traverse(ast, { // Analyze imports ImportDeclaration: (path) => { const importInfo = this._analyzeImport(path, filePath); analysis.imports.push(importInfo); if (importInfo.isComponent) { this._recordDependency(filePath, importInfo.source, importInfo.specifiers); } }, // Analyze exports ExportDeclaration: (path) => { const exportInfo = this._analyzeExport(path, filePath); analysis.exports.push(exportInfo); }, // Analyze component definitions FunctionDeclaration: (path) => { const componentInfo = this._analyzeComponent(path, filePath); if (componentInfo.isComponent) { currentComponent = componentInfo; analysis.components.push(componentInfo); this.components.set(componentInfo.name, componentInfo); } }, VariableDeclarator: (path) => { if (t.isArrowFunctionExpression(path.node.init) || t.isFunctionExpression(path.node.init)) { const componentInfo = this._analyzeComponent(path, filePath); if (componentInfo.isComponent) { currentComponent = componentInfo; analysis.components.push(componentInfo); this.components.set(componentInfo.name, componentInfo); } } }, // Analyze JSX usage JSXElement: (path) => { if (currentComponent) { const jsxInfo = this._analyzeJSXElement(path, currentComponent); if (jsxInfo.isCustomComponent) { analysis.propFlow.push(jsxInfo); this._recordPropFlow(currentComponent, jsxInfo); } } }, // Analyze hooks CallExpression: (path) => { const hookInfo = this._analyzeHook(path, currentComponent); if (hookInfo) { analysis.hooks.push(hookInfo); this._recordHookUsage(currentComponent, hookInfo); } // Analyze context usage const contextInfo = this._analyzeContextUsage(path, currentComponent); if (contextInfo) { analysis.contexts.push(contextInfo); } } }); // Analyze patterns after collecting all information analysis.patterns = this._analyzePatterns(analysis); return analysis; } /** * Detect architectural patterns and suggest improvements */ detectArchitecturalPatterns(analysis) { const patterns = []; // Detect prop drilling const propDrilling = this._detectPropDrilling(analysis); if (propDrilling.length > 0) { patterns.push({ type: 'prop-drilling', severity: 'warning', description: 'Props are being passed through multiple component levels', components: propDrilling, suggestion: 'Consider using React Context or state management library' }); } // Detect large components const largeComponents = this._detectLargeComponents(analysis); if (largeComponents.length > 0) { patterns.push({ type: 'large-component', severity: 'info', description: 'Components with high complexity detected', components: largeComponents, suggestion: 'Consider breaking down into smaller components' }); } // Detect missing error boundaries const needsErrorBoundary = this._detectMissingErrorBoundaries(analysis); if (needsErrorBoundary) { patterns.push({ type: 'missing-error-boundary', severity: 'warning', description: 'No error boundaries detected in component tree', suggestion: 'Add error boundaries for better error handling' }); } // Detect inefficient re-renders const inefficientRenders = this._detectInefficientRenders(analysis); if (inefficientRenders.length > 0) { patterns.push({ type: 'inefficient-renders', severity: 'warning', description: 'Components may be re-rendering unnecessarily', components: inefficientRenders, suggestion: 'Consider using React.memo, useMemo, or useCallback' }); } return patterns; } /** * Generate component dependency graph */ generateDependencyGraph() { const graph = { nodes: [], edges: [], clusters: [] }; // Add component nodes this.components.forEach((component, name) => { graph.nodes.push({ id: name, label: name, type: 'component', file: component.filePath, complexity: component.complexity, hooks: component.hooks, props: component.props }); }); // Add dependency edges this.dependencies.forEach((deps, from) => { deps.forEach(dep => { graph.edges.push({ from, to: dep.component, type: 'import', source: dep.source }); }); }); // Add prop flow edges this.propFlow.forEach((flows, component) => { flows.forEach(flow => { graph.edges.push({ from: component, to: flow.component, type: 'prop-flow', props: flow.props }); }); }); // Detect clusters (related components) graph.clusters = this._detectComponentClusters(graph); return graph; } /** * Analyze component performance implications */ analyzePerformanceImpact(analysis) { const performanceIssues = []; analysis.components.forEach(component => { // Check for expensive operations in render if (component.hasExpensiveOperations) { performanceIssues.push({ type: 'expensive-render', component: component.name, severity: 'warning', description: 'Component may have expensive operations in render method', suggestions: ['Move expensive calculations to useMemo', 'Use useCallback for event handlers'] }); } // Check for missing memoization if (component.shouldMemoize && !component.isMemoized) { performanceIssues.push({ type: 'missing-memoization', component: component.name, severity: 'info', description: 'Component could benefit from memoization', suggestions: ['Wrap with React.memo', 'Use PureComponent for class components'] }); } // Check for large prop objects if (component.hasLargePropObjects) { performanceIssues.push({ type: 'large-prop-objects', component: component.name, severity: 'warning', description: 'Component receives large object props that may cause re-renders', suggestions: ['Break down props into primitives', 'Use object memoization'] }); } }); return performanceIssues; } // Helper methods _analyzeImport(path, filePath) { const source = path.node.source.value; const specifiers = path.node.specifiers.map(spec => { if (t.isImportDefaultSpecifier(spec)) { return { type: 'default', imported: 'default', local: spec.local.name }; } else if (t.isImportSpecifier(spec)) { return { type: 'named', imported: spec.imported.name, local: spec.local.name }; } else if (t.isImportNamespaceSpecifier(spec)) { return { type: 'namespace', imported: '*', local: spec.local.name }; } }); return { source, specifiers, isComponent: this._isComponentImport(source, specifiers), isReactImport: source === 'react' || source.startsWith('react/'), isRelative: source.startsWith('./') || source.startsWith('../') }; } _analyzeExport(path, filePath) { if (t.isExportDefaultDeclaration(path.node)) { return { type: 'default', name: this._getExportName(path.node.declaration), isComponent: this._isComponentExport(path.node.declaration) }; } else if (t.isExportNamedDeclaration(path.node)) { const specifiers = path.node.specifiers.map(spec => ({ exported: spec.exported.name, local: spec.local?.name })); return { type: 'named', specifiers, declaration: path.node.declaration }; } } _analyzeComponent(path, filePath) { const name = this._getComponentName(path); if (!name) return { isComponent: false }; const component = { name, filePath, isComponent: this._isReactComponent(path), type: this._getComponentType(path), props: [], hooks: [], complexity: 0, loc: path.node.loc, hasExpensiveOperations: false, shouldMemoize: false, isMemoized: false, hasLargePropObjects: false }; if (!component.isComponent) return component; // Analyze component structure this._analyzeComponentStructure(path, component); return component; } _analyzeComponentStructure(path, component) { const functionNode = t.isFunctionDeclaration(path.node) ? path.node : t.isVariableDeclarator(path.node) ? path.node.init : null; if (!functionNode) return; // Analyze parameters (props) if (functionNode.params && functionNode.params.length > 0) { component.props = this._analyzeComponentProps(functionNode.params[0]); } // Analyze component body traverse(functionNode, { CallExpression: (callPath) => { // Track hooks const hookInfo = this._analyzeHook(callPath, component); if (hookInfo) { component.hooks.push(hookInfo); } // Check for expensive operations if (this._isExpensiveOperation(callPath)) { component.hasExpensiveOperations = true; } }, // Calculate complexity IfStatement: () => component.complexity++, ConditionalExpression: () => component.complexity++, LogicalExpression: () => component.complexity++, SwitchStatement: () => component.complexity++, ForStatement: () => component.complexity++, WhileStatement: () => component.complexity++ }); // Determine if should memoize component.shouldMemoize = component.complexity > 5 || component.hasExpensiveOperations; // Check if already memoized component.isMemoized = this._isComponentMemoized(path); } _analyzeJSXElement(path, currentComponent) { const elementName = this._getJSXElementName(path); if (!elementName || this._isHTMLElement(elementName)) { return { isCustomComponent: false }; } const props = this._analyzeJSXProps(path); return { isCustomComponent: true, component: elementName, props, parent: currentComponent ? currentComponent.name : null, loc: path.node.loc }; } _analyzeHook(path, currentComponent) { if (!t.isCallExpression(path.node) || !t.isIdentifier(path.node.callee)) { return null; } const hookName = path.node.callee.name; if (!hookName.startsWith('use') || hookName.length <= 3) { return null; } return { name: hookName, args: path.node.arguments.length, component: currentComponent ? currentComponent.name : null, isBuiltIn: this._isBuiltInHook(hookName), isCustom: this._isCustomHook(hookName), loc: path.node.loc }; } _analyzeContextUsage(path, currentComponent) { if (!t.isCallExpression(path.node) || !t.isIdentifier(path.node.callee)) { return null; } const functionName = path.node.callee.name; if (functionName !== 'useContext') return null; const contextArg = path.node.arguments[0]; const contextName = t.isIdentifier(contextArg) ? contextArg.name : 'unknown'; return { context: contextName, component: currentComponent ? currentComponent.name : null, type: 'consumer', loc: path.node.loc }; } _analyzePatterns(analysis) { return [ ...this.detectArchitecturalPatterns(analysis), ...this.analyzePerformanceImpact(analysis) ]; } _detectPropDrilling(analysis) { const propPaths = new Map(); // Track prop flow through components analysis.propFlow.forEach(flow => { flow.props.forEach(prop => { if (!propPaths.has(prop.name)) { propPaths.set(prop.name, []); } propPaths.get(prop.name).push({ from: flow.parent, to: flow.component, depth: flow.depth || 1 }); }); }); // Find props that flow through multiple levels const drilledProps = []; propPaths.forEach((path, propName) => { if (path.length > 2) { // More than 2 levels drilledProps.push({ prop: propName, path, depth: path.length }); } }); return drilledProps; } _detectLargeComponents(analysis) { return analysis.components.filter(component => component.complexity > 10 || component.hooks.length > 8 || component.props.length > 15 ); } _detectMissingErrorBoundaries(analysis) { const hasErrorBoundary = analysis.components.some(component => component.hooks.some(hook => hook.name === 'useErrorHandler' || hook.name === 'componentDidCatch' ) ); return !hasErrorBoundary && analysis.components.length > 5; } _detectInefficientRenders(analysis) { return analysis.components.filter(component => component.shouldMemoize && !component.isMemoized ); } _detectComponentClusters(graph) { // Simple clustering based on import relationships const clusters = []; const visited = new Set(); graph.nodes.forEach(node => { if (visited.has(node.id)) return; const cluster = this._findConnectedComponents(node, graph, visited); if (cluster.length > 1) { clusters.push({ id: `cluster-${clusters.length}`, components: cluster, relationships: this._getClusterRelationships(cluster, graph) }); } }); return clusters; } _findConnectedComponents(startNode, graph, visited) { const cluster = [startNode.id]; const queue = [startNode.id]; visited.add(startNode.id); while (queue.length > 0) { const current = queue.shift(); // Find connected nodes graph.edges.forEach(edge => { let connected = null; if (edge.from === current && !visited.has(edge.to)) { connected = edge.to; } else if (edge.to === current && !visited.has(edge.from)) { connected = edge.from; } if (connected && !visited.has(connected)) { visited.add(connected); cluster.push(connected); queue.push(connected); } }); } return cluster; } _getClusterRelationships(cluster, graph) { return graph.edges.filter(edge => cluster.includes(edge.from) && cluster.includes(edge.to) ); } // Utility methods _isComponentImport(source, specifiers) { // Check if importing React components return specifiers.some(spec => spec.local && /^[A-Z]/.test(spec.local) && // PascalCase (source.includes('component') || source.includes('Component') || source.startsWith('./') || source.startsWith('../')) ); } _isComponentExport(declaration) { if (t.isFunctionDeclaration(declaration)) { return this._isReactComponent({ node: declaration }); } if (t.isIdentifier(declaration)) { return /^[A-Z]/.test(declaration.name); } return false; } _getExportName(declaration) { if (t.isFunctionDeclaration(declaration) && declaration.id) { return declaration.id.name; } if (t.isIdentifier(declaration)) { return declaration.name; } return 'default'; } _getComponentName(path) { if (t.isFunctionDeclaration(path.node) && path.node.id) { return path.node.id.name; } if (t.isVariableDeclarator(path.node) && t.isIdentifier(path.node.id)) { return path.node.id.name; } return null; } _isReactComponent(path) { const functionNode = t.isFunctionDeclaration(path.node) ? path.node : t.isVariableDeclarator(path.node) ? path.node.init : null; if (!functionNode) return false; // Check if function returns JSX let returnsJSX = false; traverse(functionNode, { ReturnStatement(returnPath) { if (t.isJSXElement(returnPath.node.argument) || t.isJSXFragment(returnPath.node.argument)) { returnsJSX = true; } } }); return returnsJSX; } _getComponentType(path) { if (t.isFunctionDeclaration(path.node)) return 'function'; if (t.isVariableDeclarator(path.node) && t.isArrowFunctionExpression(path.node.init)) return 'arrow'; if (t.isVariableDeclarator(path.node) && t.isFunctionExpression(path.node.init)) return 'function-expression'; return 'unknown'; } _analyzeComponentProps(propsParam) { if (t.isIdentifier(propsParam)) { return [{ name: propsParam.name, type: 'any', destructured: false }]; } if (t.isObjectPattern(propsParam)) { return propsParam.properties.map(prop => { if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { return { name: prop.key.name, type: 'any', // Would need type inference destructured: true, hasDefault: t.isAssignmentPattern(prop.value) }; } return { name: 'unknown', type: 'any', destructured: true }; }); } return []; } _analyzeJSXProps(path) { return path.node.openingElement.attributes.map(attr => { if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) { return { name: attr.name.name, type: attr.value ? this._getJSXValueType(attr.value) : 'boolean', isSpread: false }; } else if (t.isJSXSpreadAttribute(attr)) { return { name: '...spread', type: 'object', isSpread: true }; } return { name: 'unknown', type: 'any', isSpread: false }; }); } _getJSXElementName(path) { const openingElement = path.node.openingElement; if (t.isJSXIdentifier(openingElement.name)) { return openingElement.name.name; } if (t.isJSXMemberExpression(openingElement.name)) { return `${openingElement.name.object.name}.${openingElement.name.property.name}`; } return null; } _getJSXValueType(value) { if (t.isStringLiteral(value)) return 'string'; if (t.isJSXExpressionContainer(value)) { const expr = value.expression; if (t.isStringLiteral(expr)) return 'string'; if (t.isNumericLiteral(expr)) return 'number'; if (t.isBooleanLiteral(expr)) return 'boolean'; if (t.isArrayExpression(expr)) return 'array'; if (t.isObjectExpression(expr)) return 'object'; if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) return 'function'; } return 'any'; } _isHTMLElement(elementName) { return elementName && elementName[0] === elementName[0].toLowerCase(); } _isBuiltInHook(hookName) { const builtInHooks = [ 'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect', 'useDebugValue' ]; return builtInHooks.includes(hookName); } _isCustomHook(hookName) { return hookName.startsWith('use') && !this._isBuiltInHook(hookName); } _isExpensiveOperation(path) { // Check for potentially expensive operations if (t.isCallExpression(path.node)) { const callee = path.node.callee; if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) { const methodName = callee.property.name; // Array methods that can be expensive if (['map', 'filter', 'reduce', 'sort', 'find', 'forEach'].includes(methodName)) { return true; } } // Math operations if (t.isMemberExpression(callee) && t.isIdentifier(callee.object, { name: 'Math' })) { return true; } } return false; } _isComponentMemoized(path) { // Check if component is wrapped with React.memo or similar let isMemoized = false; // Check parent scope for memo wrapper if (path.parent && t.isCallExpression(path.parent)) { const callee = path.parent.callee; if (t.isMemberExpression(callee) && t.isIdentifier(callee.object, { name: 'React' }) && t.isIdentifier(callee.property, { name: 'memo' })) { isMemoized = true; } if (t.isIdentifier(callee, { name: 'memo' })) { isMemoized = true; } } return isMemoized; } _recordDependency(from, source, specifiers) { if (!this.dependencies.has(from)) { this.dependencies.set(from, []); } specifiers.forEach(spec => { if (spec.type === 'default' || spec.type === 'named') { this.dependencies.get(from).push({ component: spec.local, source, type: spec.type }); } }); } _recordPropFlow(fromComponent, jsxInfo) { const from = fromComponent.name; if (!this.propFlow.has(from)) { this.propFlow.set(from, []); } this.propFlow.get(from).push({ component: jsxInfo.component, props: jsxInfo.props, loc: jsxInfo.loc }); } _recordHookUsage(component, hookInfo) { if (!component) return; if (!this.hooks.has(component.name)) { this.hooks.set(component.name, []); } this.hooks.get(component.name).push(hookInfo); } /** * Get comprehensive analysis data for dashboard */ getAnalysisData() { return { components: Object.fromEntries(this.components), dependencies: Object.fromEntries(this.dependencies), propFlow: Object.fromEntries(this.propFlow), hooks: Object.fromEntries(this.hooks), contexts: Object.fromEntries(this.contexts), dependencyGraph: this.generateDependencyGraph(), metrics: this._calculateMetrics() }; } _calculateMetrics() { return { totalComponents: this.components.size, averageComplexity: this._calculateAverageComplexity(), componentTypes: this._getComponentTypeDistribution(), hookUsage: this._getHookUsageStats(), dependencyDepth: this._calculateDependencyDepth() }; } _calculateAverageComplexity() { if (this.components.size === 0) return 0; const totalComplexity = Array.from(this.components.values()) .reduce((sum, comp) => sum + comp.complexity, 0); return totalComplexity / this.components.size; } _getComponentTypeDistribution() { const distribution = { function: 0, arrow: 0, 'function-expression': 0, unknown: 0 }; this.components.forEach(comp => { distribution[comp.type] = (distribution[comp.type] || 0) + 1; }); return distribution; } _getHookUsageStats() { const stats = {}; this.hooks.forEach(componentHooks => { componentHooks.forEach(hook => { stats[hook.name] = (stats[hook.name] || 0) + 1; }); }); return stats; } _calculateDependencyDepth() { // Calculate maximum dependency depth in the component tree let maxDepth = 0; this.dependencies.forEach((deps, component) => { const depth = this._calculateComponentDepth(component, new Set()); maxDepth = Math.max(maxDepth, depth); }); return maxDepth; } _calculateComponentDepth(component, visited) { if (visited.has(component)) return 0; // Circular dependency visited.add(component); const deps = this.dependencies.get(component) || []; if (deps.length === 0) return 1; const maxChildDepth = Math.max(...deps.map(dep => this._calculateComponentDepth(dep.component, new Set(visited)) )); return 1 + maxChildDepth; } reset() { this.components.clear(); this.dependencies.clear(); this.propFlow.clear(); this.hooks.clear(); this.contexts.clear(); } } module.exports = { ComponentRelationshipAnalyzer };