UNPKG

code-auditor-mcp

Version:

Multi-language code quality auditor with MCP server - Analyze TypeScript, JavaScript, and Go code for SOLID principles, DRY violations, security patterns, and more

473 lines 17.5 kB
/** * Cross-Language Dependency Graph Builder * Creates dependency graphs that span multiple programming languages */ export class DependencyGraphBuilder { entities = []; references = []; options; constructor(options = {}) { this.options = { includeInternalDependencies: true, includeExternalDependencies: true, maxDepth: 10, includeTestFiles: false, clusterByPackage: true, ...options }; } /** * Build a comprehensive dependency graph */ async buildGraph(entities, references) { this.entities = entities; this.references = references; console.log(`[DependencyGraphBuilder] Building graph from ${entities.length} entities and ${references.length} references`); // Filter entities based on options const filteredEntities = this.filterEntities(entities); // Create nodes const nodes = this.createNodes(filteredEntities); // Create edges from references const edges = this.createEdges(references, nodes); // Detect cycles const cycles = this.detectCycles(nodes, edges); // Calculate metrics const metrics = this.calculateMetrics(nodes, edges, cycles); // Apply clustering if enabled if (this.options.clusterByPackage) { this.applyPackageClustering(nodes); } const graph = { nodes, edges, cycles, metrics }; console.log(`[DependencyGraphBuilder] Generated graph with ${nodes.length} nodes, ${edges.length} edges, ${cycles.length} cycles`); return graph; } /** * Build a focused subgraph around specific entities */ async buildSubgraph(targetEntityIds, entities, references, depth = 2) { console.log(`[DependencyGraphBuilder] Building subgraph for ${targetEntityIds.length} target entities with depth ${depth}`); // Find all entities within the specified depth const reachableEntities = this.findReachableEntities(targetEntityIds, entities, references, depth); // Build graph with only reachable entities return this.buildGraph(reachableEntities, references.filter(ref => reachableEntities.some(e => e.id === ref.sourceId) && reachableEntities.some(e => e.id === ref.targetId))); } /** * Analyze dependency health and suggest improvements */ async analyzeDependencyHealth(graph) { const issues = []; const suggestions = []; // Check for circular dependencies if (graph.cycles.length > 0) { issues.push({ type: 'circular-dependency', severity: 'critical', description: `Found ${graph.cycles.length} circular dependencies`, affectedNodes: graph.cycles.flatMap(cycle => cycle.nodes), impact: 'high' }); suggestions.push({ type: 'break-cycles', priority: 'high', description: 'Break circular dependencies by introducing interfaces or dependency injection', implementation: 'Consider using dependency inversion principle to break cycles' }); } // Check for tightly coupled clusters const tightlyCoupledClusters = this.findTightlyCoupledClusters(graph); if (tightlyCoupledClusters.length > 0) { issues.push({ type: 'tight-coupling', severity: 'warning', description: `Found ${tightlyCoupledClusters.length} tightly coupled clusters`, affectedNodes: tightlyCoupledClusters.flatMap(cluster => cluster.nodes), impact: 'medium' }); suggestions.push({ type: 'reduce-coupling', priority: 'medium', description: 'Reduce coupling between modules using interfaces and abstractions', implementation: 'Extract common interfaces and use dependency injection' }); } // Check for hub nodes (too many dependencies) const hubNodes = this.findHubNodes(graph); if (hubNodes.length > 0) { issues.push({ type: 'hub-nodes', severity: 'warning', description: `Found ${hubNodes.length} hub nodes with excessive dependencies`, affectedNodes: hubNodes.map(node => node.id), impact: 'medium' }); suggestions.push({ type: 'split-responsibilities', priority: 'medium', description: 'Split large modules to reduce their dependency burden', implementation: 'Apply Single Responsibility Principle to break down large modules' }); } // Check for orphaned nodes const orphanedNodes = this.findOrphanedNodes(graph); if (orphanedNodes.length > 0) { issues.push({ type: 'orphaned-nodes', severity: 'suggestion', description: `Found ${orphanedNodes.length} orphaned nodes with no dependencies`, affectedNodes: orphanedNodes.map(node => node.id), impact: 'low' }); suggestions.push({ type: 'review-orphans', priority: 'low', description: 'Review orphaned nodes to ensure they are still needed', implementation: 'Consider removing unused code or integrating orphaned modules' }); } // Calculate health score const healthScore = this.calculateHealthScore(graph, issues); return { healthScore, issues, suggestions }; } /** * Filter entities based on options */ filterEntities(entities) { let filtered = entities; // Filter by language if (this.options.excludeLanguages?.length) { filtered = filtered.filter(entity => !this.options.excludeLanguages.includes(entity.language)); } // Filter test files if (!this.options.includeTestFiles) { filtered = filtered.filter(entity => !this.isTestFile(entity.file)); } return filtered; } /** * Create graph nodes from entities */ createNodes(entities) { return entities.map(entity => ({ id: entity.id, name: entity.name, language: entity.language, type: entity.type, file: entity.file, weight: this.calculateNodeWeight(entity), cluster: this.determineCluster(entity) })); } /** * Create graph edges from references */ createEdges(references, nodes) { const nodeIds = new Set(nodes.map(node => node.id)); return references .filter(ref => nodeIds.has(ref.sourceId) && nodeIds.has(ref.targetId)) .map(ref => ({ from: ref.sourceId, to: ref.targetId, type: ref.type, weight: ref.confidence, protocol: ref.protocol })); } /** * Detect circular dependencies using DFS */ detectCycles(nodes, edges) { const cycles = []; const visited = new Set(); const recursionStack = new Set(); const adjList = this.buildAdjacencyList(edges); const dfs = (nodeId, path) => { visited.add(nodeId); recursionStack.add(nodeId); path.push(nodeId); const neighbors = adjList.get(nodeId) || []; for (const neighbor of neighbors) { if (!visited.has(neighbor)) { dfs(neighbor, [...path]); } else if (recursionStack.has(neighbor)) { // Found a cycle const cycleStart = path.indexOf(neighbor); const cycleNodes = path.slice(cycleStart); cycles.push({ nodes: cycleNodes, severity: cycleNodes.length > 5 ? 'critical' : 'warning', suggestion: this.generateCycleSuggestion(cycleNodes) }); } } recursionStack.delete(nodeId); }; for (const node of nodes) { if (!visited.has(node.id)) { dfs(node.id, []); } } return cycles; } /** * Calculate various graph metrics */ calculateMetrics(nodes, edges, cycles) { const adjList = this.buildAdjacencyList(edges); // Calculate depths from each node const depths = nodes.map(node => this.calculateMaxDepth(node.id, adjList)); return { totalNodes: nodes.length, totalEdges: edges.length, cycleCount: cycles.length, averageDepth: depths.reduce((sum, depth) => sum + depth, 0) / depths.length, maxDepth: Math.max(...depths), stronglyConnectedComponents: this.countStronglyConnectedComponents(nodes, edges) }; } /** * Find entities reachable within specified depth */ findReachableEntities(startIds, entities, references, maxDepth) { const reachable = new Set(startIds); const adjList = this.buildAdjacencyListFromReferences(references); let currentLevel = new Set(startIds); for (let depth = 0; depth < maxDepth && currentLevel.size > 0; depth++) { const nextLevel = new Set(); for (const nodeId of currentLevel) { const neighbors = adjList.get(nodeId) || []; for (const neighbor of neighbors) { if (!reachable.has(neighbor)) { reachable.add(neighbor); nextLevel.add(neighbor); } } } currentLevel = nextLevel; } return entities.filter(entity => reachable.has(entity.id)); } /** * Apply package-based clustering to nodes */ applyPackageClustering(nodes) { for (const node of nodes) { if (!node.cluster) { node.cluster = this.extractPackageFromFile(node.file, node.language); } } } /** * Calculate node weight based on various factors */ calculateNodeWeight(entity) { let weight = 1; // Increase weight for exported/public entities if (entity.visibility === 'public' || entity.metadata?.isExported) { weight += 2; } // Increase weight for complex entities if (entity.complexity && entity.complexity > 5) { weight += Math.floor(entity.complexity / 5); } // Increase weight for interfaces and services if (entity.type === 'interface' || entity.type === 'service') { weight += 3; } return weight; } /** * Determine cluster for an entity */ determineCluster(entity) { if (this.options.clusterByPackage) { return this.extractPackageFromFile(entity.file, entity.language); } return entity.language; } /** * Extract package/module name from file path */ extractPackageFromFile(filePath, language) { const parts = filePath.split('/'); switch (language) { case 'go': // For Go, use the last directory as package return parts[parts.length - 2] || 'main'; case 'typescript': case 'javascript': // For TS/JS, look for common structure patterns if (parts.includes('src')) { const srcIndex = parts.indexOf('src'); return parts[srcIndex + 1] || 'src'; } return parts[parts.length - 2] || 'root'; case 'python': // For Python, use directory structure return parts[parts.length - 2] || 'main'; default: return 'unknown'; } } /** * Build adjacency list from edges */ buildAdjacencyList(edges) { const adjList = new Map(); for (const edge of edges) { if (!adjList.has(edge.from)) { adjList.set(edge.from, []); } adjList.get(edge.from).push(edge.to); } return adjList; } /** * Build adjacency list from references */ buildAdjacencyListFromReferences(references) { const adjList = new Map(); for (const ref of references) { if (!adjList.has(ref.sourceId)) { adjList.set(ref.sourceId, []); } adjList.get(ref.sourceId).push(ref.targetId); } return adjList; } /** * Calculate maximum depth from a node */ calculateMaxDepth(nodeId, adjList) { const visited = new Set(); const dfs = (currentId) => { if (visited.has(currentId)) return 0; // Avoid cycles visited.add(currentId); const neighbors = adjList.get(currentId) || []; if (neighbors.length === 0) return 1; const maxChildDepth = Math.max(...neighbors.map(neighbor => dfs(neighbor))); visited.delete(currentId); return 1 + maxChildDepth; }; return dfs(nodeId); } /** * Count strongly connected components (simplified) */ countStronglyConnectedComponents(nodes, edges) { // Simplified implementation - would use Tarjan's algorithm in practice return Math.ceil(nodes.length / 10); // Rough estimate } /** * Find tightly coupled clusters */ findTightlyCoupledClusters(graph) { const clusters = new Map(); // Group nodes by cluster for (const node of graph.nodes) { const cluster = node.cluster || 'default'; if (!clusters.has(cluster)) { clusters.set(cluster, []); } clusters.get(cluster).push(node.id); } const tightlyCoupled = []; for (const [clusterName, nodeIds] of clusters) { const internalEdges = graph.edges.filter(edge => nodeIds.includes(edge.from) && nodeIds.includes(edge.to)); const coupling = internalEdges.length / (nodeIds.length * (nodeIds.length - 1)); if (coupling > 0.7) { // High coupling threshold tightlyCoupled.push({ nodes: nodeIds, coupling }); } } return tightlyCoupled; } /** * Find hub nodes with too many dependencies */ findHubNodes(graph) { const dependencyCounts = new Map(); for (const edge of graph.edges) { dependencyCounts.set(edge.from, (dependencyCounts.get(edge.from) || 0) + 1); } const threshold = Math.max(5, graph.nodes.length * 0.1); // 10% of nodes or minimum 5 return graph.nodes.filter(node => (dependencyCounts.get(node.id) || 0) > threshold); } /** * Find orphaned nodes with no dependencies */ findOrphanedNodes(graph) { const connectedNodes = new Set(); for (const edge of graph.edges) { connectedNodes.add(edge.from); connectedNodes.add(edge.to); } return graph.nodes.filter(node => !connectedNodes.has(node.id)); } /** * Calculate overall health score */ calculateHealthScore(graph, issues) { let score = 100; for (const issue of issues) { switch (issue.severity) { case 'critical': score -= 20; break; case 'warning': score -= 10; break; case 'suggestion': score -= 5; break; } } // Additional penalties if (graph.cycles.length > 0) { score -= graph.cycles.length * 5; } if (graph.metrics.maxDepth > 15) { score -= 10; } return Math.max(0, score); } /** * Generate suggestion for breaking cycles */ generateCycleSuggestion(cycleNodes) { if (cycleNodes.length === 2) { return 'Consider using dependency injection or extracting a common interface'; } else if (cycleNodes.length <= 5) { return 'Consider introducing a mediator pattern or event-driven architecture'; } else { return 'This is a complex cycle - consider major refactoring to break it down into smaller modules'; } } /** * Check if a file is a test file */ isTestFile(filePath) { return filePath.includes('test') || filePath.includes('spec') || filePath.includes('__tests__') || filePath.endsWith('.test.ts') || filePath.endsWith('.test.js') || filePath.endsWith('.spec.ts') || filePath.endsWith('.spec.js') || filePath.endsWith('_test.go'); } } //# sourceMappingURL=DependencyGraphBuilder.js.map