From dbdecaf5a1c54a876b7ee0153d58dd39b1080f97 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 23 May 2026 23:14:52 +0900 Subject: refactor(semver): change ConstraintInterface to a closed enum Replace the dyn ConstraintInterface trait objects with an AnyConstraint enum closing over its four implementors (Simple, Multi, MatchAll, MatchNone), mirroring the earlier Rule enum conversion. Rename constraint.rs to simple_constraint.rs to match the renamed Constraint type. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/constraint/any_constraint.rs | 169 +++++++++ crates/shirabe-semver/src/constraint/constraint.rs | 409 --------------------- .../src/constraint/constraint_interface.rs | 43 --- .../src/constraint/match_all_constraint.rs | 55 +-- .../src/constraint/match_none_constraint.rs | 41 +-- .../src/constraint/multi_constraint.rs | 196 ++++------ .../src/constraint/simple_constraint.rs | 335 +++++++++++++++++ 7 files changed, 599 insertions(+), 649 deletions(-) create mode 100644 crates/shirabe-semver/src/constraint/any_constraint.rs delete mode 100644 crates/shirabe-semver/src/constraint/constraint.rs delete mode 100644 crates/shirabe-semver/src/constraint/constraint_interface.rs create mode 100644 crates/shirabe-semver/src/constraint/simple_constraint.rs (limited to 'crates/shirabe-semver/src/constraint') diff --git a/crates/shirabe-semver/src/constraint/any_constraint.rs b/crates/shirabe-semver/src/constraint/any_constraint.rs new file mode 100644 index 0000000..b61a36f --- /dev/null +++ b/crates/shirabe-semver/src/constraint/any_constraint.rs @@ -0,0 +1,169 @@ +//! ref: composer/vendor/composer/semver/src/Constraint/ConstraintInterface.php + +use crate::constraint::Bound; +use crate::constraint::MatchAllConstraint; +use crate::constraint::MatchNoneConstraint; +use crate::constraint::MultiConstraint; +use crate::constraint::SimpleConstraint; + +/// Corresponds to PHP's `ConstraintInterface`. +#[derive(Clone, Debug)] +pub enum AnyConstraint { + Simple(SimpleConstraint), + Multi(MultiConstraint), + MatchAll(MatchAllConstraint), + MatchNone(MatchNoneConstraint), +} + +impl AnyConstraint { + pub fn matches(&self, provider: &AnyConstraint) -> bool { + match self { + Self::MatchAll(_) => true, + Self::MatchNone(_) => false, + Self::Simple(c) => match provider.as_constraint() { + Some(p) => c.match_specific(p, false), + None => provider.matches(self), + }, + Self::Multi(m) => { + if !m.conjunctive { + m.constraints.iter().any(|sub| provider.matches(sub)) + } else if provider.is_disjunctive() { + provider.matches(self) + } else { + m.constraints.iter().all(|sub| provider.matches(sub)) + } + } + } + } + + pub fn compile(&self, other_operator: i64) -> String { + match self { + Self::Simple(c) => c.compile(other_operator), + Self::Multi(c) => c.compile(other_operator), + Self::MatchAll(c) => c.compile(other_operator), + Self::MatchNone(c) => c.compile(other_operator), + } + } + + pub fn get_upper_bound(&self) -> Bound { + match self { + Self::Simple(c) => c.get_upper_bound(), + Self::Multi(c) => c.get_upper_bound(), + Self::MatchAll(c) => c.get_upper_bound(), + Self::MatchNone(c) => c.get_upper_bound(), + } + } + + pub fn get_lower_bound(&self) -> Bound { + match self { + Self::Simple(c) => c.get_lower_bound(), + Self::Multi(c) => c.get_lower_bound(), + Self::MatchAll(c) => c.get_lower_bound(), + Self::MatchNone(c) => c.get_lower_bound(), + } + } + + pub fn get_pretty_string(&self) -> String { + match self { + Self::Simple(c) => c.get_pretty_string(), + Self::Multi(c) => c.get_pretty_string(), + Self::MatchAll(c) => c.get_pretty_string(), + Self::MatchNone(c) => c.get_pretty_string(), + } + } + + /// PHP `$c instanceof MultiConstraint && $c->isDisjunctive()`. + pub fn is_disjunctive(&self) -> bool { + matches!(self, Self::Multi(m) if !m.conjunctive) + } + + /// PHP `$c instanceof Constraint`. + pub fn is_constraint(&self) -> bool { + matches!(self, Self::Simple(_)) + } + + pub fn get_operator(&self) -> &'static str { + match self { + Self::Simple(c) => c.get_operator(), + _ => "", + } + } + + pub fn get_version(&self) -> &str { + match self { + Self::Simple(c) => c.get_version(), + _ => "", + } + } + + pub fn as_constraint(&self) -> Option<&SimpleConstraint> { + match self { + Self::Simple(c) => Some(c), + _ => None, + } + } + + pub fn as_multi_constraint(&self) -> Option<&MultiConstraint> { + match self { + Self::Multi(c) => Some(c), + _ => None, + } + } + + pub fn is_match_all(&self) -> bool { + matches!(self, Self::MatchAll(_)) + } + + pub fn is_match_none(&self) -> bool { + matches!(self, Self::MatchNone(_)) + } + + /// PHP exposes `ConstraintInterface::setPrettyString()` and defaults the + /// pretty string to the constraint's string form when unset. This port takes + /// the pretty string at construction instead; this setter exists only so + /// `MultiConstraint::create()` can apply the pretty string PHP sets on its + /// (possibly polymorphic) result. + pub(crate) fn set_pretty_string(&mut self, pretty_string: Option) { + match self { + Self::Simple(c) => c.pretty_string = pretty_string, + Self::Multi(c) => c.pretty_string = pretty_string, + Self::MatchAll(c) => c.pretty_string = pretty_string, + Self::MatchNone(c) => c.pretty_string = pretty_string, + } + } +} + +impl From for AnyConstraint { + fn from(c: SimpleConstraint) -> Self { + Self::Simple(c) + } +} + +impl From for AnyConstraint { + fn from(c: MultiConstraint) -> Self { + Self::Multi(c) + } +} + +impl From for AnyConstraint { + fn from(c: MatchAllConstraint) -> Self { + Self::MatchAll(c) + } +} + +impl From for AnyConstraint { + fn from(c: MatchNoneConstraint) -> Self { + Self::MatchNone(c) + } +} + +impl std::fmt::Display for AnyConstraint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Simple(c) => write!(f, "{}", c), + Self::Multi(c) => write!(f, "{}", c), + Self::MatchAll(c) => write!(f, "{}", c), + Self::MatchNone(c) => write!(f, "{}", c), + } + } +} diff --git a/crates/shirabe-semver/src/constraint/constraint.rs b/crates/shirabe-semver/src/constraint/constraint.rs deleted file mode 100644 index 3ac633d..0000000 --- a/crates/shirabe-semver/src/constraint/constraint.rs +++ /dev/null @@ -1,409 +0,0 @@ -//! ref: composer/vendor/composer/semver/src/Constraint/Constraint.php - -use std::sync::Mutex; - -use anyhow::bail; -use shirabe_php_shim as php; - -use crate::constraint::Bound; -use crate::constraint::ConstraintInterface; - -#[derive(Debug)] -pub struct Constraint { - pub(crate) operator: i64, - pub(crate) version: String, - pub(crate) pretty_string: Option, - pub(crate) lower_bound: Mutex>, - pub(crate) upper_bound: Mutex>, -} - -impl Constraint { - pub const OP_EQ: i64 = 0; - pub const OP_LT: i64 = 1; - pub const OP_LE: i64 = 2; - pub const OP_GT: i64 = 3; - pub const OP_GE: i64 = 4; - pub const OP_NE: i64 = 5; - - pub const STR_OP_EQ: &'static str = "=="; - pub const STR_OP_EQ_ALT: &'static str = "="; - pub const STR_OP_LT: &'static str = "<"; - pub const STR_OP_LE: &'static str = "<="; - pub const STR_OP_GT: &'static str = ">"; - pub const STR_OP_GE: &'static str = ">="; - pub const STR_OP_NE: &'static str = "!="; - pub const STR_OP_NE_ALT: &'static str = "<>"; - - fn trans_op_str(op: &str) -> Option { - match op { - "=" => Some(Self::OP_EQ), - "==" => Some(Self::OP_EQ), - "<" => Some(Self::OP_LT), - "<=" => Some(Self::OP_LE), - ">" => Some(Self::OP_GT), - ">=" => Some(Self::OP_GE), - "<>" => Some(Self::OP_NE), - "!=" => Some(Self::OP_NE), - _ => None, - } - } - - fn trans_op_int(op: i64) -> &'static str { - match op { - Self::OP_EQ => "==", - Self::OP_LT => "<", - Self::OP_LE => "<=", - Self::OP_GT => ">", - Self::OP_GE => ">=", - Self::OP_NE => "!=", - _ => panic!("unknown operator: {}", op), - } - } - - pub fn new(operator: impl Into, version: impl Into) -> Self { - let operator: String = operator.into(); - let op_int = Self::trans_op_str(&operator).unwrap_or_else(|| { - // PHP raises InvalidArgumentException; in the Rust port keep that as a panic - // because invalid operators are programmer errors caught during porting. - panic!( - "Invalid operator \"{}\" given, expected one of: {}", - operator, - Self::get_supported_operators().join(", ") - ) - }); - - Self { - operator: op_int, - version: version.into(), - pretty_string: None, - lower_bound: Mutex::new(None), - upper_bound: Mutex::new(None), - } - } - - pub fn get_version(&self) -> &str { - &self.version - } - - pub fn get_operator(&self) -> &'static str { - Self::trans_op_int(self.operator) - } - - pub fn get_supported_operators() -> Vec<&'static str> { - vec!["=", "==", "<", "<=", ">", ">=", "<>", "!="] - } - - pub fn get_operator_constant(operator: &str) -> i64 { - Self::trans_op_str(operator).expect("valid operator") - } - - pub fn version_compare( - &self, - a: &str, - b: &str, - operator: &str, - compare_branches: bool, - ) -> anyhow::Result { - if Self::trans_op_str(operator).is_none() { - bail!( - "Invalid operator \"{}\" given, expected one of: {}", - operator, - Self::get_supported_operators().join(", ") - ); - } - - let a_is_branch = a.starts_with("dev-"); - let b_is_branch = b.starts_with("dev-"); - - if operator == "!=" && (a_is_branch || b_is_branch) { - return Ok(a != b); - } - - if a_is_branch && b_is_branch { - return Ok(operator == "==" && a == b); - } - - if !compare_branches && (a_is_branch || b_is_branch) { - return Ok(false); - } - - Ok(php::version_compare(a, b, operator)) - } - - pub fn compile_constraint(&self, other_operator: i64) -> String { - if self.version.starts_with("dev-") { - if Self::OP_EQ == self.operator { - if Self::OP_EQ == other_operator { - return format!("$b && $v === {}", php::var_export_str(&self.version, true)); - } - if Self::OP_NE == other_operator { - return format!("!$b || $v !== {}", php::var_export_str(&self.version, true)); - } - return "false".to_string(); - } - - if Self::OP_NE == self.operator { - if Self::OP_EQ == other_operator { - return format!("!$b || $v !== {}", php::var_export_str(&self.version, true)); - } - if Self::OP_NE == other_operator { - return "true".to_string(); - } - return "!$b".to_string(); - } - - return "false".to_string(); - } - - if Self::OP_EQ == self.operator { - if Self::OP_EQ == other_operator { - return format!( - "\\version_compare($v, {}, '==')", - php::var_export_str(&self.version, true) - ); - } - if Self::OP_NE == other_operator { - return format!( - "$b || \\version_compare($v, {}, '!=')", - php::var_export_str(&self.version, true) - ); - } - return format!( - "!$b && \\version_compare({}, $v, '{}')", - php::var_export_str(&self.version, true), - Self::trans_op_int(other_operator) - ); - } - - if Self::OP_NE == self.operator { - if Self::OP_EQ == other_operator { - return format!( - "$b || (!$b && \\version_compare($v, {}, '!='))", - php::var_export_str(&self.version, true) - ); - } - if Self::OP_NE == other_operator { - return "true".to_string(); - } - return "!$b".to_string(); - } - - if Self::OP_LT == self.operator || Self::OP_LE == self.operator { - if Self::OP_LT == other_operator || Self::OP_LE == other_operator { - return "!$b".to_string(); - } - } else if Self::OP_GT == other_operator || Self::OP_GE == other_operator { - return "!$b".to_string(); - } - - if Self::OP_NE == other_operator { - return "true".to_string(); - } - - let code_comparison = format!( - "\\version_compare($v, {}, '{}')", - php::var_export_str(&self.version, true), - Self::trans_op_int(self.operator) - ); - - if self.operator == Self::OP_LE && other_operator == Self::OP_GT { - return format!( - "!$b && \\version_compare($v, {}, '!=') && {}", - php::var_export_str(&self.version, true), - code_comparison - ); - } - - if self.operator == Self::OP_GE && other_operator == Self::OP_LT { - return format!( - "!$b && \\version_compare($v, {}, '!=') && {}", - php::var_export_str(&self.version, true), - code_comparison - ); - } - - format!("!$b && {}", code_comparison) - } - - pub fn match_specific(&self, provider: &Constraint, compare_branches: bool) -> bool { - let no_equal_op = Self::trans_op_int(self.operator).replace('=', ""); - let provider_no_equal_op = Self::trans_op_int(provider.operator).replace('=', ""); - - let is_equal_op = Self::OP_EQ == self.operator; - let is_non_equal_op = Self::OP_NE == self.operator; - let is_provider_equal_op = Self::OP_EQ == provider.operator; - let is_provider_non_equal_op = Self::OP_NE == provider.operator; - - if is_non_equal_op || is_provider_non_equal_op { - if is_non_equal_op - && !is_provider_non_equal_op - && !is_provider_equal_op - && provider.version.starts_with("dev-") - { - return false; - } - - if is_provider_non_equal_op - && !is_non_equal_op - && !is_equal_op - && self.version.starts_with("dev-") - { - return false; - } - - if !is_equal_op && !is_provider_equal_op { - return true; - } - return self - .version_compare(&provider.version, &self.version, "!=", compare_branches) - .expect("valid operator"); - } - - if self.operator != Self::OP_EQ && no_equal_op == provider_no_equal_op { - return !(self.version.starts_with("dev-") || provider.version.starts_with("dev-")); - } - - let (version1, version2, operator) = if is_equal_op { - (&self.version, &provider.version, provider.operator) - } else { - (&provider.version, &self.version, self.operator) - }; - - if self - .version_compare( - version1, - version2, - Self::trans_op_int(operator), - compare_branches, - ) - .expect("valid operator") - { - return !(Self::trans_op_int(provider.operator) == provider_no_equal_op - && Self::trans_op_int(self.operator) != no_equal_op - && php::version_compare(&provider.version, &self.version, "==")); - } - - false - } - - fn extract_bounds(&self) { - if self.lower_bound.lock().unwrap().is_some() { - return; - } - - if self.version.starts_with("dev-") { - *self.lower_bound.lock().unwrap() = Some(Bound::zero()); - *self.upper_bound.lock().unwrap() = Some(Bound::positive_infinity()); - return; - } - - let (lower, upper) = match self.operator { - Self::OP_EQ => ( - Bound::new(self.version.clone(), true), - Bound::new(self.version.clone(), true), - ), - Self::OP_LT => (Bound::zero(), Bound::new(self.version.clone(), false)), - Self::OP_LE => (Bound::zero(), Bound::new(self.version.clone(), true)), - Self::OP_GT => ( - Bound::new(self.version.clone(), false), - Bound::positive_infinity(), - ), - Self::OP_GE => ( - Bound::new(self.version.clone(), true), - Bound::positive_infinity(), - ), - Self::OP_NE => (Bound::zero(), Bound::positive_infinity()), - _ => panic!("unknown operator: {}", self.operator), - }; - - *self.lower_bound.lock().unwrap() = Some(lower); - *self.upper_bound.lock().unwrap() = Some(upper); - } -} - -impl Clone for Constraint { - fn clone(&self) -> Self { - Self { - operator: self.operator, - version: self.version.clone(), - pretty_string: self.pretty_string.clone(), - lower_bound: Mutex::new(self.lower_bound.lock().unwrap().clone()), - upper_bound: Mutex::new(self.upper_bound.lock().unwrap().clone()), - } - } -} - -impl ConstraintInterface for Constraint { - fn matches(&self, provider: &dyn ConstraintInterface) -> bool { - if let Some(p) = provider.as_any().downcast_ref::() { - return self.match_specific(p, false); - } - provider.matches(self) - } - - fn compile(&self, other_operator: i64) -> String { - self.compile_constraint(other_operator) - } - - fn set_pretty_string(&mut self, pretty_string: Option) { - self.pretty_string = pretty_string; - } - - fn get_pretty_string(&self) -> String { - if let Some(ref s) = self.pretty_string - && !s.is_empty() - { - return s.clone(); - } - self.__to_string() - } - - fn __to_string(&self) -> String { - format!("{} {}", Self::trans_op_int(self.operator), self.version) - } - - fn get_lower_bound(&self) -> Bound { - self.extract_bounds(); - self.lower_bound - .lock() - .unwrap() - .clone() - .expect("extract_bounds should have set lower_bound") - } - - fn get_upper_bound(&self) -> Bound { - self.extract_bounds(); - self.upper_bound - .lock() - .unwrap() - .clone() - .expect("extract_bounds should have set upper_bound") - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn is_constraint(&self) -> bool { - true - } - - fn get_operator(&self) -> &'static str { - Self::trans_op_int(self.operator) - } - - fn get_version(&self) -> &str { - &self.version - } -} - -impl std::fmt::Display for Constraint { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use crate::constraint::ConstraintInterface; - write!(f, "{}", ConstraintInterface::__to_string(self)) - } -} diff --git a/crates/shirabe-semver/src/constraint/constraint_interface.rs b/crates/shirabe-semver/src/constraint/constraint_interface.rs deleted file mode 100644 index f04ed2a..0000000 --- a/crates/shirabe-semver/src/constraint/constraint_interface.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! ref: composer/vendor/composer/semver/src/Constraint/ConstraintInterface.php - -use crate::constraint::Bound; - -pub trait ConstraintInterface: std::fmt::Debug { - fn matches(&self, provider: &dyn ConstraintInterface) -> bool; - - fn compile(&self, other_operator: i64) -> String; - - fn get_upper_bound(&self) -> Bound; - - fn get_lower_bound(&self) -> Bound; - - fn get_pretty_string(&self) -> String; - - fn set_pretty_string(&mut self, pretty_string: Option); - - fn __to_string(&self) -> String; - - // Rust-specific helpers for instanceof checks in MultiConstraint::matches and optimizeConstraints. - fn is_disjunctive(&self) -> bool { - false - } - - /// Rust-specific helper: PHP `$c instanceof Constraint` check. - fn is_constraint(&self) -> bool { - false - } - - /// Rust-specific helper: PHP `$c->getOperator()`. Only meaningful when `is_constraint()` is true. - fn get_operator(&self) -> &'static str { - "" - } - - /// Rust-specific helper: PHP `$c->getVersion()`. Only meaningful when `is_constraint()` is true. - fn get_version(&self) -> &str { - "" - } - - fn clone_box(&self) -> Box; - - fn as_any(&self) -> &dyn std::any::Any; -} diff --git a/crates/shirabe-semver/src/constraint/match_all_constraint.rs b/crates/shirabe-semver/src/constraint/match_all_constraint.rs index b695ce2..8d4e870 100644 --- a/crates/shirabe-semver/src/constraint/match_all_constraint.rs +++ b/crates/shirabe-semver/src/constraint/match_all_constraint.rs @@ -1,68 +1,41 @@ //! ref: composer/vendor/composer/semver/src/Constraint/MatchAllConstraint.php use crate::constraint::Bound; -use crate::constraint::ConstraintInterface; -#[derive(Debug)] +#[derive(Debug, Clone, Default)] pub struct MatchAllConstraint { pub(crate) pretty_string: Option, } impl MatchAllConstraint { - pub fn new() -> Self { - Self { - pretty_string: None, - } + pub fn new(pretty_string: Option) -> Self { + Self { pretty_string } } -} - -impl Default for MatchAllConstraint { - fn default() -> Self { - Self::new() - } -} -impl ConstraintInterface for MatchAllConstraint { - fn matches(&self, _provider: &dyn ConstraintInterface) -> bool { - true - } - - fn compile(&self, _other_operator: i64) -> String { + pub fn compile(&self, _other_operator: i64) -> String { "true".to_string() } - fn set_pretty_string(&mut self, pretty_string: Option) { - self.pretty_string = pretty_string; - } - - fn get_pretty_string(&self) -> String { + pub fn get_pretty_string(&self) -> String { if let Some(ref s) = self.pretty_string && !s.is_empty() { return s.clone(); } - self.__to_string() + self.to_string() } - fn __to_string(&self) -> String { - "*".to_string() - } - - fn clone_box(&self) -> Box { - Box::new(MatchAllConstraint { - pretty_string: self.pretty_string.clone(), - }) - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn get_upper_bound(&self) -> Bound { + pub fn get_upper_bound(&self) -> Bound { Bound::positive_infinity() } - fn get_lower_bound(&self) -> Bound { + pub fn get_lower_bound(&self) -> Bound { Bound::zero() } } + +impl std::fmt::Display for MatchAllConstraint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "*") + } +} diff --git a/crates/shirabe-semver/src/constraint/match_none_constraint.rs b/crates/shirabe-semver/src/constraint/match_none_constraint.rs index 587058a..08cb319 100644 --- a/crates/shirabe-semver/src/constraint/match_none_constraint.rs +++ b/crates/shirabe-semver/src/constraint/match_none_constraint.rs @@ -1,54 +1,41 @@ //! ref: composer/vendor/composer/semver/src/Constraint/MatchNoneConstraint.php use crate::constraint::Bound; -use crate::constraint::ConstraintInterface; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MatchNoneConstraint { pub(crate) pretty_string: Option, } -impl ConstraintInterface for MatchNoneConstraint { - fn matches(&self, _provider: &dyn ConstraintInterface) -> bool { - false +impl MatchNoneConstraint { + pub fn new(pretty_string: Option) -> Self { + Self { pretty_string } } - fn compile(&self, _other_operator: i64) -> String { + pub fn compile(&self, _other_operator: i64) -> String { "false".to_string() } - fn set_pretty_string(&mut self, pretty_string: Option) { - self.pretty_string = pretty_string; - } - - fn get_pretty_string(&self) -> String { + pub fn get_pretty_string(&self) -> String { if let Some(ref s) = self.pretty_string && !s.is_empty() { return s.clone(); } - self.__to_string() + self.to_string() } - fn __to_string(&self) -> String { - "[]".to_string() - } - - fn clone_box(&self) -> Box { - Box::new(MatchNoneConstraint { - pretty_string: self.pretty_string.clone(), - }) - } - - fn as_any(&self) -> &dyn std::any::Any { - self + pub fn get_upper_bound(&self) -> Bound { + Bound::new("0.0.0.0-dev".to_string(), false) } - fn get_upper_bound(&self) -> Bound { + pub fn get_lower_bound(&self) -> Bound { Bound::new("0.0.0.0-dev".to_string(), false) } +} - fn get_lower_bound(&self) -> Bound { - Bound::new("0.0.0.0-dev".to_string(), false) +impl std::fmt::Display for MatchNoneConstraint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[]") } } diff --git a/crates/shirabe-semver/src/constraint/multi_constraint.rs b/crates/shirabe-semver/src/constraint/multi_constraint.rs index 4222095..05cc16e 100644 --- a/crates/shirabe-semver/src/constraint/multi_constraint.rs +++ b/crates/shirabe-semver/src/constraint/multi_constraint.rs @@ -1,30 +1,22 @@ //! ref: composer/vendor/composer/semver/src/Constraint/MultiConstraint.php -use std::cell::RefCell; - +use crate::constraint::AnyConstraint; use crate::constraint::Bound; -use crate::constraint::ConstraintInterface; use crate::constraint::MatchAllConstraint; +#[derive(Debug, Clone)] pub struct MultiConstraint { - pub(crate) constraints: Vec>, + pub(crate) constraints: Vec, pub(crate) pretty_string: Option, - string: RefCell>, pub(crate) conjunctive: bool, - lower_bound: RefCell>, - upper_bound: RefCell>, -} - -impl std::fmt::Debug for MultiConstraint { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MultiConstraint") - .field("conjunctive", &self.conjunctive) - .finish() - } } impl MultiConstraint { - pub fn new(constraints: Vec>, conjunctive: bool) -> Self { + pub fn new( + constraints: Vec, + conjunctive: bool, + pretty_string: Option, + ) -> Self { assert!( constraints.len() >= 2, "Must provide at least two constraints for a MultiConstraint. Use \ @@ -34,15 +26,12 @@ impl MultiConstraint { Self { constraints, - pretty_string: None, - string: RefCell::new(None), + pretty_string, conjunctive, - lower_bound: RefCell::new(None), - upper_bound: RefCell::new(None), } } - pub fn get_constraints(&self) -> &[Box] { + pub fn get_constraints(&self) -> &[AnyConstraint] { &self.constraints } @@ -54,11 +43,9 @@ impl MultiConstraint { !self.conjunctive } - fn extract_bounds(&self) { - if self.lower_bound.borrow().is_some() { - return; - } - + /// Composer memoizes the result; this port recomputes on every call. It is not heavy + /// calculation so caching is a premature optimization. + fn extract_bounds(&self) -> (Bound, Bound) { let mut current_lower: Option = None; let mut current_upper: Option = None; @@ -93,52 +80,61 @@ impl MultiConstraint { } } - *self.lower_bound.borrow_mut() = current_lower; - *self.upper_bound.borrow_mut() = current_upper; + ( + current_lower.expect("MultiConstraint always has at least two constraints"), + current_upper.expect("MultiConstraint always has at least two constraints"), + ) } pub fn create( - constraints: Vec>, + constraints: Vec, conjunctive: bool, - ) -> anyhow::Result> { + pretty_string: Option, + ) -> anyhow::Result { if constraints.is_empty() { - return Ok(Box::new(MatchAllConstraint { - pretty_string: None, - })); + return Ok(MatchAllConstraint::new(pretty_string).into()); } if constraints.len() == 1 { - return Ok(constraints.into_iter().next().unwrap()); + let mut single = constraints.into_iter().next().unwrap(); + if pretty_string.is_some() { + single.set_pretty_string(pretty_string); + } + return Ok(single); } let (constraints, conjunctive) = Self::optimize_constraints(constraints, conjunctive); if constraints.len() == 1 { - return Ok(constraints.into_iter().next().unwrap()); + let mut single = constraints.into_iter().next().unwrap(); + if pretty_string.is_some() { + single.set_pretty_string(pretty_string); + } + return Ok(single); } - Ok(Box::new(MultiConstraint::new(constraints, conjunctive))) + Ok(MultiConstraint::new(constraints, conjunctive, pretty_string).into()) } // Returns the (possibly optimized) constraints and the effective conjunctive flag. // Always returns the constraints vector (consuming it), whether or not optimization was applied. // The PHP version returns null for no optimization; here we return the original values unchanged. fn optimize_constraints( - constraints: Vec>, + constraints: Vec, conjunctive: bool, - ) -> (Vec>, bool) { + ) -> (Vec, bool) { // Parse the two OR groups and if they are contiguous collapse into one constraint. // [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4] if !conjunctive { let mut iter = constraints.into_iter(); - let mut left: Box = iter.next().unwrap(); - let mut merged_constraints: Vec> = Vec::new(); + let mut left: AnyConstraint = iter.next().unwrap(); + let mut merged_constraints: Vec = Vec::new(); let mut optimized = false; for right in iter { - let merged: Option> = { - let maybe_l_mc = left.as_any().downcast_ref::(); - let maybe_r_mc = right.as_any().downcast_ref::(); + let merged: Option = { + let maybe_l_mc = left.as_multi_constraint(); + let maybe_r_mc = right.as_multi_constraint(); if let (Some(l_mc), Some(r_mc)) = (maybe_l_mc, maybe_r_mc) { if l_mc.conjunctive @@ -146,10 +142,10 @@ impl MultiConstraint { && l_mc.constraints.len() == 2 && r_mc.constraints.len() == 2 { - let left0 = l_mc.constraints[0].__to_string(); - let left1 = l_mc.constraints[1].__to_string(); - let right0 = r_mc.constraints[0].__to_string(); - let right1 = r_mc.constraints[1].__to_string(); + let left0 = l_mc.constraints[0].to_string(); + let left1 = l_mc.constraints[1].to_string(); + let right0 = r_mc.constraints[0].to_string(); + let right1 = r_mc.constraints[1].to_string(); if left0.starts_with(">=") && left1.starts_with('<') @@ -157,14 +153,17 @@ impl MultiConstraint { && right1.starts_with('<') && left1.get(2..) == right0.get(3..) { - Some(Box::new(MultiConstraint::new( - vec![ - l_mc.constraints[0].clone_box(), - r_mc.constraints[1].clone_box(), - ], - true, - )) - as Box) + Some( + MultiConstraint::new( + vec![ + l_mc.constraints[0].clone(), + r_mc.constraints[1].clone(), + ], + true, + None, + ) + .into(), + ) } else { None } @@ -198,10 +197,8 @@ impl MultiConstraint { (constraints, conjunctive) } -} -impl ConstraintInterface for MultiConstraint { - fn compile(&self, other_operator: i64) -> String { + pub fn compile(&self, other_operator: i64) -> String { let mut parts = Vec::new(); for constraint in &self.constraints { let code = constraint.compile(other_operator); @@ -233,87 +230,28 @@ impl ConstraintInterface for MultiConstraint { } } - fn matches(&self, provider: &dyn ConstraintInterface) -> bool { - if !self.conjunctive { - for constraint in &self.constraints { - if provider.matches(constraint.as_ref()) { - return true; - } - } - return false; - } - - if provider.is_disjunctive() { - return provider.matches(self); - } - - for constraint in &self.constraints { - if !provider.matches(constraint.as_ref()) { - return false; - } - } - - true - } - - fn set_pretty_string(&mut self, pretty_string: Option) { - self.pretty_string = pretty_string; - } - - fn get_pretty_string(&self) -> String { + pub fn get_pretty_string(&self) -> String { if let Some(ref s) = self.pretty_string && !s.is_empty() { return s.clone(); } - self.__to_string() - } - - fn __to_string(&self) -> String { - if let Some(ref s) = *self.string.borrow() { - return s.clone(); - } - - let parts: Vec = self.constraints.iter().map(|c| c.__to_string()).collect(); - let sep = if self.conjunctive { " " } else { " || " }; - let result = format!("[{}]", parts.join(sep)); - - *self.string.borrow_mut() = Some(result.clone()); - result - } - - fn get_lower_bound(&self) -> Bound { - self.extract_bounds(); - self.lower_bound - .borrow() - .clone() - .expect("extractBounds should have populated the lowerBound property") - } - - fn get_upper_bound(&self) -> Bound { - self.extract_bounds(); - self.upper_bound - .borrow() - .clone() - .expect("extractBounds should have populated the upperBound property") + self.to_string() } - fn is_disjunctive(&self) -> bool { - !self.conjunctive + pub fn get_lower_bound(&self) -> Bound { + self.extract_bounds().0 } - fn clone_box(&self) -> Box { - Box::new(MultiConstraint { - constraints: self.constraints.iter().map(|c| c.clone_box()).collect(), - pretty_string: self.pretty_string.clone(), - string: RefCell::new(self.string.borrow().clone()), - conjunctive: self.conjunctive, - lower_bound: RefCell::new(self.lower_bound.borrow().clone()), - upper_bound: RefCell::new(self.upper_bound.borrow().clone()), - }) + pub fn get_upper_bound(&self) -> Bound { + self.extract_bounds().1 } +} - fn as_any(&self) -> &dyn std::any::Any { - self +impl std::fmt::Display for MultiConstraint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let parts: Vec = self.constraints.iter().map(|c| c.to_string()).collect(); + let sep = if self.conjunctive { " " } else { " || " }; + write!(f, "[{}]", parts.join(sep)) } } diff --git a/crates/shirabe-semver/src/constraint/simple_constraint.rs b/crates/shirabe-semver/src/constraint/simple_constraint.rs new file mode 100644 index 0000000..161425a --- /dev/null +++ b/crates/shirabe-semver/src/constraint/simple_constraint.rs @@ -0,0 +1,335 @@ +//! ref: composer/vendor/composer/semver/src/Constraint/Constraint.php + +use anyhow::bail; +use shirabe_php_shim as php; + +use crate::constraint::Bound; + +/// Corresponds to PHP's `Constraint`. +#[derive(Debug, Clone)] +pub struct SimpleConstraint { + pub(crate) operator: i64, + pub(crate) version: String, + pub(crate) pretty_string: Option, +} + +impl SimpleConstraint { + pub const OP_EQ: i64 = 0; + pub const OP_LT: i64 = 1; + pub const OP_LE: i64 = 2; + pub const OP_GT: i64 = 3; + pub const OP_GE: i64 = 4; + pub const OP_NE: i64 = 5; + + pub const STR_OP_EQ: &'static str = "=="; + pub const STR_OP_EQ_ALT: &'static str = "="; + pub const STR_OP_LT: &'static str = "<"; + pub const STR_OP_LE: &'static str = "<="; + pub const STR_OP_GT: &'static str = ">"; + pub const STR_OP_GE: &'static str = ">="; + pub const STR_OP_NE: &'static str = "!="; + pub const STR_OP_NE_ALT: &'static str = "<>"; + + fn trans_op_str(op: &str) -> Option { + match op { + "=" => Some(Self::OP_EQ), + "==" => Some(Self::OP_EQ), + "<" => Some(Self::OP_LT), + "<=" => Some(Self::OP_LE), + ">" => Some(Self::OP_GT), + ">=" => Some(Self::OP_GE), + "<>" => Some(Self::OP_NE), + "!=" => Some(Self::OP_NE), + _ => None, + } + } + + fn trans_op_int(op: i64) -> &'static str { + match op { + Self::OP_EQ => "==", + Self::OP_LT => "<", + Self::OP_LE => "<=", + Self::OP_GT => ">", + Self::OP_GE => ">=", + Self::OP_NE => "!=", + _ => panic!("unknown operator: {}", op), + } + } + + pub fn new(operator: String, version: String, pretty_string: Option) -> Self { + let op_int = Self::trans_op_str(&operator).unwrap_or_else(|| { + // PHP raises InvalidArgumentException; in the Rust port keep that as a panic + // because invalid operators are programmer errors caught during porting. + panic!( + "Invalid operator \"{}\" given, expected one of: {}", + operator, + Self::get_supported_operators().join(", ") + ) + }); + + Self { + operator: op_int, + version, + pretty_string, + } + } + + pub fn get_version(&self) -> &str { + &self.version + } + + pub fn get_operator(&self) -> &'static str { + Self::trans_op_int(self.operator) + } + + pub fn get_supported_operators() -> Vec<&'static str> { + vec!["=", "==", "<", "<=", ">", ">=", "<>", "!="] + } + + pub fn get_operator_constant(operator: &str) -> i64 { + Self::trans_op_str(operator).expect("valid operator") + } + + pub fn version_compare( + &self, + a: &str, + b: &str, + operator: &str, + compare_branches: bool, + ) -> anyhow::Result { + if Self::trans_op_str(operator).is_none() { + bail!( + "Invalid operator \"{}\" given, expected one of: {}", + operator, + Self::get_supported_operators().join(", ") + ); + } + + let a_is_branch = a.starts_with("dev-"); + let b_is_branch = b.starts_with("dev-"); + + if operator == "!=" && (a_is_branch || b_is_branch) { + return Ok(a != b); + } + + if a_is_branch && b_is_branch { + return Ok(operator == "==" && a == b); + } + + if !compare_branches && (a_is_branch || b_is_branch) { + return Ok(false); + } + + Ok(php::version_compare(a, b, operator)) + } + + pub fn compile_constraint(&self, other_operator: i64) -> String { + if self.version.starts_with("dev-") { + if Self::OP_EQ == self.operator { + if Self::OP_EQ == other_operator { + return format!("$b && $v === {}", php::var_export_str(&self.version, true)); + } + if Self::OP_NE == other_operator { + return format!("!$b || $v !== {}", php::var_export_str(&self.version, true)); + } + return "false".to_string(); + } + + if Self::OP_NE == self.operator { + if Self::OP_EQ == other_operator { + return format!("!$b || $v !== {}", php::var_export_str(&self.version, true)); + } + if Self::OP_NE == other_operator { + return "true".to_string(); + } + return "!$b".to_string(); + } + + return "false".to_string(); + } + + if Self::OP_EQ == self.operator { + if Self::OP_EQ == other_operator { + return format!( + "\\version_compare($v, {}, '==')", + php::var_export_str(&self.version, true) + ); + } + if Self::OP_NE == other_operator { + return format!( + "$b || \\version_compare($v, {}, '!=')", + php::var_export_str(&self.version, true) + ); + } + return format!( + "!$b && \\version_compare({}, $v, '{}')", + php::var_export_str(&self.version, true), + Self::trans_op_int(other_operator) + ); + } + + if Self::OP_NE == self.operator { + if Self::OP_EQ == other_operator { + return format!( + "$b || (!$b && \\version_compare($v, {}, '!='))", + php::var_export_str(&self.version, true) + ); + } + if Self::OP_NE == other_operator { + return "true".to_string(); + } + return "!$b".to_string(); + } + + if Self::OP_LT == self.operator || Self::OP_LE == self.operator { + if Self::OP_LT == other_operator || Self::OP_LE == other_operator { + return "!$b".to_string(); + } + } else if Self::OP_GT == other_operator || Self::OP_GE == other_operator { + return "!$b".to_string(); + } + + if Self::OP_NE == other_operator { + return "true".to_string(); + } + + let code_comparison = format!( + "\\version_compare($v, {}, '{}')", + php::var_export_str(&self.version, true), + Self::trans_op_int(self.operator) + ); + + if self.operator == Self::OP_LE && other_operator == Self::OP_GT { + return format!( + "!$b && \\version_compare($v, {}, '!=') && {}", + php::var_export_str(&self.version, true), + code_comparison + ); + } + + if self.operator == Self::OP_GE && other_operator == Self::OP_LT { + return format!( + "!$b && \\version_compare($v, {}, '!=') && {}", + php::var_export_str(&self.version, true), + code_comparison + ); + } + + format!("!$b && {}", code_comparison) + } + + pub fn match_specific(&self, provider: &SimpleConstraint, compare_branches: bool) -> bool { + let no_equal_op = Self::trans_op_int(self.operator).replace('=', ""); + let provider_no_equal_op = Self::trans_op_int(provider.operator).replace('=', ""); + + let is_equal_op = Self::OP_EQ == self.operator; + let is_non_equal_op = Self::OP_NE == self.operator; + let is_provider_equal_op = Self::OP_EQ == provider.operator; + let is_provider_non_equal_op = Self::OP_NE == provider.operator; + + if is_non_equal_op || is_provider_non_equal_op { + if is_non_equal_op + && !is_provider_non_equal_op + && !is_provider_equal_op + && provider.version.starts_with("dev-") + { + return false; + } + + if is_provider_non_equal_op + && !is_non_equal_op + && !is_equal_op + && self.version.starts_with("dev-") + { + return false; + } + + if !is_equal_op && !is_provider_equal_op { + return true; + } + return self + .version_compare(&provider.version, &self.version, "!=", compare_branches) + .expect("valid operator"); + } + + if self.operator != Self::OP_EQ && no_equal_op == provider_no_equal_op { + return !(self.version.starts_with("dev-") || provider.version.starts_with("dev-")); + } + + let (version1, version2, operator) = if is_equal_op { + (&self.version, &provider.version, provider.operator) + } else { + (&provider.version, &self.version, self.operator) + }; + + if self + .version_compare( + version1, + version2, + Self::trans_op_int(operator), + compare_branches, + ) + .expect("valid operator") + { + return !(Self::trans_op_int(provider.operator) == provider_no_equal_op + && Self::trans_op_int(self.operator) != no_equal_op + && php::version_compare(&provider.version, &self.version, "==")); + } + + false + } + + /// Composer memoizes the result; this port recomputes on every call. It is not heavy + /// calculation so caching is a premature optimization. + fn extract_bounds(&self) -> (Bound, Bound) { + if self.version.starts_with("dev-") { + return (Bound::zero(), Bound::positive_infinity()); + } + + match self.operator { + Self::OP_EQ => ( + Bound::new(self.version.clone(), true), + Bound::new(self.version.clone(), true), + ), + Self::OP_LT => (Bound::zero(), Bound::new(self.version.clone(), false)), + Self::OP_LE => (Bound::zero(), Bound::new(self.version.clone(), true)), + Self::OP_GT => ( + Bound::new(self.version.clone(), false), + Bound::positive_infinity(), + ), + Self::OP_GE => ( + Bound::new(self.version.clone(), true), + Bound::positive_infinity(), + ), + Self::OP_NE => (Bound::zero(), Bound::positive_infinity()), + _ => panic!("unknown operator: {}", self.operator), + } + } + + pub fn compile(&self, other_operator: i64) -> String { + self.compile_constraint(other_operator) + } + + pub fn get_pretty_string(&self) -> String { + if let Some(ref s) = self.pretty_string + && !s.is_empty() + { + return s.clone(); + } + self.to_string() + } + + pub fn get_lower_bound(&self) -> Bound { + self.extract_bounds().0 + } + + pub fn get_upper_bound(&self) -> Bound { + self.extract_bounds().1 + } +} + +impl std::fmt::Display for SimpleConstraint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", Self::trans_op_int(self.operator), self.version) + } +} -- cgit v1.3.1