@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
JavaScript
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 };