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

1,055 lines (914 loc) 33.4 kB
/** * PatternDetector.js * * Analyzes code to detect naming conventions, architectural patterns, * and common coding patterns used throughout the project. */ import fs from 'node:fs/promises' import path from 'node:path' import chalk from 'chalk' // Helper functions for parsing specific file types import { analyzeJavaScript } from '../analyzers/javascript.js' import { analyzePython } from '../analyzers/python.js' import { analyzeSwift } from '../analyzers/swift.js' import { analyzeTypeScript } from '../analyzers/typescript.js' import { DependencyAnalyzer } from './DependencyAnalyzer.js' export class PatternDetector { constructor(options = {}) { this.verbose = options.verbose this.sampleSize = options.sampleSize || 50 // Max files to analyze per type // Initialize pattern storage this.namingConventions = { variables: {}, functions: {}, classes: {}, components: {}, files: {}, directories: {}, } this.architecturalPatterns = [] this.codePatterns = [] this.consistencyMetrics = {} // Initialize dependency analyzer this.dependencyAnalyzer = new DependencyAnalyzer({ verbose: this.verbose, maxFilesToParse: options.maxFilesToParse || 200, }) } /** * Detects patterns from project structure data * @param {Object} projectStructure - Project structure from ProjectScanner * @param {Object} techData - Technology data from TechnologyAnalyzer (optional) * @returns {Object} Detected patterns */ async detectPatterns(projectStructure, techData = {}) { if (this.verbose) { console.log(chalk.gray('Starting pattern detection...')) } // Reset pattern storage for a clean analysis this.resetPatternStorage() try { // Analyze naming conventions for files and directories await this.analyzeFileAndDirectoryNaming(projectStructure) // Detect architectural patterns based on directory structure and dependencies await this.detectArchitecturalPatterns(projectStructure, techData) // Analyze code samples for naming conventions and patterns await this.analyzeCodeSamples(projectStructure) // Calculate consistency metrics this.calculateConsistencyMetrics() // Return combined pattern detection results return { namingConventions: this.namingConventions, architecturalPatterns: this.architecturalPatterns, codePatterns: this.codePatterns, consistencyMetrics: this.consistencyMetrics, dependencyInsights: { moduleCount: this.dependencyAnalyzer.dependencyGraph?.size || 0, edgeCount: this.dependencyAnalyzer.countEdges ? this.dependencyAnalyzer.countEdges() : 0, }, } } catch (error) { if (this.verbose) { console.error(chalk.red(`Error in pattern detection: ${error.message}`)) console.error(chalk.gray(error.stack)) } throw new Error(`Pattern detection failed: ${error.message}`) } } /** * Resets pattern storage for a clean analysis */ resetPatternStorage() { // Reset naming conventions this.namingConventions = { variables: { patterns: {}, total: 0, dominant: null }, functions: { patterns: {}, total: 0, dominant: null }, classes: { patterns: {}, total: 0, dominant: null }, components: { patterns: {}, total: 0, dominant: null }, files: { patterns: {}, total: 0, dominant: null }, directories: { patterns: {}, total: 0, dominant: null }, } // Reset other patterns this.architecturalPatterns = [] this.codePatterns = [] this.consistencyMetrics = {} } /** * Analyzes naming conventions for files and directories * @param {Object} projectStructure - Project structure data */ async analyzeFileAndDirectoryNaming(projectStructure) { if (this.verbose) { console.log(chalk.gray('Analyzing file and directory naming conventions...')) } // Analyze file naming for (const file of projectStructure.files) { const name = path.basename(file.name, path.extname(file.name)) this.analyzeNamingConvention(name, 'files') } // Analyze directory naming for (const dir of projectStructure.directories) { this.analyzeNamingConvention(dir.name, 'directories') } // Determine dominant conventions this.determineDominantConvention('files') this.determineDominantConvention('directories') if (this.verbose) { if (this.namingConventions.files.dominant) { console.log(chalk.gray(`File naming convention: ${this.namingConventions.files.dominant}`)) } if (this.namingConventions.directories.dominant) { console.log( chalk.gray(`Directory naming convention: ${this.namingConventions.directories.dominant}`) ) } } } /** * Determines the naming convention used for a given name * @param {string} name - The name to analyze * @param {string} category - Category for storing results (files, directories, etc.) */ analyzeNamingConvention(name, category) { // Skip empty names if (!name) { return } // Check for various naming conventions let convention = 'unknown' // camelCase: first character is lowercase, has mixed case if (/^[a-z][a-zA-Z0-9]*$/.test(name) && /[A-Z]/.test(name)) { convention = 'camelCase' } // PascalCase: first character is uppercase, has mixed case else if (/^[A-Z][a-zA-Z0-9]*$/.test(name)) { convention = 'PascalCase' } // snake_case: contains underscores, all lowercase else if (/^[a-z0-9_]+$/.test(name) && name.includes('_')) { convention = 'snake_case' } // kebab-case: contains hyphens, all lowercase else if (/^[a-z0-9-]+$/.test(name) && name.includes('-')) { convention = 'kebab-case' } // lowercase: all lowercase, no separators else if (/^[a-z0-9]+$/.test(name)) { convention = 'lowercase' } // UPPERCASE: all uppercase, may contain underscores else if (/^[A-Z0-9_]+$/.test(name)) { convention = 'UPPERCASE' } // Update naming convention statistics const stats = this.namingConventions[category] stats.patterns[convention] = (stats.patterns[convention] || 0) + 1 stats.total++ } /** * Determines the dominant convention for a naming category * @param {string} category - Category to analyze (files, directories, etc.) */ determineDominantConvention(category) { const stats = this.namingConventions[category] if (stats.total === 0) { return } let dominant = 'unknown' let highestCount = 0 for (const [convention, count] of Object.entries(stats.patterns)) { if (count > highestCount) { highestCount = count dominant = convention } } // Calculate percentage of the dominant convention const percentage = Math.round((highestCount / stats.total) * 100) // If the dominant convention is less than 60% of the total, // consider it a mixed convention if (percentage < 60) { dominant = 'mixed' } stats.dominant = dominant } /** * Detects architectural patterns based on directory structure and code dependencies * @param {Object} projectStructure - Project structure data * @param {Object} techData - Technology data from TechnologyAnalyzer (optional) */ async detectArchitecturalPatterns(projectStructure, techData = {}) { if (this.verbose) { console.log(chalk.gray('Detecting architectural patterns...')) } // Basic pattern detection based on directory structure this.detectDirectoryBasedPatterns(projectStructure) // Enhanced pattern detection with dependency analysis await this.detectDependencyBasedPatterns(projectStructure, techData) // Merge and reconcile pattern detections this.reconcilePatternDetections() // Sort patterns by confidence score this.architecturalPatterns.sort((a, b) => b.confidence - a.confidence) if (this.verbose && this.architecturalPatterns.length > 0) { console.log( chalk.gray( `Detected architectural pattern: ${this.architecturalPatterns[0].name} (${this.architecturalPatterns[0].confidence}% confidence)` ) ) } } /** * Detects architectural patterns based on directory structure * @param {Object} projectStructure - Project structure data */ detectDirectoryBasedPatterns(projectStructure) { // MVC Pattern Detection const mvcScore = this.detectMVCPattern(projectStructure) if (mvcScore > 60) { this.architecturalPatterns.push({ name: 'MVC', confidence: mvcScore, description: 'Model-View-Controller pattern separating data, UI, and application logic.', source: 'directory-structure', }) } // MVVM Pattern Detection const mvvmScore = this.detectMVVMPattern(projectStructure) if (mvvmScore > 60) { this.architecturalPatterns.push({ name: 'MVVM', confidence: mvvmScore, description: 'Model-View-ViewModel pattern with data binding between View and ViewModel.', source: 'directory-structure', }) } // Clean Architecture / Layered Pattern Detection const layeredScore = this.detectLayeredPattern(projectStructure) if (layeredScore > 60) { this.architecturalPatterns.push({ name: 'Layered Architecture', confidence: layeredScore, description: 'Layered architecture with clear separation of concerns between layers.', source: 'directory-structure', }) } // Microservices Pattern Detection const microservicesScore = this.detectMicroservicesPattern(projectStructure) if (microservicesScore > 60) { this.architecturalPatterns.push({ name: 'Microservices', confidence: microservicesScore, description: 'Microservices architecture with multiple independent services.', source: 'directory-structure', }) } // Feature-based Pattern Detection const featureBasedScore = this.detectFeatureBasedPattern(projectStructure) if (featureBasedScore > 60) { this.architecturalPatterns.push({ name: 'Feature-based', confidence: featureBasedScore, description: 'Feature-based organization with code grouped by feature rather than technical layer.', source: 'directory-structure', }) } } /** * Detects MVC pattern indicators * @param {Object} projectStructure - Project structure data * @returns {number} Confidence score (0-100) */ detectMVCPattern(projectStructure) { const dirNames = projectStructure.directories.map((d) => d.name.toLowerCase()) let score = 0 // Look for model/view/controller directories if (dirNames.includes('models') || dirNames.includes('model')) { score += 30 } if (dirNames.includes('views') || dirNames.includes('view')) { score += 30 } if (dirNames.includes('controllers') || dirNames.includes('controller')) { score += 30 } // Look for files with these names const fileBasenames = projectStructure.files.map((f) => path.basename(f.name, path.extname(f.name)).toLowerCase() ) const modelFiles = fileBasenames.filter( (name) => name.endsWith('model') || name.endsWith('models') ) const viewFiles = fileBasenames.filter( (name) => name.endsWith('view') || name.endsWith('views') ) const controllerFiles = fileBasenames.filter( (name) => name.endsWith('controller') || name.endsWith('controllers') ) if (modelFiles.length > 0) { score += 15 } if (viewFiles.length > 0) { score += 15 } if (controllerFiles.length > 0) { score += 15 } // Cap at 100% return Math.min(score, 100) } /** * Detects MVVM pattern indicators * @param {Object} projectStructure - Project structure data * @returns {number} Confidence score (0-100) */ detectMVVMPattern(projectStructure) { const dirNames = projectStructure.directories.map((d) => d.name.toLowerCase()) let score = 0 // Look for model/view/viewmodel directories if (dirNames.includes('models') || dirNames.includes('model')) { score += 25 } if (dirNames.includes('views') || dirNames.includes('view')) { score += 25 } if (dirNames.includes('viewmodels') || dirNames.includes('viewmodel')) { score += 40 } // Look for files with these names const fileBasenames = projectStructure.files.map((f) => path.basename(f.name, path.extname(f.name)).toLowerCase() ) const modelFiles = fileBasenames.filter( (name) => name.endsWith('model') || name.endsWith('models') ) const viewFiles = fileBasenames.filter( (name) => name.endsWith('view') || name.endsWith('views') ) const viewModelFiles = fileBasenames.filter( (name) => name.endsWith('viewmodel') || name.endsWith('viewmodels') || name.includes('_vm') || name.includes('-vm') ) if (modelFiles.length > 0) { score += 10 } if (viewFiles.length > 0) { score += 10 } if (viewModelFiles.length > 0) { score += 20 } // Cap at 100% return Math.min(score, 100) } /** * Detects layered architecture pattern indicators * @param {Object} projectStructure - Project structure data * @returns {number} Confidence score (0-100) */ detectLayeredPattern(projectStructure) { const dirNames = projectStructure.directories.map((d) => d.name.toLowerCase()) let score = 0 // Check for various layer names const layerNames = [ 'data', 'domain', 'presentation', 'infrastructure', 'application', 'api', 'core', 'services', 'repositories', 'interfaces', 'adapters', 'persistence', 'entities', ] for (const layer of layerNames) { if (dirNames.includes(layer)) { score += 15 } } // Check for common patterns in filenames const fileBasenames = projectStructure.files.map((f) => path.basename(f.name, path.extname(f.name)).toLowerCase() ) const repositoryFiles = fileBasenames.filter( (name) => name.endsWith('repository') || name.includes('repo') ) const serviceFiles = fileBasenames.filter((name) => name.endsWith('service')) const entityFiles = fileBasenames.filter((name) => name.endsWith('entity')) const dtoFiles = fileBasenames.filter((name) => name.endsWith('dto')) if (repositoryFiles.length > 0) { score += 10 } if (serviceFiles.length > 0) { score += 10 } if (entityFiles.length > 0) { score += 10 } if (dtoFiles.length > 0) { score += 10 } // Cap at 100% return Math.min(score, 100) } /** * Detects microservices architecture pattern indicators * @param {Object} projectStructure - Project structure data * @returns {number} Confidence score (0-100) */ detectMicroservicesPattern(projectStructure) { const dirNames = projectStructure.directories.map((d) => d.name.toLowerCase()) let score = 0 // Check for services, apis, or microservices directory if ( dirNames.includes('services') || dirNames.includes('apis') || dirNames.includes('microservices') ) { score += 40 } // Check for multiple API/service directories const servicesDirs = projectStructure.directories.filter( (d) => d.name.toLowerCase().includes('service') || d.name.toLowerCase().includes('api') ) if (servicesDirs.length >= 3) { score += 30 // Multiple services indicates microservices architecture } // Check for Docker/Kubernetes configuration const dockerFiles = projectStructure.files.filter( (f) => f.name.toLowerCase().includes('dockerfile') || f.name.toLowerCase().includes('docker-compose') ) const k8sFiles = projectStructure.files.filter( (f) => f.name.toLowerCase().includes('kubernetes') || (f.extension === 'yaml' && f.name.toLowerCase().includes('deployment')) ) if (dockerFiles.length > 0) { score += 15 } if (k8sFiles.length > 0) { score += 15 } // Cap at 100% return Math.min(score, 100) } /** * Detects feature-based architecture pattern indicators * @param {Object} projectStructure - Project structure data * @returns {number} Confidence score (0-100) */ detectFeatureBasedPattern(projectStructure) { const dirNames = projectStructure.directories.map((d) => d.name.toLowerCase()) let score = 0 // Check for features or modules directory if (dirNames.includes('features') || dirNames.includes('modules')) { score += 50 } // Check if feature directories contain multiple technical aspects // (e.g., a feature directory contains model, view, and controller files) const featureDirs = projectStructure.directories.filter((d) => { const path = d.path.toLowerCase() return path.includes('/features/') || path.includes('/modules/') }) if (featureDirs.length >= 2) { score += 30 // Multiple feature directories is a strong indicator } // Check for feature-specific files const featureSpecificFiles = projectStructure.files.filter((f) => { const path = f.path.toLowerCase() return path.includes('/features/') || path.includes('/modules/') }) if (featureSpecificFiles.length > 10) { score += 20 // Significant number of files in feature directories } // Cap at 100% return Math.min(score, 100) } /** * Detects architectural patterns using dependency graph analysis * @param {Object} projectStructure - Project structure data * @param {Object} techData - Technology data from TechnologyAnalyzer (optional) */ async detectDependencyBasedPatterns(projectStructure, techData = {}) { if (this.verbose) { console.log( chalk.gray( 'Performing advanced architectural pattern detection with dependency analysis...' ) ) } try { // Use the DependencyAnalyzer to build and analyze the dependency graph const dependencyAnalysis = await this.dependencyAnalyzer.analyzeDependencies( projectStructure, techData ) // If we couldn't build a dependency graph, return early if (!dependencyAnalysis || dependencyAnalysis.moduleCount === 0) { if (this.verbose) { console.log( chalk.yellow( 'No dependencies found for analysis. Using directory-based detection only.' ) ) } return } if (this.verbose) { console.log( chalk.gray( `Dependency graph built with ${dependencyAnalysis.moduleCount} modules and ${dependencyAnalysis.edgeCount} edges` ) ) } // Add architectural patterns from dependency analysis for (const hint of dependencyAnalysis.architecturalHints) { this.architecturalPatterns.push({ name: hint.pattern, confidence: hint.confidence, description: hint.evidence, source: 'dependency-analysis', }) } // Add layered architecture pattern if layers were detected if (dependencyAnalysis.layeredStructure && dependencyAnalysis.layeredStructure.length > 1) { const layerCount = dependencyAnalysis.layeredStructure.length this.architecturalPatterns.push({ name: 'Layered Architecture', confidence: Math.min(60 + layerCount * 10, 90), // More layers increase confidence description: `Detected ${layerCount} distinct layers in code dependencies with clear separation.`, source: 'dependency-analysis', details: { layers: dependencyAnalysis.layeredStructure.map((l) => ({ name: l.name, moduleCount: l.modules.length, })), }, }) } // Add information about circular dependencies if detected if (dependencyAnalysis.cyclesDetected) { this.codePatterns.push('circular-dependencies') } } catch (error) { if (this.verbose) { console.error(chalk.yellow(`Error in dependency-based pattern detection: ${error.message}`)) } } } /** * Reconciles patterns detected by different methods and merges duplicates */ reconcilePatternDetections() { if (this.architecturalPatterns.length <= 1) { return } // Group patterns by name const patternsByName = {} for (const pattern of this.architecturalPatterns) { if (!patternsByName[pattern.name]) { patternsByName[pattern.name] = [] } patternsByName[pattern.name].push(pattern) } // Merge duplicate patterns this.architecturalPatterns = Object.entries(patternsByName).map(([name, patterns]) => { if (patterns.length === 1) { return patterns[0] } // Merge duplicate patterns const mergedPattern = { name, // Take highest confidence level and boost it slightly for multiple detections confidence: Math.min( Math.max(...patterns.map((p) => p.confidence)) + (patterns.length > 1 ? 10 : 0), 100 ), description: patterns.map((p) => p.description).join(' '), source: patterns.map((p) => p.source).join('+'), detectionCount: patterns.length, } // Merge any additional details if (patterns.some((p) => p.details)) { mergedPattern.details = {} for (const pattern of patterns) { if (pattern.details) { Object.assign(mergedPattern.details, pattern.details) } } } return mergedPattern }) } /** * Analyzes code samples for naming conventions and patterns * @param {Object} projectStructure - Project structure data */ async analyzeCodeSamples(projectStructure) { if (this.verbose) { console.log(chalk.gray('Analyzing code samples for patterns...')) } // Group files by type for analysis const filesByType = {} // Instead of using projectStructure.fileTypes which is an object with counts, // we'll create filesByType from the actual files array for (const file of projectStructure.files) { const type = file.type || 'unknown' if (!filesByType[type]) { filesByType[type] = [] } filesByType[type].push(file) } // Set up language-specific analyzers with correct mapping const analyzers = { // JavaScript family javascript: analyzeJavaScript, 'javascript-react': analyzeJavaScript, jsx: analyzeJavaScript, js: analyzeJavaScript, // TypeScript family - use TypeScript analyzer typescript: analyzeTypeScript, 'typescript-react': analyzeTypeScript, tsx: analyzeTypeScript, ts: analyzeTypeScript, // Other languages python: analyzePython, py: analyzePython, swift: analyzeSwift, } // Track files analyzed let totalFilesAnalyzed = 0 let skippedFiles = 0 // These are the file types we know how to analyze const knownCodeTypes = [ 'javascript', 'javascript-react', 'jsx', 'js', 'typescript', 'typescript-react', 'tsx', 'ts', 'python', 'py', 'swift', ] // Analyze samples of each supported language for (const [type, files] of Object.entries(filesByType)) { // Special handling for TypeScript files - always process with TypeScript analyzer const tsExtensions = ['.ts', '.tsx'] const isTypeScript = files.some((file) => tsExtensions.some((ext) => file.path.endsWith(ext))) if (isTypeScript) { // For TypeScript files, always use TypeScript analyzer const analyzer = analyzeTypeScript await this.analyzeFilesWithAnalyzer(files, analyzer, totalFilesAnalyzed, skippedFiles) continue } // Skip non-source code files and unknown types const isKnownType = knownCodeTypes.some((knownType) => type.includes(knownType)) if (!isKnownType) { skippedFiles += files.length continue } // Get the appropriate analyzer for this file type const analyzer = this.getAnalyzerForType(type, analyzers) if (!analyzer) { if (this.verbose) { console.warn(chalk.yellow(`No analyzer found for file type: ${type}`)) } skippedFiles += files.length continue } // Sample files for analysis (to avoid analyzing too many files) const sampleSize = Math.min(this.sampleSize, files.length) const sampleFiles = files.slice(0, sampleSize) if (this.verbose) { console.log(chalk.gray(`Analyzing ${sampleFiles.length} ${type} files...`)) } // Analyze each file in the sample for (const file of sampleFiles) { try { const content = await fs.readFile(file.path, 'utf8') // Determine the analyzer based on file extension first, then fallback to type const fileExt = path.extname(file.path).toLowerCase().substring(1) let fileAnalyzer = null // Map file extensions directly to analyzers const extensionMap = { ts: analyzeTypeScript, tsx: analyzeTypeScript, js: analyzeJavaScript, jsx: analyzeJavaScript, json: null, // Don't try to analyze JSON with JS parser py: analyzePython, swift: analyzeSwift, } // Use extension-based analyzer if available, otherwise fallback to type-based if (extensionMap[fileExt] !== undefined) { fileAnalyzer = extensionMap[fileExt] } else { fileAnalyzer = analyzer } // Skip analysis if no analyzer is available (e.g., for JSON files) if (!fileAnalyzer) { if (this.verbose) { console.log(chalk.gray(`Skipping analysis for ${fileExt} file: ${file.path}`)) } continue } const analysis = await fileAnalyzer(content, file.path) totalFilesAnalyzed++ // Update naming conventions with analysis results this.updateNamingConventions('variables', analysis.variables) this.updateNamingConventions('functions', analysis.functions) this.updateNamingConventions('classes', analysis.classes) // If analyzing React components, update component naming if (type.includes('react') || file.path.includes('.jsx') || file.path.includes('.tsx')) { this.updateNamingConventions('components', analysis.components || []) } // Track code patterns if (analysis.patterns && analysis.patterns.length > 0) { for (const pattern of analysis.patterns) { if (!this.codePatterns.includes(pattern)) { this.codePatterns.push(pattern) } } } } catch (error) { if (this.verbose) { console.warn(chalk.yellow(`Error analyzing file ${file.path}: ${error.message}`)) } } } } if (this.verbose) { console.log( chalk.gray(`Analyzed ${totalFilesAnalyzed} files, skipped ${skippedFiles} non-code files`) ) } // Determine dominant conventions this.determineDominantConvention('variables') this.determineDominantConvention('functions') this.determineDominantConvention('classes') this.determineDominantConvention('components') if (this.verbose) { if (this.namingConventions.variables.dominant) { console.log( chalk.gray(`Variable naming convention: ${this.namingConventions.variables.dominant}`) ) } if (this.namingConventions.functions.dominant) { console.log( chalk.gray(`Function naming convention: ${this.namingConventions.functions.dominant}`) ) } if (this.namingConventions.classes.dominant) { console.log( chalk.gray(`Class naming convention: ${this.namingConventions.classes.dominant}`) ) } } } /** * Updates naming convention statistics for a given category * @param {string} category - The category (variables, functions, classes, etc.) * @param {Array} names - Array of names to analyze */ updateNamingConventions(category, names) { if (!names || names.length === 0) { return } for (const name of names) { this.analyzeNamingConvention(name, category) } } /** * Gets the appropriate analyzer function for the given file type */ getAnalyzerForType(fileType, analyzers) { const type = fileType.toLowerCase() // Direct match if (analyzers[type]) { return analyzers[type] } // Specific partial matches to avoid false positives like 'json' matching 'js' const partialMatches = [ { pattern: 'javascript', key: 'javascript' }, { pattern: 'typescript', key: 'typescript' }, { pattern: 'python', key: 'python' }, { pattern: 'swift', key: 'swift' }, ] for (const match of partialMatches) { if (type.includes(match.pattern) && analyzers[match.key]) { return analyzers[match.key] } } // Legacy partial match for backward compatibility (more conservative) for (const [key, analyzer] of Object.entries(analyzers)) { if (key.length > 2 && type.includes(key)) { return analyzer } } return null } /** * Analyzes a set of files with the specified analyzer * @param {Array} files - Files to analyze * @param {Function} analyzer - Analyzer function to use * @param {number} totalFilesAnalyzed - Reference to track total files analyzed * @param {number} skippedFiles - Reference to track skipped files */ async analyzeFilesWithAnalyzer(files, analyzer, totalFilesAnalyzed, skippedFiles) { // Sample files for analysis (to avoid analyzing too many files) const sampleSize = Math.min(this.sampleSize, files.length) const sampleFiles = files.slice(0, sampleSize) if (this.verbose) { console.log(chalk.gray(`Analyzing ${sampleFiles.length} files with specialized analyzer...`)) } // Analyze each file in the sample for (const file of sampleFiles) { try { const content = await fs.readFile(file.path, 'utf8') // Skip analysis if no analyzer is available if (!analyzer) { if (this.verbose) { console.log(chalk.gray(`No analyzer available for file: ${file.path}`)) } skippedFiles++ continue } const analysis = await analyzer(content, file.path) totalFilesAnalyzed++ // Update naming conventions with analysis results this.updateNamingConventions('variables', analysis.variables) this.updateNamingConventions('functions', analysis.functions) this.updateNamingConventions('classes', analysis.classes) // If analyzing React components, update component naming if (file.path.includes('.jsx') || file.path.includes('.tsx')) { this.updateNamingConventions('components', analysis.components || []) } // Track code patterns if (analysis.patterns && analysis.patterns.length > 0) { for (const pattern of analysis.patterns) { if (!this.codePatterns.includes(pattern)) { this.codePatterns.push(pattern) } } } } catch (error) { if (this.verbose) { console.warn(chalk.yellow(`Error analyzing file ${file.path}: ${error.message}`)) } skippedFiles++ } } } /** * Calculates consistency metrics for the analyzed code */ calculateConsistencyMetrics() { const metrics = { overallConsistency: 0, namingConsistency: 0, architecturalConsistency: 0, patternConsistency: 0, } // Calculate naming consistency const namingScores = [] for (const [_category, data] of Object.entries(this.namingConventions)) { if ( data.dominant && data.dominant !== 'mixed' && data.patterns && data.patterns[data.dominant] ) { namingScores.push(data.patterns[data.dominant] / 100) } } metrics.namingConsistency = namingScores.length > 0 ? Math.round( (namingScores.reduce((sum, score) => sum + score, 0) / namingScores.length) * 100 ) : 0 // Calculate architectural consistency metrics.architecturalConsistency = this.architecturalPatterns.length > 0 ? this.architecturalPatterns[0].confidence : 0 // Calculate overall consistency const scores = [metrics.namingConsistency, metrics.architecturalConsistency].filter( (score) => score > 0 ) metrics.overallConsistency = scores.length > 0 ? Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length) : 0 this.consistencyMetrics = metrics } }