smartui-migration-tool
Version:
Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI
669 lines (556 loc) ⢠20.3 kB
JavaScript
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;