UNPKG

@vibe-dev-kit/cli

Version:

Advanced Command-line toolkit that analyzes your codebase and deploys project-aware rules, memories, commands and agents to any AI coding assistant - VDK is the world's first Vibe Development Kit

621 lines (539 loc) 20.3 kB
/** * DependencyAnalyzer.js * * Analyzes import/require statements in code to build a dependency graph * and enhance architectural pattern detection with more sophisticated * insights beyond just directory naming conventions. */ import chalk from 'chalk'; import fs from 'fs/promises'; import path from 'path'; export class DependencyAnalyzer { constructor(options = {}) { this.verbose = options.verbose || false; this.maxFilesToParse = options.maxFilesToParse || 200; // Limit files to parse for performance this.dependencyGraph = new Map(); // Map of module -> Set(dependencies) this.inverseGraph = new Map(); // Map of module -> Set(dependents) this.fileModuleMap = new Map(); // Map of filePath -> logical module name this.moduleFileMap = new Map(); // Map of logical module name -> filePath this.ignoredExtensions = new Set([ '.json', '.md', '.txt', '.css', '.scss', '.png', '.jpg', '.gif', '.svg', ]); } /** * Analyzes import/require statements in the project to build a dependency graph * @param {Object} projectStructure - Project structure from ProjectScanner * @param {Object} techData - Technology data from TechnologyAnalyzer * @returns {Object} Dependency graph analysis */ async analyzeDependencies(projectStructure, techData) { if (this.verbose) { console.log(chalk.gray('Building dependency graph...')); } // Reset dependency graph this.dependencyGraph.clear(); this.inverseGraph.clear(); this.fileModuleMap.clear(); this.moduleFileMap.clear(); try { // Create a list of files to analyze, prioritizing key file types const filesToAnalyze = this.getFilesToAnalyze(projectStructure, techData); // Extract dependencies from each file await this.extractDependenciesFromFiles(filesToAnalyze, projectStructure.projectPath); // Analyze the graph const graphAnalysis = this.analyzeGraph(); if (this.verbose) { console.log( chalk.gray( `Dependency graph built with ${this.dependencyGraph.size} modules and ${this.countEdges()} edges` ) ); } return { dependencyGraph: this.dependencyGraph, inverseGraph: this.inverseGraph, moduleCount: this.dependencyGraph.size, edgeCount: this.countEdges(), centralModules: graphAnalysis.centralModules, layeredStructure: graphAnalysis.layeredStructure, cyclesDetected: graphAnalysis.cyclesDetected, architecturalHints: graphAnalysis.architecturalHints, }; } catch (error) { if (this.verbose) { console.error(chalk.red(`Error in dependency analysis: ${error.message}`)); console.error(chalk.gray(error.stack)); } // Return an empty analysis instead of failing completely return { dependencyGraph: new Map(), inverseGraph: new Map(), moduleCount: 0, edgeCount: 0, centralModules: [], layeredStructure: [], cyclesDetected: false, architecturalHints: [], }; } } /** * Get a prioritized list of files to analyze * @param {Object} projectStructure - Project structure from ProjectScanner * @param {Object} techData - Technology data from TechnologyAnalyzer * @returns {Array} Files to analyze */ getFilesToAnalyze(projectStructure, techData) { // Get all source files let files = projectStructure.files || []; // Filter out irrelevant files files = files.filter((file) => { const ext = path.extname(file.path || file).toLowerCase(); return !this.ignoredExtensions.has(ext); }); // Sort files by their likely importance files.sort((a, b) => { const fileA = typeof a === 'object' ? a.path : a; const fileB = typeof b === 'object' ? b.path : b; // Put index files and main files first const nameA = path.basename(fileA).toLowerCase(); const nameB = path.basename(fileB).toLowerCase(); if (nameA.includes('index') && !nameB.includes('index')) return -1; if (!nameA.includes('index') && nameB.includes('index')) return 1; if (nameA.includes('main') && !nameB.includes('main')) return -1; if (!nameA.includes('main') && nameB.includes('main')) return 1; // Prioritize specific directories const isInSrcA = fileA.includes('/src/') || fileA.includes('\\src\\'); const isInSrcB = fileB.includes('/src/') || fileB.includes('\\src\\'); if (isInSrcA && !isInSrcB) return -1; if (!isInSrcA && isInSrcB) return 1; return 0; }); // Limit the number of files for performance return files.slice(0, this.maxFilesToParse); } /** * Extract dependencies from a list of files * @param {Array} files - List of files to analyze * @param {string} projectRoot - Root directory of the project */ async extractDependenciesFromFiles(files, projectRoot) { for (const file of files) { const filePath = typeof file === 'object' ? file.path : file; try { // Skip binary files and already processed files const ext = path.extname(filePath).toLowerCase(); if (this.ignoredExtensions.has(ext) || this.fileModuleMap.has(filePath)) { continue; } // Read the file const content = await fs.readFile(filePath, 'utf8'); // Generate a logical module name const moduleName = this.getLogicalModuleName(filePath, projectRoot); this.fileModuleMap.set(filePath, moduleName); this.moduleFileMap.set(moduleName, filePath); // Initialize the module's dependency sets if they don't exist if (!this.dependencyGraph.has(moduleName)) { this.dependencyGraph.set(moduleName, new Set()); } if (!this.inverseGraph.has(moduleName)) { this.inverseGraph.set(moduleName, new Set()); } // Extract dependencies based on file type const dependencies = this.extractDependenciesFromContent(content, ext); // Resolve relative dependencies to logical module names const resolvedDependencies = this.resolveDependencies(dependencies, filePath, projectRoot); // Add dependencies to the graph for (const dep of resolvedDependencies) { this.dependencyGraph.get(moduleName).add(dep); // Add to inverse graph if (!this.inverseGraph.has(dep)) { this.inverseGraph.set(dep, new Set()); } this.inverseGraph.get(dep).add(moduleName); } } catch (error) { // Skip files that can't be read if (this.verbose) { console.log(chalk.yellow(`Skipping file ${filePath}: ${error.message}`)); } } } } /** * Extract dependencies from file content * @param {string} content - File content * @param {string} fileExtension - File extension * @returns {Array} List of dependencies */ extractDependenciesFromContent(content, fileExtension) { const dependencies = new Set(); // JavaScript/TypeScript import statements if (['.js', '.jsx', '.ts', '.tsx'].includes(fileExtension)) { // ES imports const esImportRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g; let match; while ((match = esImportRegex.exec(content)) !== null) { dependencies.add(match[1]); } // CommonJS requires const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; while ((match = requireRegex.exec(content)) !== null) { dependencies.add(match[1]); } // Dynamic imports const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g; while ((match = dynamicImportRegex.exec(content)) !== null) { dependencies.add(match[1]); } } // Python imports else if (['.py'].includes(fileExtension)) { // Regular import statements const pyImportRegex = /^\s*import\s+([^\s.]+(?:\.[^\s,]+)*)/gm; let match; while ((match = pyImportRegex.exec(content)) !== null) { // Split multi-imports like "import os, sys" const imports = match[1].split(',').map((i) => i.trim()); for (const imp of imports) { dependencies.add(imp); } } // From import statements const fromImportRegex = /^\s*from\s+([^\s]+)\s+import/gm; while ((match = fromImportRegex.exec(content)) !== null) { dependencies.add(match[1]); } } // Java/Kotlin imports else if (['.java', '.kt'].includes(fileExtension)) { const javaImportRegex = /^\s*import\s+([^;]+);/gm; let match; while ((match = javaImportRegex.exec(content)) !== null) { dependencies.add(match[1]); } } // C# using statements else if (['.cs'].includes(fileExtension)) { const csharpUsingRegex = /^\s*using\s+([^;]+);/gm; let match; while ((match = csharpUsingRegex.exec(content)) !== null) { dependencies.add(match[1]); } } return Array.from(dependencies); } /** * Resolve relative dependencies to logical module names * @param {Array} dependencies - List of dependencies * @param {string} currentFilePath - Path of the file containing the dependencies * @param {string} projectRoot - Root directory of the project * @returns {Array} List of resolved dependencies */ resolveDependencies(dependencies, currentFilePath, projectRoot) { const resolvedDeps = new Set(); // Validate inputs if (!currentFilePath || !projectRoot || !Array.isArray(dependencies)) { // Only log warnings in verbose mode to reduce noise if (this.verbose) { console.log( chalk.yellow( `Invalid parameters for dependency resolution: currentFilePath=${currentFilePath}, projectRoot=${projectRoot}, dependencies=${dependencies}` ) ); } return []; } const currentDir = path.dirname(currentFilePath); for (const dep of dependencies) { // Skip invalid dependencies if (!dep || typeof dep !== 'string') { continue; } // Skip node_modules and external dependencies if (dep.startsWith('@') || !dep.startsWith('.')) { // For now, just skip external modules continue; } try { // Resolve relative path const absolutePath = path.resolve(currentDir, dep); const relativePath = path.relative(projectRoot, absolutePath); // Generate logical module name const moduleName = this.getLogicalModuleName(relativePath, projectRoot); if (moduleName) { resolvedDeps.add(moduleName); } } catch (error) { // Skip dependencies that can't be resolved if (this.verbose) { console.log(chalk.yellow(`Could not resolve dependency ${dep}: ${error.message}`)); } } } return Array.from(resolvedDeps); } /** * Generate a logical module name from a file path * @param {string} filePath - Path to the file * @param {string} projectRoot - Root directory of the project * @returns {string} Logical module name */ getLogicalModuleName(filePath, projectRoot) { // Validate inputs if ( !filePath || typeof filePath !== 'string' || !projectRoot || typeof projectRoot !== 'string' ) { return null; } // Remove file extension let modulePath = filePath.replace(/\.[^/.]+$/, ''); // Make path relative to project root if (modulePath.startsWith(projectRoot)) { modulePath = path.relative(projectRoot, modulePath); } // Normalize separators modulePath = modulePath.replace(/\\/g, '/'); // Remove index files modulePath = modulePath.replace(/\/index$/, ''); return modulePath || null; } /** * Analyze the dependency graph for architectural patterns * @returns {Object} Analysis results */ analyzeGraph() { const analysis = { centralModules: this.findCentralModules(), layeredStructure: this.detectLayers(), cyclesDetected: this.detectCycles(), architecturalHints: [], }; // Generate architectural hints based on analysis if (analysis.layeredStructure && analysis.layeredStructure.length > 0) { analysis.architecturalHints.push({ pattern: 'Layered Architecture', confidence: 80, evidence: `Found ${analysis.layeredStructure.length} distinct layers in the codebase.`, }); } if (analysis.centralModules.length > 0) { const centralityPattern = this.inferPatternFromCentralModules(analysis.centralModules); analysis.architecturalHints.push(centralityPattern); } if (analysis.cyclesDetected) { analysis.architecturalHints.push({ pattern: 'Circular Dependencies', confidence: 70, evidence: 'Detected circular dependencies, which might indicate architectural issues.', }); } return analysis; } /** * Find central modules in the dependency graph based on incoming/outgoing connections * @returns {Array} List of central modules with metrics */ findCentralModules() { const centralModules = []; // Calculate centrality metrics for each module for (const [module, dependencies] of this.dependencyGraph.entries()) { const outDegree = dependencies.size; // How many modules this depends on const inDegree = this.inverseGraph.has(module) ? this.inverseGraph.get(module).size : 0; // How many depend on this const centralityScore = inDegree * 2 + outDegree; // Weight incoming more heavily if (centralityScore > 0) { centralModules.push({ name: module, inDegree, outDegree, centralityScore, }); } } // Sort by centrality score in descending order centralModules.sort((a, b) => b.centralityScore - a.centralityScore); // Return top 10 most central modules return centralModules.slice(0, 10); } /** * Detect layers in the dependency graph * @returns {Array} List of layers */ detectLayers() { // Initialize layers const layers = []; const visited = new Set(); // Start with leaf nodes (those with no outgoing dependencies or only external ones) const currentLayer = Array.from(this.dependencyGraph.keys()).filter((module) => { const deps = this.dependencyGraph.get(module); return deps.size === 0 || Array.from(deps).every((dep) => !this.dependencyGraph.has(dep)); }); // Mark current layer as visited currentLayer.forEach((module) => visited.add(module)); // Add layer if not empty if (currentLayer.length > 0) { layers.push({ level: layers.length, modules: currentLayer, name: 'Infrastructure/Utility Layer', }); } // Continue until all modules are visited while (visited.size < this.dependencyGraph.size) { // Find modules that depend only on already visited modules const nextLayer = Array.from(this.dependencyGraph.keys()) .filter((module) => !visited.has(module)) // Not already visited .filter((module) => { const deps = this.dependencyGraph.get(module); return Array.from(deps).every( (dep) => visited.has(dep) || !this.dependencyGraph.has(dep) ); }); // If we can't find any more modules for the next layer, break if (nextLayer.length === 0) break; // Add current layer and mark as visited nextLayer.forEach((module) => visited.add(module)); // Assign a name based on layer position let layerName = 'Intermediate Layer'; if (layers.length === 0) layerName = 'Infrastructure/Utility Layer'; else if ( nextLayer.every((m) => { const inDegree = this.inverseGraph.has(m) ? this.inverseGraph.get(m).size : 0; return inDegree === 0; }) ) { layerName = 'Entry Points/UI Layer'; } layers.push({ level: layers.length, modules: nextLayer, name: layerName, }); } return layers; } /** * Detect cycles in the dependency graph * @returns {boolean} True if cycles are detected */ detectCycles() { // Simple DFS to detect cycles const visited = new Set(); const recursionStack = new Set(); const hasCycle = (module) => { // Mark current node as visited and add to recursion stack visited.add(module); recursionStack.add(module); // Check all dependencies const dependencies = this.dependencyGraph.get(module) || new Set(); for (const dep of dependencies) { // Skip external dependencies if (!this.dependencyGraph.has(dep)) continue; // If not visited, check if there's a cycle in its dependencies if (!visited.has(dep)) { if (hasCycle(dep)) return true; } // If the dependency is in recursion stack, we found a cycle else if (recursionStack.has(dep)) { return true; } } // Remove from recursion stack recursionStack.delete(module); return false; }; // Check each module for (const module of this.dependencyGraph.keys()) { if (!visited.has(module)) { if (hasCycle(module)) return true; } } return false; } /** * Infer architectural pattern from central modules * @param {Array} centralModules - List of central modules * @returns {Object} Pattern description */ inferPatternFromCentralModules(centralModules) { // Check module names for clues const moduleNames = centralModules.map((m) => m.name.toLowerCase()); const hasController = moduleNames.some((name) => name.includes('controller')); const hasService = moduleNames.some((name) => name.includes('service')); const hasStore = moduleNames.some((name) => name.includes('store') || name.includes('redux')); const hasViewModel = moduleNames.some((name) => name.includes('viewmodel')); const hasRepository = moduleNames.some( (name) => name.includes('repository') || name.includes('dao') ); const hasProvider = moduleNames.some((name) => name.includes('provider')); const hasComponent = moduleNames.some((name) => name.includes('component')); // Detecting patterns based on naming conventions in central modules if (hasController && hasService) { return { pattern: 'MVC/Service', confidence: 85, evidence: 'Detected controller and service modules as central components.', }; } else if (hasViewModel) { return { pattern: 'MVVM', confidence: 85, evidence: 'Detected viewmodel modules as central components.', }; } else if (hasStore) { return { pattern: 'Flux/Redux', confidence: 90, evidence: 'Detected store modules as central components.', }; } else if (hasRepository) { return { pattern: 'Repository Pattern', confidence: 80, evidence: 'Detected repository modules as central components.', }; } else if (hasProvider) { return { pattern: 'Provider Pattern', confidence: 75, evidence: 'Detected provider modules as central components.', }; } else if (hasComponent && centralModules.length > 5) { return { pattern: 'Component-Based', confidence: 80, evidence: 'Detected multiple component modules as central parts of the architecture.', }; } else { // Default if we can't detect a specific pattern return { pattern: 'Modular Architecture', confidence: 65, evidence: `Detected ${centralModules.length} central modules with high interdependency.`, }; } } /** * Count the total number of edges in the dependency graph * @returns {number} Total number of edges */ countEdges() { let count = 0; for (const dependencies of this.dependencyGraph.values()) { count += dependencies.size; } return count; } }