// Copyright 2025 Chris Bunting
// Brief: Main dependency analysis service that orchestrates all other services
// Scope: Provides comprehensive dependency analysis functionality
import { DependencyInfo, DependencyOptions, PackageManager, SeverityLevel, DependencyType } from '@mcp-code-analysis/shared-types';
import { PackageManagerDetector } from './PackageManagerDetector.js';
import { DependencyGraphBuilder, DependencyGraph } from './DependencyGraphBuilder.js';
import { VersionResolver, UpdateInfo, VersionConflict } from './VersionResolver.js';
import { SecurityAuditor, SecurityAudit } from './SecurityAuditor.js';
import { Logger } from '../utils/Logger.js';
export interface AnalysisResult {
projectPath: string;
packageManager: PackageManager;
dependencies: DependencyInfo[];
graph: DependencyGraph;
metrics: {
totalDependencies: number;
directDependencies: number;
transitiveDependencies: number;
maxDepth: number;
averageDepth: number;
packageCount: number;
};
conflicts: VersionConflict[];
circularDependencies: string[][];
securityAudits: SecurityAudit[];
outdatedPackages: UpdateInfo[];
recommendations: string[];
analysisTime: number;
}
export interface AlternativePackage {
name: string;
description: string;
packageManager: PackageManager;
downloads: number;
stars: number;
lastUpdated: string;
score: number;
features: string[];
}
export class DependencyAnalyzer {
private packageManagerDetector: PackageManagerDetector;
private dependencyGraphBuilder: DependencyGraphBuilder;
private versionResolver: VersionResolver;
private securityAuditor: SecurityAuditor;
private logger: Logger;
constructor(
packageManagerDetector: PackageManagerDetector,
dependencyGraphBuilder: DependencyGraphBuilder,
versionResolver: VersionResolver,
securityAuditor: SecurityAuditor,
logger: Logger
) {
this.packageManagerDetector = packageManagerDetector;
this.dependencyGraphBuilder = dependencyGraphBuilder;
this.versionResolver = versionResolver;
this.securityAuditor = securityAuditor;
this.logger = logger;
}
async analyzeProject(projectPath: string, options: DependencyOptions = {}): Promise<AnalysisResult> {
const startTime = Date.now();
this.logger.info(`Starting dependency analysis for: ${projectPath}`);
try {
// Detect package manager
const packageManager = await this.packageManagerDetector.getPrimaryPackageManager(projectPath);
if (!packageManager) {
throw new Error('No package manager detected in the project');
}
this.logger.debug(`Detected package manager: ${packageManager}`);
// Parse dependencies based on package manager
const dependencies = await this.parseDependencies(projectPath, packageManager, options);
this.logger.debug(`Found ${dependencies.length} dependencies`);
// Get project info (name and version)
const projectInfo = await this.getProjectInfo(projectPath, packageManager);
// Build dependency graph
const graph = await this.dependencyGraphBuilder.buildGraph(
projectInfo.name,
projectInfo.version,
dependencies,
packageManager
);
// Calculate metrics
const metrics = this.dependencyGraphBuilder.calculateDependencyMetrics(graph);
// Find version conflicts
const conflicts = await this.versionResolver.findVersionConflicts(
this.extractDependencyInfo(dependencies)
);
// Find circular dependencies
const circularDependencies = this.dependencyGraphBuilder.findCircularDependencies(graph);
// Perform security audits
const securityAudits = await this.performSecurityAudits(dependencies, packageManager);
// Check for outdated packages
const outdatedPackages = await this.checkForOutdatedPackages(dependencies, packageManager);
// Generate recommendations
const recommendations = await this.generateRecommendations({
dependencies,
metrics,
conflicts,
circularDependencies,
securityAudits,
outdatedPackages,
packageManager,
});
const analysisTime = Date.now() - startTime;
const result: AnalysisResult = {
projectPath,
packageManager,
dependencies,
graph,
metrics,
conflicts,
circularDependencies,
securityAudits,
outdatedPackages,
recommendations,
analysisTime,
};
this.logger.info(`Dependency analysis completed in ${analysisTime}ms`);
return result;
} catch (error) {
this.logger.error('Error analyzing project:', error);
throw error;
}
}
private async parseDependencies(
projectPath: string,
packageManager: PackageManager,
options: DependencyOptions
): Promise<DependencyInfo[]> {
switch (packageManager) {
case PackageManager.NPM:
case PackageManager.YARN:
return this.parseNpmDependencies(projectPath, options);
case PackageManager.PIP:
return this.parsePipDependencies(projectPath, options);
case PackageManager.CARGO:
return this.parseCargoDependencies(projectPath, options);
case PackageManager.MAVEN:
return this.parseMavenDependencies(projectPath, options);
case PackageManager.GRADLE:
return this.parseGradleDependencies(projectPath, options);
case PackageManager.GO:
return this.parseGoDependencies(projectPath, options);
default:
throw new Error(`Unsupported package manager: ${packageManager}`);
}
}
private async parseNpmDependencies(projectPath: string, options: DependencyOptions): Promise<DependencyInfo[]> {
const fs = await import('fs');
const path = await import('path');
const packageJsonPath = path.join(projectPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
throw new Error('package.json not found');
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const dependencies: DependencyInfo[] = [];
const processDeps = (deps: Record<string, string>, type: DependencyType) => {
if (!deps) return;
for (const [name, version] of Object.entries(deps)) {
dependencies.push({
name,
version,
type,
manager: PackageManager.NPM,
licenses: [],
vulnerabilities: [],
dependencies: [],
});
}
};
if (options.includeDev !== false) {
processDeps(packageJson.devDependencies, DependencyType.DEVELOPMENT);
}
if (options.includePeer !== false) {
processDeps(packageJson.peerDependencies, DependencyType.PEER);
}
if (options.includeOptional !== false) {
processDeps(packageJson.optionalDependencies, DependencyType.OPTIONAL);
}
processDeps(packageJson.dependencies, DependencyType.PRODUCTION);
return dependencies;
}
private async parsePipDependencies(projectPath: string, _options: DependencyOptions): Promise<DependencyInfo[]> {
const fs = await import('fs');
const path = await import('path');
const dependencies: DependencyInfo[] = [];
// Try requirements.txt first
const requirementsPath = path.join(projectPath, 'requirements.txt');
if (fs.existsSync(requirementsPath)) {
const content = fs.readFileSync(requirementsPath, 'utf8');
const lines = content.split('\n').filter(line => line.trim() && !line.startsWith('#'));
for (const line of lines) {
const match = line.match(/^([a-zA-Z0-9\-_.]+)([><=!~]+.*)?$/);
if (match) {
const name = match[1];
const version = match[2] || '*';
dependencies.push({
name,
version,
type: DependencyType.PRODUCTION,
manager: PackageManager.PIP,
licenses: [],
vulnerabilities: [],
dependencies: [],
});
}
}
}
// Try setup.py
const setupPyPath = path.join(projectPath, 'setup.py');
if (fs.existsSync(setupPyPath) && dependencies.length === 0) {
// This is a simplified parser - in reality, you'd need to execute setup.py or parse it more carefully
dependencies.push({
name: 'python-package',
version: '*',
type: DependencyType.PRODUCTION,
manager: PackageManager.PIP,
licenses: [],
vulnerabilities: [],
dependencies: [],
});
}
return dependencies;
}
private async parseCargoDependencies(projectPath: string, options: DependencyOptions): Promise<DependencyInfo[]> {
const fs = await import('fs');
const path = await import('path');
const toml = await import('toml');
const cargoTomlPath = path.join(projectPath, 'Cargo.toml');
if (!fs.existsSync(cargoTomlPath)) {
throw new Error('Cargo.toml not found');
}
const cargoToml = toml.parse(fs.readFileSync(cargoTomlPath, 'utf8'));
const dependencies: DependencyInfo[] = [];
const processDeps = (deps: Record<string, any>, type: DependencyType) => {
if (!deps) return;
for (const [name, versionSpec] of Object.entries(deps)) {
const version = typeof versionSpec === 'string' ? versionSpec : versionSpec.version || '*';
dependencies.push({
name,
version,
type,
manager: PackageManager.CARGO,
licenses: [],
vulnerabilities: [],
dependencies: [],
});
}
};
processDeps(cargoToml.dependencies, DependencyType.PRODUCTION);
if (options.includeDev !== false) {
processDeps(cargoToml['dev-dependencies'], DependencyType.DEVELOPMENT);
}
return dependencies;
}
private async parseMavenDependencies(projectPath: string, _options: DependencyOptions): Promise<DependencyInfo[]> {
const fs = await import('fs');
const path = await import('path');
const xml2js = await import('xml2js');
const pomXmlPath = path.join(projectPath, 'pom.xml');
if (!fs.existsSync(pomXmlPath)) {
throw new Error('pom.xml not found');
}
const pomContent = fs.readFileSync(pomXmlPath, 'utf8');
const pom = await xml2js.parseStringPromise(pomContent);
const dependencies: DependencyInfo[] = [];
const deps = pom.project?.dependencies?.dependency || [];
for (const dep of deps) {
dependencies.push({
name: `${dep.groupId[0]}:${dep.artifactId[0]}`,
version: dep.version?.[0] || '*',
type: DependencyType.PRODUCTION,
manager: PackageManager.MAVEN,
licenses: [],
vulnerabilities: [],
dependencies: [],
});
}
return dependencies;
}
private async parseGradleDependencies(projectPath: string, _options: DependencyOptions): Promise<DependencyInfo[]> {
const fs = await import('fs');
const path = await import('path');
// This is a simplified implementation
// In reality, you'd need to parse Gradle build files which can be complex
const buildGradlePath = path.join(projectPath, 'build.gradle');
if (!fs.existsSync(buildGradlePath)) {
throw new Error('build.gradle not found');
}
const content = fs.readFileSync(buildGradlePath, 'utf8');
const dependencies: DependencyInfo[] = [];
// Simple regex-based parsing for demonstration
const depRegex = /(?:implementation|compile|api|testImplementation)\s+['"]([^'"]+)['"]/g;
let match;
while ((match = depRegex.exec(content)) !== null) {
const depSpec = match[1];
const parts = depSpec.split(':');
if (parts.length >= 2) {
const name = parts.length === 2 ? parts[0] : `${parts[0]}:${parts[1]}`;
const version = parts.length === 2 ? parts[1] : (parts[2] || '*');
dependencies.push({
name,
version,
type: DependencyType.PRODUCTION,
manager: PackageManager.GRADLE,
licenses: [],
vulnerabilities: [],
dependencies: [],
});
}
}
return dependencies;
}
private async parseGoDependencies(projectPath: string, _options: DependencyOptions): Promise<DependencyInfo[]> {
const fs = await import('fs');
const path = await import('path');
const goModPath = path.join(projectPath, 'go.mod');
if (!fs.existsSync(goModPath)) {
throw new Error('go.mod not found');
}
const content = fs.readFileSync(goModPath, 'utf8');
const dependencies: DependencyInfo[] = [];
const lines = content.split('\n');
let inRequireBlock = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === 'require (') {
inRequireBlock = true;
continue;
} else if (trimmed === ')' && inRequireBlock) {
inRequireBlock = false;
continue;
}
if (inRequireBlock || trimmed.startsWith('require ')) {
const match = trimmed.match(/(?:require\s+)?([^\s]+)\s+([^\s]+)/);
if (match) {
const name = match[1];
const version = match[2];
dependencies.push({
name,
version,
type: DependencyType.PRODUCTION,
manager: PackageManager.GO,
licenses: [],
vulnerabilities: [],
dependencies: [],
});
}
}
}
return dependencies;
}
private async getProjectInfo(projectPath: string, packageManager: PackageManager): Promise<{ name: string; version: string }> {
switch (packageManager) {
case PackageManager.NPM:
case PackageManager.YARN:
const fs = await import('fs');
const path = await import('path');
const packageJsonPath = path.join(projectPath, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return {
name: packageJson.name || 'unknown',
version: packageJson.version || '1.0.0',
};
default:
return {
name: 'unknown',
version: '1.0.0',
};
}
}
private extractDependencyInfo(dependencies: DependencyInfo[]): Array<{ name: string; version: string; constraint?: string }> {
return dependencies.map(dep => ({
name: dep.name,
version: dep.version,
constraint: dep.version, // For now, use version as constraint
}));
}
private async performSecurityAudits(dependencies: DependencyInfo[], packageManager: PackageManager): Promise<SecurityAudit[]> {
const audits: SecurityAudit[] = [];
for (const dep of dependencies) {
try {
const audit = await this.securityAuditor.auditPackage(dep.name, dep.version, packageManager);
audits.push(audit);
} catch (error) {
this.logger.warn(`Failed to audit ${dep.name}:`, error);
}
}
return audits;
}
private async checkForOutdatedPackages(dependencies: DependencyInfo[], packageManager: PackageManager): Promise<UpdateInfo[]> {
const outdatedPackages: UpdateInfo[] = [];
for (const dep of dependencies) {
try {
// Mock registry versions - in reality, you'd query the actual package registry
const mockRegistryVersions = await this.getMockRegistryVersions(dep.name, packageManager);
const updateInfo = await this.versionResolver.checkForUpdates(
dep.name,
dep.version,
mockRegistryVersions
);
if (updateInfo && updateInfo.semverDiff !== 'none') {
outdatedPackages.push(updateInfo);
}
} catch (error) {
this.logger.warn(`Failed to check updates for ${dep.name}:`, error);
}
}
return outdatedPackages;
}
private async getMockRegistryVersions(_packageName: string, _packageManager: PackageManager): Promise<string[]> {
// Mock registry versions for demonstration
// In reality, you'd query the actual package registry APIs
const baseVersions = ['1.0.0', '1.1.0', '1.2.0', '2.0.0', '2.1.0', '2.2.0'];
return baseVersions.map(v => v);
}
private async generateRecommendations(context: {
dependencies: DependencyInfo[];
metrics: any;
conflicts: VersionConflict[];
circularDependencies: string[][];
securityAudits: SecurityAudit[];
outdatedPackages: UpdateInfo[];
packageManager: PackageManager;
}): Promise<string[]> {
const recommendations: string[] = [];
// Security recommendations
const vulnerableAudits = context.securityAudits.filter(audit => audit.vulnerabilities.length > 0);
if (vulnerableAudits.length > 0) {
recommendations.push(
`🚨 ${vulnerableAudits.length} packages have security vulnerabilities. Update them immediately.`
);
}
// Outdated packages recommendations
if (context.outdatedPackages.length > 0) {
const criticalUpdates = context.outdatedPackages.filter(pkg => pkg.semverDiff === 'major');
const minorUpdates = context.outdatedPackages.filter(pkg => pkg.semverDiff === 'minor');
if (criticalUpdates.length > 0) {
recommendations.push(
`⚠️ ${criticalUpdates.length} packages have major updates available. Review breaking changes carefully.`
);
}
if (minorUpdates.length > 0) {
recommendations.push(
`📦 ${minorUpdates.length} packages have minor updates available. Consider updating them.`
);
}
}
// Version conflict recommendations
if (context.conflicts.length > 0) {
recommendations.push(
`🔀 ${context.conflicts.length} packages have version conflicts. Resolve them to ensure stability.`
);
}
// Circular dependency recommendations
if (context.circularDependencies.length > 0) {
recommendations.push(
`🔄 ${context.circularDependencies.length} circular dependencies detected. Refactor to improve build performance.`
);
}
// Dependency count recommendations
if (context.metrics.totalDependencies > 100) {
recommendations.push(
`📊 High dependency count (${context.metrics.totalDependencies}). Consider auditing and removing unused dependencies.`
);
}
// Tree depth recommendations
if (context.metrics.maxDepth > 8) {
recommendations.push(
`🌳 Deep dependency tree (${context.metrics.maxDepth} levels). Consider flattening dependencies where possible.`
);
}
// Package manager specific recommendations
switch (context.packageManager) {
case PackageManager.NPM:
case PackageManager.YARN:
recommendations.push('Run `npm audit` or `yarn audit` regularly to check for security issues.');
recommendations.push('Consider using `npm outdated` or `yarn outdated` to check for updates.');
break;
case PackageManager.PIP:
recommendations.push('Use `pip-audit` or `safety check` for security scanning.');
recommendations.push('Consider using `pip list --outdated` to check for updates.');
break;
case PackageManager.CARGO:
recommendations.push('Use `cargo audit` for security vulnerability scanning.');
recommendations.push('Use `cargo update` to update dependencies.');
break;
}
// General recommendations
recommendations.push('Implement dependency management in your CI/CD pipeline.');
recommendations.push('Regularly review and update dependencies to patch security vulnerabilities.');
recommendations.push('Consider using dependency locking for reproducible builds.');
return recommendations;
}
async checkUpdates(projectPath: string, packageName?: string): Promise<UpdateInfo[]> {
this.logger.info(`Checking for updates in: ${projectPath}${packageName ? ` for package: ${packageName}` : ''}`);
try {
const packageManager = await this.packageManagerDetector.getPrimaryPackageManager(projectPath);
if (!packageManager) {
throw new Error('No package manager detected');
}
const dependencies = await this.parseDependencies(projectPath, packageManager, {});
if (packageName) {
// Check updates for specific package
const dep = dependencies.find(d => d.name === packageName);
if (!dep) {
throw new Error(`Package ${packageName} not found in dependencies`);
}
const mockRegistryVersions = await this.getMockRegistryVersions(dep.name, packageManager);
const updateInfo = await this.versionResolver.checkForUpdates(
dep.name,
dep.version,
mockRegistryVersions
);
return updateInfo ? [updateInfo] : [];
} else {
// Check updates for all packages
const outdatedPackages = await this.checkForOutdatedPackages(dependencies, packageManager);
return outdatedPackages;
}
} catch (error) {
this.logger.error('Error checking for updates:', error);
throw error;
}
}
async findConflicts(projectPath: string): Promise<VersionConflict[]> {
this.logger.info(`Finding version conflicts in: ${projectPath}`);
try {
const packageManager = await this.packageManagerDetector.getPrimaryPackageManager(projectPath);
if (!packageManager) {
throw new Error('No package manager detected');
}
const dependencies = await this.parseDependencies(projectPath, packageManager, {});
const conflicts = await this.versionResolver.findVersionConflicts(
this.extractDependencyInfo(dependencies)
);
return conflicts;
} catch (error) {
this.logger.error('Error finding conflicts:', error);
throw error;
}
}
async suggestAlternatives(packageName: string, packageManager?: PackageManager): Promise<AlternativePackage[]> {
this.logger.info(`Suggesting alternatives for: ${packageName}`);
try {
// This is a mock implementation
// In reality, you'd query package registry APIs to find alternatives
const alternatives: AlternativePackage[] = [];
// Mock alternatives for common packages
const mockAlternatives: Record<string, AlternativePackage[]> = {
'lodash': [
{
name: 'underscore',
description: 'JavaScript utility library',
packageManager: PackageManager.NPM,
downloads: 5000000,
stars: 26000,
lastUpdated: '2024-01-15',
score: 85,
features: ['functional programming', 'utility functions'],
},
{
name: 'ramda',
description: 'Functional programming library',
packageManager: PackageManager.NPM,
downloads: 800000,
stars: 22000,
lastUpdated: '2024-01-10',
score: 90,
features: ['functional programming', 'immutable operations'],
},
],
'express': [
{
name: 'fastify',
description: 'Fast and low overhead web framework',
packageManager: PackageManager.NPM,
downloads: 2000000,
stars: 28000,
lastUpdated: '2024-01-20',
score: 95,
features: ['high performance', 'plugin system', 'schema validation'],
},
{
name: 'koa',
description: 'Expressive middleware for node.js',
packageManager: PackageManager.NPM,
downloads: 1500000,
stars: 34000,
lastUpdated: '2024-01-18',
score: 88,
features: ['async/await', 'middleware', 'context'],
},
],
};
const packageAlternatives = mockAlternatives[packageName.toLowerCase()];
if (packageAlternatives) {
alternatives.push(...packageAlternatives);
} else {
// Generate generic alternative
alternatives.push({
name: `${packageName}-alternative`,
description: `Alternative to ${packageName}`,
packageManager: packageManager || PackageManager.NPM,
downloads: 100000,
stars: 1000,
lastUpdated: '2024-01-01',
score: 70,
features: ['alternative implementation'],
});
}
// Sort by score
alternatives.sort((a, b) => b.score - a.score);
return alternatives;
} catch (error) {
this.logger.error('Error suggesting alternatives:', error);
throw error;
}
}
async securityAudit(projectPath: string, severity: SeverityLevel = SeverityLevel.WARNING): Promise<any> {
this.logger.info(`Performing security audit for: ${projectPath}`);
try {
const packageManager = await this.packageManagerDetector.getPrimaryPackageManager(projectPath);
if (!packageManager) {
throw new Error('No package manager detected');
}
const dependencies = await this.parseDependencies(projectPath, packageManager, {});
const audits = await this.performSecurityAudits(dependencies, packageManager);
// Filter by severity
const filteredAudits = audits.filter(audit =>
audit.vulnerabilities.some(vuln =>
this.compareSeverity(vuln.severity, severity) >= 0
)
);
// Generate security report
const securityReport = await this.securityAuditor.generateSecurityReport(filteredAudits);
return {
audits: filteredAudits,
report: securityReport,
severity,
};
} catch (error) {
this.logger.error('Error performing security audit:', error);
throw error;
}
}
private compareSeverity(severity1: SeverityLevel, severity2: SeverityLevel): number {
const severityOrder = {
[SeverityLevel.ERROR]: 4,
[SeverityLevel.WARNING]: 3,
[SeverityLevel.INFO]: 2,
[SeverityLevel.HINT]: 1,
};
return severityOrder[severity1] - severityOrder[severity2];
}
clearCache(): void {
this.versionResolver.clearCache();
this.securityAuditor.clearCache();
this.logger.debug('Dependency analyzer cache cleared');
}
}