use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ResolutionError {
#[error("SAT solver error: {0}")]
Solver(#[from] SatError),
#[error("Dependency conflict: {message}")]
Conflict {
message: String,
conflicts: Vec<ConflictDetail>,
},
#[error("Resolution timed out after {timeout:?}")]
Timeout { timeout: Duration },
#[error("Invalid package specification: {package} {version}")]
InvalidPackage { package: String, version: String },
#[error("Invalid version constraint: '{constraint}' for package {package}")]
InvalidConstraint { package: String, constraint: String },
#[error("Circular dependency detected: {cycle}")]
CircularDependency { cycle: String },
#[error("Registry error: {0}")]
Registry(#[from] anyhow::Error),
#[error("Registry error: {message}")]
RegistryError { message: String },
#[error("Invalid version constraint: '{constraint}' for package {package}")]
InvalidVersionConstraint { package: String, constraint: String },
#[error("Package not found: {package}@{version}")]
PackageNotFound { package: String, version: String },
#[error("Peer dependency conflict: {package} requires {peer} {constraint}")]
PeerConflict {
package: String,
peer: String,
constraint: String,
reason: String,
},
#[error("Optional dependency unavailable: {package}@{version}")]
OptionalUnavailable { package: String, version: String },
#[error("Internal resolver error: {message}")]
Internal { message: String },
}
#[derive(Debug, Error)]
pub enum SatError {
#[error("Failed to initialize SAT solver: {reason}")]
InitializationFailed { reason: String },
#[error("Failed to encode problem: {reason}")]
EncodingFailed { reason: String },
#[error("SAT solver failure: {reason}")]
SolverFailure { reason: String },
#[error("Problem is unsatisfiable")]
Unsatisfiable,
#[error("SAT solver timed out after {duration:?}")]
Timeout { duration: Duration },
#[error("SAT solver ran out of memory")]
OutOfMemory,
#[error("Invalid solution model from solver")]
InvalidModel,
#[error("Failed to generate clauses: {reason}")]
ClauseGeneration { reason: String },
}
#[derive(Debug, Clone)]
pub struct ConflictDetail {
pub package: String,
pub constraint: String,
pub conflicts_with: String,
pub reason: ConflictReason,
}
#[derive(Debug, Clone)]
pub enum ConflictReason {
VersionIncompatible,
PeerDependency,
CircularDependency,
PackageNotFound,
EngineIncompatible,
PlatformIncompatible,
}
impl ConflictDetail {
pub fn new(
package: String,
constraint: String,
conflicts_with: String,
reason: ConflictReason,
) -> Self {
Self {
package,
constraint,
conflicts_with,
reason,
}
}
pub fn version_conflict(package: String, constraint: String, conflicts_with: String) -> Self {
Self::new(
package,
constraint,
conflicts_with,
ConflictReason::VersionIncompatible,
)
}
pub fn peer_conflict(package: String, constraint: String, conflicts_with: String) -> Self {
Self::new(
package,
constraint,
conflicts_with,
ConflictReason::PeerDependency,
)
}
}
impl ResolutionError {
pub fn conflict(message: String, conflicts: Vec<ConflictDetail>) -> Self {
Self::Conflict { message, conflicts }
}
pub fn simple_conflict(message: String) -> Self {
Self::Conflict {
message,
conflicts: Vec::new(),
}
}
pub fn timeout(timeout: Duration) -> Self {
Self::Timeout { timeout }
}
pub fn invalid_package(package: String, version: String) -> Self {
Self::InvalidPackage { package, version }
}
pub fn no_matching_versions(package: String, version_set: crate::version::VersionSet) -> Self {
Self::InvalidPackage {
package: format!("{} with constraint {}", package, version_set),
version: "no matching versions".to_string(),
}
}
pub fn invalid_constraint(package: String, constraint: String) -> Self {
Self::InvalidConstraint {
package,
constraint,
}
}
pub fn circular_dependency(cycle: String) -> Self {
Self::CircularDependency { cycle }
}
pub fn package_not_found(package: String, version: String) -> Self {
Self::PackageNotFound { package, version }
}
pub fn peer_conflict(
package: String,
peer: String,
constraint: String,
reason: String,
) -> Self {
Self::PeerConflict {
package,
peer,
constraint,
reason,
}
}
pub fn internal(message: String) -> Self {
Self::Internal { message }
}
pub fn is_resolvable_conflict(&self) -> bool {
matches!(
self,
Self::Conflict { .. } | Self::PeerConflict { .. } | Self::InvalidConstraint { .. }
)
}
pub fn is_fatal(&self) -> bool {
matches!(
self,
Self::CircularDependency { .. }
| Self::PackageNotFound { .. }
| Self::InvalidPackage { .. }
| Self::Internal { .. }
)
}
pub fn user_message(&self) -> String {
match self {
Self::Conflict { message, conflicts } => {
let mut msg = format!("Dependency conflict: {}", message);
if !conflicts.is_empty() {
msg.push_str("\n\nConflicts:");
for conflict in conflicts {
msg.push_str(&format!(
"\n • {} {} conflicts with {}",
conflict.package, conflict.constraint, conflict.conflicts_with
));
}
msg.push_str(
"\n\nTry:\n • Update conflicting packages\n • Use --force to override \
(not recommended)",
);
}
msg
}
Self::PackageNotFound { package, version } => {
format!(
"Package not found: {}@{}\n\nTry:\n • Check package name spelling\n • \
Verify version exists\n • Check registry configuration",
package, version
)
}
Self::CircularDependency { cycle } => {
format!(
"Circular dependency detected: {}\n\nThis indicates a problem with the \
packages themselves.",
cycle
)
}
_ => self.to_string(),
}
}
}
impl SatError {
pub fn initialization_failed(reason: String) -> Self {
Self::InitializationFailed { reason }
}
pub fn encoding_failed(reason: String) -> Self {
Self::EncodingFailed { reason }
}
pub fn solver_failure(reason: String) -> Self {
Self::SolverFailure { reason }
}
pub fn timeout(duration: Duration) -> Self {
Self::Timeout { duration }
}
pub fn clause_generation(reason: String) -> Self {
Self::ClauseGeneration { reason }
}
pub fn is_unsatisfiable(&self) -> bool {
matches!(self, Self::Unsatisfiable)
}
pub fn is_solver_failure(&self) -> bool {
matches!(
self,
Self::SolverFailure { .. } | Self::OutOfMemory | Self::InvalidModel
)
}
}
pub type ResolutionResult<T> = std::result::Result<T, ResolutionError>;
pub type SatResult<T> = std::result::Result<T, SatError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conflict_detail_creation() {
let conflict = ConflictDetail::version_conflict(
"lodash".to_string(),
"^4.0.0".to_string(),
"lodash@3.10.1".to_string(),
);
assert_eq!(conflict.package, "lodash");
assert_eq!(conflict.constraint, "^4.0.0");
assert_eq!(conflict.conflicts_with, "lodash@3.10.1");
assert!(matches!(
conflict.reason,
ConflictReason::VersionIncompatible
));
}
#[test]
fn test_resolution_error_classification() {
let conflict = ResolutionError::simple_conflict("test conflict".to_string());
assert!(conflict.is_resolvable_conflict());
assert!(!conflict.is_fatal());
let circular = ResolutionError::circular_dependency("A -> B -> A".to_string());
assert!(!circular.is_resolvable_conflict());
assert!(circular.is_fatal());
let not_found =
ResolutionError::package_not_found("missing".to_string(), "1.0.0".to_string());
assert!(!not_found.is_resolvable_conflict());
assert!(not_found.is_fatal());
}
#[test]
fn test_sat_error_classification() {
let unsatisfiable = SatError::Unsatisfiable;
assert!(unsatisfiable.is_unsatisfiable());
assert!(!unsatisfiable.is_solver_failure());
let failure = SatError::solver_failure("crashed".to_string());
assert!(!failure.is_unsatisfiable());
assert!(failure.is_solver_failure());
let timeout = SatError::timeout(Duration::from_secs(30));
assert!(!timeout.is_unsatisfiable());
assert!(!timeout.is_solver_failure());
}
#[test]
fn test_user_friendly_messages() {
let conflict =
ResolutionError::package_not_found("lodash".to_string(), "999.0.0".to_string());
let message = conflict.user_message();
assert!(message.contains("Package not found"));
assert!(message.contains("Check package name spelling"));
}
}