@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,010 lines (870 loc) • 33.3 kB
JavaScript
/**
* PatternDetector.js
*
* Analyzes code to detect naming conventions, architectural patterns,
* and common coding patterns used throughout the project.
*/
import chalk from 'chalk';
import fs from 'fs/promises';
import path from 'path';
// 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 || false;
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;
}
}