UNPKG

smartui-migration-tool

Version:

Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI

669 lines (556 loc) • 20.3 kB
const { Command, Flags } = require('@oclif/core'); const chalk = require('chalk'); const fs = require('fs-extra'); const path = require('path'); const glob = require('glob'); class DependencyAnalyzer extends Command { static description = 'Analyze cross-file dependencies and build dependency graphs'; static flags = { path: Flags.string({ char: 'p', description: 'Path to analyze (default: current directory)', default: process.cwd() }), include: Flags.string({ char: 'i', description: 'File patterns to include (comma-separated)', default: '**/*.{js,ts,jsx,tsx,py,java,cs}' }), exclude: Flags.string({ char: 'e', description: 'File patterns to exclude (comma-separated)', default: 'node_modules/**,dist/**,build/**,*.min.js' }), output: Flags.string({ char: 'o', description: 'Output file for dependency graph', default: 'dependency-graph.json' }), format: Flags.string({ char: 'f', description: 'Output format (json, dot, mermaid)', default: 'json', options: ['json', 'dot', 'mermaid'] }), depth: Flags.integer({ char: 'd', description: 'Maximum dependency depth to analyze', default: 5 }), verbose: Flags.boolean({ char: 'v', description: 'Enable verbose output', default: false }) }; async run() { const { flags } = await this.parse(DependencyAnalyzer); console.log(chalk.blue.bold('\nšŸ”— Cross-File Dependency Analyzer')); console.log(chalk.gray('Analyzing file dependencies and building dependency graphs...\n')); try { // Create dependency analyzer const analyzer = this.createDependencyAnalyzer(); // Perform analysis const results = await this.performDependencyAnalysis(flags, analyzer); // Display results this.displayResults(results, flags.verbose); // Save results if (flags.output) { await this.saveResults(results, flags.output, flags.format); console.log(chalk.green(`\nāœ… Dependency graph saved to: ${flags.output}`)); } } catch (error) { console.error(chalk.red(`\nāŒ Error during dependency analysis: ${error.message}`)); this.exit(1); } } createDependencyAnalyzer() { return { // Inter-file Dependency Analysis analyzeInterFileDependencies: (files) => { const dependencies = []; files.forEach(file => { if (file.content) { const fileDeps = this.extractFileDependencies(file.content, file.language, file.path); dependencies.push(...fileDeps); } }); return dependencies; }, // Graph Construction buildDependencyGraph: (dependencies, files) => { const graph = { nodes: [], edges: [], clusters: [], metrics: {} }; // Create nodes for each file files.forEach(file => { graph.nodes.push({ id: file.path, label: path.basename(file.path), type: this.getFileType(file.path), language: file.language, size: file.content ? file.content.length : 0, lines: file.content ? file.content.split('\n').length : 0 }); }); // Create edges for dependencies dependencies.forEach(dep => { if (dep.target && dep.source) { graph.edges.push({ source: dep.source, target: dep.target, type: dep.type, weight: dep.weight || 1 }); } }); // Identify clusters graph.clusters = this.identifyClusters(graph.nodes, graph.edges); // Calculate metrics graph.metrics = this.calculateGraphMetrics(graph); return graph; }, // Cycle Detection detectCycles: (graph) => { const cycles = []; const visited = new Set(); const recursionStack = new Set(); const dfs = (node, path) => { if (recursionStack.has(node)) { // Cycle detected const cycleStart = path.indexOf(node); cycles.push(path.slice(cycleStart)); return; } if (visited.has(node)) { return; } visited.add(node); recursionStack.add(node); const outgoingEdges = graph.edges.filter(edge => edge.source === node); outgoingEdges.forEach(edge => { dfs(edge.target, [...path, node]); }); recursionStack.delete(node); }; graph.nodes.forEach(node => { if (!visited.has(node.id)) { dfs(node.id, []); } }); return cycles; }, // Metrics Calculation calculateMetrics: (graph) => { const metrics = { modularity: 0, efficiency: 0, robustness: 0, resilience: 0, coupling: 0, cohesion: 0 }; // Calculate modularity metrics.modularity = this.calculateModularity(graph); // Calculate efficiency metrics.efficiency = this.calculateEfficiency(graph); // Calculate robustness metrics.robustness = this.calculateRobustness(graph); // Calculate resilience metrics.resilience = this.calculateResilience(graph); // Calculate coupling metrics.coupling = this.calculateCoupling(graph); // Calculate cohesion metrics.cohesion = this.calculateCohesion(graph); return metrics; } }; } async performDependencyAnalysis(flags, analyzer) { const results = { timestamp: new Date().toISOString(), path: flags.path, files: [], dependencies: [], graph: {}, cycles: [], metrics: {}, recommendations: [] }; // Find files to analyze const files = await this.findFiles(flags); results.files = files; // Analyze dependencies results.dependencies = analyzer.analyzeInterFileDependencies(files); // Build dependency graph results.graph = analyzer.buildDependencyGraph(results.dependencies, files); // Detect cycles results.cycles = analyzer.detectCycles(results.graph); // Calculate metrics results.metrics = analyzer.calculateMetrics(results.graph); // Generate recommendations results.recommendations = this.generateRecommendations(results); return results; } async findFiles(flags) { const includePatterns = flags.include.split(','); const excludePatterns = flags.exclude.split(','); const files = []; for (const pattern of includePatterns) { const matches = glob.sync(pattern, { cwd: flags.path, absolute: true, ignore: excludePatterns }); files.push(...matches.map(file => ({ path: file }))); } // Read file contents for (const file of files) { try { const content = await fs.readFile(file.path, 'utf8'); const language = this.detectLanguage(file.path); file.content = content; file.language = language; file.size = content.length; file.lines = content.split('\n').length; } catch (error) { if (flags.verbose) { console.warn(chalk.yellow(`āš ļø Could not read file: ${file.path}`)); } } } return files; } detectLanguage(filePath) { const ext = path.extname(filePath).toLowerCase(); const languageMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.java': 'java', '.cs': 'csharp' }; return languageMap[ext] || 'unknown'; } getFileType(filePath) { if (filePath.includes('test') || filePath.includes('spec')) { return 'test'; } else if (filePath.includes('config') || filePath.includes('Config')) { return 'config'; } else if (filePath.includes('util') || filePath.includes('helper')) { return 'utility'; } else if (filePath.includes('component') || filePath.includes('Component')) { return 'component'; } else { return 'source'; } } extractFileDependencies(content, language, filePath) { const dependencies = []; if (language === 'javascript' || language === 'typescript') { // Import statements const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g; let match; while ((match = importRegex.exec(content)) !== null) { const importPath = match[1]; const resolvedPath = this.resolveImportPath(importPath, filePath); if (resolvedPath) { dependencies.push({ source: filePath, target: resolvedPath, type: 'import', weight: 1 }); } } // Require statements const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; while ((match = requireRegex.exec(content)) !== null) { const requirePath = match[1]; const resolvedPath = this.resolveImportPath(requirePath, filePath); if (resolvedPath) { dependencies.push({ source: filePath, target: resolvedPath, type: 'require', weight: 1 }); } } } else if (language === 'python') { // Import statements const importRegex = /import\s+([a-zA-Z_][a-zA-Z0-9_]*)/g; let match; while ((match = importRegex.exec(content)) !== null) { const moduleName = match[1]; dependencies.push({ source: filePath, target: moduleName, type: 'import', weight: 1 }); } } return dependencies; } resolveImportPath(importPath, fromFile) { // Simple path resolution - in a real implementation, this would be more sophisticated if (importPath.startsWith('./') || importPath.startsWith('../')) { const resolvedPath = path.resolve(path.dirname(fromFile), importPath); return resolvedPath; } else if (importPath.startsWith('/')) { return importPath; } else { // Node modules or other packages return null; } } identifyClusters(nodes, edges) { const clusters = []; const visited = new Set(); const dfs = (node, cluster) => { if (visited.has(node)) return; visited.add(node); cluster.push(node); const outgoingEdges = edges.filter(edge => edge.source === node); const incomingEdges = edges.filter(edge => edge.target === node); [...outgoingEdges, ...incomingEdges].forEach(edge => { const connectedNode = edge.source === node ? edge.target : edge.source; dfs(connectedNode, cluster); }); }; nodes.forEach(node => { if (!visited.has(node.id)) { const cluster = []; dfs(node.id, cluster); if (cluster.length > 1) { clusters.push({ id: `cluster_${clusters.length}`, nodes: cluster, size: cluster.length }); } } }); return clusters; } calculateModularity(graph) { // Simple modularity calculation based on cluster structure const totalNodes = graph.nodes.length; const totalEdges = graph.edges.length; if (totalNodes === 0 || totalEdges === 0) return 0; const clusters = graph.clusters; let modularity = 0; clusters.forEach(cluster => { const clusterNodes = cluster.nodes; const clusterEdges = graph.edges.filter(edge => clusterNodes.includes(edge.source) && clusterNodes.includes(edge.target) ); const clusterSize = clusterNodes.length; const clusterEdgeCount = clusterEdges.length; if (clusterSize > 0) { modularity += (clusterEdgeCount / totalEdges) - Math.pow(clusterSize / totalNodes, 2); } }); return Math.max(0, modularity); } calculateEfficiency(graph) { // Efficiency based on shortest paths and connectivity const totalNodes = graph.nodes.length; if (totalNodes <= 1) return 1; const totalEdges = graph.edges.length; const maxPossibleEdges = totalNodes * (totalNodes - 1); return totalEdges / maxPossibleEdges; } calculateRobustness(graph) { // Robustness based on node degree distribution const nodeDegrees = graph.nodes.map(node => { const outgoing = graph.edges.filter(edge => edge.source === node.id).length; const incoming = graph.edges.filter(edge => edge.target === node.id).length; return outgoing + incoming; }); const avgDegree = nodeDegrees.reduce((sum, degree) => sum + degree, 0) / nodeDegrees.length; const maxDegree = Math.max(...nodeDegrees); return maxDegree > 0 ? avgDegree / maxDegree : 1; } calculateResilience(graph) { // Resilience based on cycle detection and critical nodes const cycles = graph.cycles || []; const totalNodes = graph.nodes.length; if (totalNodes === 0) return 1; const cycleNodes = new Set(); cycles.forEach(cycle => { cycle.forEach(node => cycleNodes.add(node)); }); return 1 - (cycleNodes.size / totalNodes); } calculateCoupling(graph) { // Coupling based on inter-cluster connections const totalEdges = graph.edges.length; if (totalEdges === 0) return 0; const interClusterEdges = graph.edges.filter(edge => { const sourceCluster = this.getNodeCluster(edge.source, graph.clusters); const targetCluster = this.getNodeCluster(edge.target, graph.clusters); return sourceCluster !== targetCluster; }); return interClusterEdges.length / totalEdges; } calculateCohesion(graph) { // Cohesion based on intra-cluster connections const totalEdges = graph.edges.length; if (totalEdges === 0) return 0; const intraClusterEdges = graph.edges.filter(edge => { const sourceCluster = this.getNodeCluster(edge.source, graph.clusters); const targetCluster = this.getNodeCluster(edge.target, graph.clusters); return sourceCluster === targetCluster; }); return intraClusterEdges.length / totalEdges; } getNodeCluster(nodeId, clusters) { for (const cluster of clusters) { if (cluster.nodes.includes(nodeId)) { return cluster.id; } } return null; } generateRecommendations(results) { const recommendations = []; // Cycle recommendations if (results.cycles.length > 0) { recommendations.push({ type: 'cycle', priority: 'high', title: 'Circular Dependencies Detected', description: `${results.cycles.length} circular dependencies found`, action: 'Refactor code to eliminate circular dependencies' }); } // Coupling recommendations if (results.metrics.coupling > 0.7) { recommendations.push({ type: 'coupling', priority: 'medium', title: 'High Coupling Detected', description: `Coupling level: ${results.metrics.coupling.toFixed(2)}`, action: 'Reduce inter-module dependencies and improve encapsulation' }); } // Cohesion recommendations if (results.metrics.cohesion < 0.3) { recommendations.push({ type: 'cohesion', priority: 'medium', title: 'Low Cohesion Detected', description: `Cohesion level: ${results.metrics.cohesion.toFixed(2)}`, action: 'Group related functionality together and improve module organization' }); } // Modularity recommendations if (results.metrics.modularity < 0.5) { recommendations.push({ type: 'modularity', priority: 'low', title: 'Improve Modularity', description: `Modularity score: ${results.metrics.modularity.toFixed(2)}`, action: 'Consider breaking down large modules into smaller, more focused ones' }); } return recommendations; } displayResults(results, verbose) { console.log(chalk.green.bold('\nšŸ“Š Dependency Analysis Results')); console.log(chalk.gray('=' * 50)); // File Statistics console.log(chalk.blue.bold('\nšŸ“ File Statistics:')); console.log(` Total files analyzed: ${results.files.length}`); console.log(` Total dependencies: ${results.dependencies.length}`); console.log(` Graph nodes: ${results.graph.nodes.length}`); console.log(` Graph edges: ${results.graph.edges.length}`); // Cluster Information console.log(chalk.blue.bold('\nšŸ”— Clusters:')); if (results.graph.clusters.length > 0) { results.graph.clusters.forEach((cluster, index) => { console.log(` Cluster ${index + 1}: ${cluster.size} nodes`); }); } else { console.log(' No clusters detected'); } // Cycle Detection console.log(chalk.blue.bold('\nšŸ”„ Cycles:')); if (results.cycles.length > 0) { console.log(` ${results.cycles.length} circular dependencies found`); results.cycles.forEach((cycle, index) => { console.log(` Cycle ${index + 1}: ${cycle.join(' → ')}`); }); } else { console.log(' No circular dependencies detected'); } // Metrics console.log(chalk.blue.bold('\nšŸ“ˆ Metrics:')); console.log(` Modularity: ${results.metrics.modularity.toFixed(3)}`); console.log(` Efficiency: ${results.metrics.efficiency.toFixed(3)}`); console.log(` Robustness: ${results.metrics.robustness.toFixed(3)}`); console.log(` Resilience: ${results.metrics.resilience.toFixed(3)}`); console.log(` Coupling: ${results.metrics.coupling.toFixed(3)}`); console.log(` Cohesion: ${results.metrics.cohesion.toFixed(3)}`); // Recommendations console.log(chalk.blue.bold('\nšŸ’” Recommendations:')); results.recommendations.forEach((rec, index) => { const priorityColor = rec.priority === 'high' ? chalk.red : rec.priority === 'medium' ? chalk.yellow : chalk.green; console.log(` ${index + 1}. ${priorityColor(rec.title)} (${rec.priority})`); console.log(` ${rec.description}`); console.log(` Action: ${rec.action}`); }); if (verbose) { console.log(chalk.blue.bold('\nšŸ” Detailed Analysis:')); console.log(JSON.stringify(results, null, 2)); } } async saveResults(results, outputPath, format) { if (format === 'json') { await fs.writeJson(outputPath, results, { spaces: 2 }); } else if (format === 'dot') { const dotContent = this.generateDotFormat(results); await fs.writeFile(outputPath, dotContent); } else if (format === 'mermaid') { const mermaidContent = this.generateMermaidFormat(results); await fs.writeFile(outputPath, mermaidContent); } } generateDotFormat(results) { let dot = 'digraph DependencyGraph {\n'; dot += ' rankdir=LR;\n'; dot += ' node [shape=box];\n\n'; // Add nodes results.graph.nodes.forEach(node => { dot += ` "${node.id}" [label="${node.label}"];\n`; }); // Add edges results.graph.edges.forEach(edge => { dot += ` "${edge.source}" -> "${edge.target}" [label="${edge.type}"];\n`; }); dot += '}\n'; return dot; } generateMermaidFormat(results) { let mermaid = 'graph TD\n'; // Add nodes results.graph.nodes.forEach(node => { mermaid += ` ${node.id.replace(/[^a-zA-Z0-9]/g, '_')}["${node.label}"]\n`; }); // Add edges results.graph.edges.forEach(edge => { const source = edge.source.replace(/[^a-zA-Z0-9]/g, '_'); const target = edge.target.replace(/[^a-zA-Z0-9]/g, '_'); mermaid += ` ${source} --> ${target}\n`; }); return mermaid; } } module.exports.default = DependencyAnalyzer;