use nargo_types::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConflictType {
VersionConflict,
PeerDependencyConflict,
MissingDependency,
CircularDependency,
}
impl std::fmt::Display for ConflictType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConflictType::VersionConflict => write!(f, "Version Conflict"),
ConflictType::PeerDependencyConflict => write!(f, "Peer Dependency Conflict"),
ConflictType::MissingDependency => write!(f, "Missing Dependency"),
ConflictType::CircularDependency => write!(f, "Circular Dependency"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conflict {
pub conflict_type: ConflictType,
pub package: String,
pub description: String,
pub versions: Vec<String>,
pub required_by: Vec<String>,
}
impl Conflict {
pub fn new(conflict_type: ConflictType, package: impl Into<String>, description: impl Into<String>) -> Self {
Self { conflict_type, package: package.into(), description: description.into(), versions: Vec::new(), required_by: Vec::new() }
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.versions.push(version.into());
self
}
pub fn required_by(mut self, package: impl Into<String>) -> Self {
self.required_by.push(package.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictSolution {
pub conflict: Conflict,
pub recommended_version: Option<String>,
pub description: String,
pub is_automatic: bool,
}
impl ConflictSolution {
pub fn new(conflict: Conflict, description: impl Into<String>) -> Self {
Self { conflict, recommended_version: None, description: description.into(), is_automatic: false }
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.recommended_version = Some(version.into());
self
}
pub fn automatic(mut self) -> Self {
self.is_automatic = true;
self
}
}
#[derive(Debug, Default)]
pub struct ConflictDetector {
conflicts: Vec<Conflict>,
}
impl ConflictDetector {
pub fn new() -> Self {
Self::default()
}
pub fn detect_version_conflicts(&mut self, dependencies: &HashMap<String, Vec<(String, String)>>) -> &Vec<Conflict> {
for (package, version_sources) in dependencies {
if version_sources.len() > 1 {
let versions: Vec<String> = version_sources.iter().map(|(v, _)| v.clone()).collect();
let unique_versions: Vec<String> = versions.iter().filter(|v| !versions.iter().all(|u| u == *v)).cloned().collect();
if unique_versions.len() > 1 {
let sources: Vec<String> = version_sources.iter().map(|(_, s)| s.clone()).collect();
let conflict = Conflict::new(ConflictType::VersionConflict, package.clone(), format!("Multiple incompatible versions of '{}' required: {}", package, versions.join(", "))).with_version(versions.join(", "));
let conflict = sources.iter().fold(conflict, |c, s| c.required_by(s));
self.conflicts.push(conflict);
}
}
}
&self.conflicts
}
pub fn detect_peer_conflicts(&mut self, peer_deps: &HashMap<String, (String, Option<String>)>, resolved: &HashMap<String, String>) -> &Vec<Conflict> {
for (package, (required, peer_of)) in peer_deps {
if let Some(resolved_version) = resolved.get(package) {
if resolved_version != required {
let conflict = Conflict::new(ConflictType::PeerDependencyConflict, package.clone(), format!("Peer dependency '{}' requires version {}, but {} is installed", package, required, resolved_version)).with_version(required.clone()).with_version(resolved_version.clone());
let conflict = if let Some(parent) = peer_of { conflict.required_by(parent) } else { conflict };
self.conflicts.push(conflict);
}
}
else {
let conflict = Conflict::new(ConflictType::MissingDependency, package.clone(), format!("Peer dependency '{}' is required but not installed", package)).with_version(required.clone());
let conflict = if let Some(parent) = peer_of { conflict.required_by(parent) } else { conflict };
self.conflicts.push(conflict);
}
}
&self.conflicts
}
pub fn conflicts(&self) -> &[Conflict] {
&self.conflicts
}
pub fn has_conflicts(&self) -> bool {
!self.conflicts.is_empty()
}
pub fn clear(&mut self) {
self.conflicts.clear();
}
pub fn find_solutions(&self) -> Vec<ConflictSolution> {
self.conflicts.iter().map(|conflict| self.solve_conflict(conflict)).collect()
}
fn solve_conflict(&self, conflict: &Conflict) -> ConflictSolution {
match conflict.conflict_type {
ConflictType::VersionConflict => {
let highest = conflict
.versions
.iter()
.max_by(|a, b| {
let a_parts: Vec<u32> = a.split('.').filter_map(|s| s.parse().ok()).collect();
let b_parts: Vec<u32> = b.split('.').filter_map(|s| s.parse().ok()).collect();
a_parts.cmp(&b_parts)
})
.cloned();
ConflictSolution::new(conflict.clone(), "Use the highest compatible version".to_string()).with_version(highest.unwrap_or_default()).automatic()
}
ConflictType::PeerDependencyConflict => ConflictSolution::new(conflict.clone(), "Install the required peer dependency version".to_string()).with_version(conflict.versions.first().cloned().unwrap_or_default()),
ConflictType::MissingDependency => ConflictSolution::new(conflict.clone(), "Install the missing peer dependency".to_string()).with_version(conflict.versions.first().cloned().unwrap_or_default()),
ConflictType::CircularDependency => ConflictSolution::new(conflict.clone(), "Refactor to remove the circular dependency".to_string()),
}
}
}