1use crate::bound::Bound;
26use crate::version::{
27 CmpOp, Suffix, Version, VersionKind, is_branch_alias as composer_semver_is_branch_alias,
28};
29use regex::Regex;
30use std::sync::OnceLock;
31
32#[derive(Debug, Clone, Eq, PartialEq)]
33pub enum Constraint {
34 Any,
36 Op {
42 op: CmpOp,
43 version: Version,
44 explicit_lower_bound: bool,
45 },
46 And(Vec<Constraint>),
48 Or(Vec<Constraint>),
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum ParseError {
54 Invalid(String),
55}
56
57impl std::fmt::Display for ParseError {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 Self::Invalid(s) => write!(f, "invalid constraint: {s:?}"),
61 }
62 }
63}
64
65impl std::error::Error for ParseError {}
66
67impl Constraint {
68 pub fn parse(input: &str) -> Result<Self, ParseError> {
75 let trimmed = input.trim();
76 if trimmed.is_empty() {
77 return Err(ParseError::Invalid(input.to_owned()));
78 }
79 let alts: Vec<&str> = re_or_split()
81 .split(trimmed)
82 .map(str::trim)
83 .filter(|s| !s.is_empty())
84 .collect();
85 if alts.is_empty() {
86 return Err(ParseError::Invalid(input.to_owned()));
87 }
88 let parsed: Vec<Constraint> = alts
89 .into_iter()
90 .map(parse_intersection)
91 .collect::<Result<_, _>>()?;
92 Ok(if parsed.len() == 1 {
93 parsed.into_iter().next().unwrap()
94 } else {
95 Self::Or(parsed)
96 })
97 }
98
99 pub fn matches(&self, version: &Version) -> bool {
109 match self {
110 Self::Any => true,
111 Self::Op {
112 op,
113 version: target,
114 explicit_lower_bound,
115 } => matches_atom(version, *op, target, *explicit_lower_bound),
116 Self::And(items) => items.iter().all(|c| c.matches(version)),
117 Self::Or(items) => items.iter().any(|c| c.matches(version)),
118 }
119 }
120
121 #[must_use]
135 pub fn lower_bound(&self) -> Bound {
136 match self {
137 Self::Any => Bound::zero(),
138 Self::Op { op, version, .. } => {
139 if version.normalized.starts_with("dev-") {
140 return Bound::zero();
141 }
142 match op {
143 CmpOp::Eq | CmpOp::Ge => Bound::new(version.normalized.clone(), true),
144 CmpOp::Gt => Bound::new(version.normalized.clone(), false),
145 CmpOp::Lt | CmpOp::Le | CmpOp::Ne => Bound::zero(),
146 }
147 }
148 Self::And(items) => fold_bound(items, true, Constraint::lower_bound),
149 Self::Or(items) => fold_bound(items, false, Constraint::lower_bound),
150 }
151 }
152
153 #[must_use]
159 pub fn upper_bound(&self) -> Bound {
160 match self {
161 Self::Any => Bound::positive_infinity(),
162 Self::Op { op, version, .. } => {
163 if version.normalized.starts_with("dev-") {
164 return Bound::positive_infinity();
165 }
166 match op {
167 CmpOp::Eq | CmpOp::Le => Bound::new(version.normalized.clone(), true),
168 CmpOp::Lt => Bound::new(version.normalized.clone(), false),
169 CmpOp::Gt | CmpOp::Ge | CmpOp::Ne => Bound::positive_infinity(),
170 }
171 }
172 Self::And(items) => fold_bound(items, false, Constraint::upper_bound),
175 Self::Or(items) => fold_bound(items, true, Constraint::upper_bound),
176 }
177 }
178
179 #[must_use]
189 pub fn intersects(&self, other: &Constraint) -> bool {
190 match (self, other) {
191 (Self::Or(items), _) => items.iter().any(|c| c.intersects(other)),
192 (_, Self::Or(items)) => items.iter().any(|c| self.intersects(c)),
193 (Self::Any, _) | (_, Self::Any) => true,
194 _ => intervals_overlap(self, other),
195 }
196 }
197}
198
199fn fold_bound(
205 items: &[Constraint],
206 keep_greater: bool,
207 extract: fn(&Constraint) -> Bound,
208) -> Bound {
209 let mut acc: Option<Bound> = None;
210 for c in items {
211 let b = extract(c);
212 match &acc {
213 None => acc = Some(b),
214 Some(cur) => {
215 if b.compare_to(cur, keep_greater) {
216 acc = Some(b);
217 }
218 }
219 }
220 }
221 acc.unwrap_or_else(Bound::zero)
222}
223
224fn intervals_overlap(a: &Constraint, b: &Constraint) -> bool {
230 let la = a.lower_bound();
231 let lb = b.lower_bound();
232 let ua = a.upper_bound();
233 let ub = b.upper_bound();
234 let lower = if la.compare_to(&lb, true) { la } else { lb };
235 let upper = if ua.compare_to(&ub, false) { ua } else { ub };
236
237 if lower.is_zero() || upper.is_positive_infinity() {
238 return true;
239 }
240 if lower.is_positive_infinity() {
241 return false;
242 }
243 match lower.version_cmp(&upper) {
247 std::cmp::Ordering::Less => true,
248 std::cmp::Ordering::Greater => false,
249 std::cmp::Ordering::Equal => lower.is_inclusive() && upper.is_inclusive(),
250 }
251}
252
253fn matches_atom(candidate: &Version, op: CmpOp, target: &Version, explicit_lower: bool) -> bool {
254 if same_numeric_prerelease_vs_stable(candidate, target) {
255 let admit_as_equal = match op {
264 CmpOp::Lt | CmpOp::Le | CmpOp::Eq | CmpOp::Ne => true,
265 CmpOp::Gt | CmpOp::Ge => explicit_lower,
266 };
267 if admit_as_equal {
268 return matches!(op, CmpOp::Eq | CmpOp::Ge | CmpOp::Le);
269 }
270 }
271 candidate.compare(op, target)
272}
273
274fn same_numeric_prerelease_vs_stable(a: &Version, b: &Version) -> bool {
278 let (
279 VersionKind::Numeric {
280 segments_raw: sa,
281 suffix: suf_a,
282 },
283 VersionKind::Numeric {
284 segments_raw: sb,
285 suffix: suf_b,
286 },
287 ) = (&a.kind, &b.kind)
288 else {
289 return false;
290 };
291 if !segments_equal_numerically(sa, sb) {
292 return false;
293 }
294 is_prerelease(suf_a) ^ is_prerelease(suf_b)
295}
296
297fn is_prerelease(s: &Suffix) -> bool {
298 matches!(
299 s,
300 Suffix::Prerelease { .. }
301 | Suffix::PrereleaseDev { .. }
302 | Suffix::PatchDev(_)
303 | Suffix::Dev
304 )
305}
306
307fn segments_equal_numerically(a: &[String], b: &[String]) -> bool {
308 let len = a.len().max(b.len());
309 for i in 0..len {
310 let va: u64 = a.get(i).and_then(|s| s.parse().ok()).unwrap_or(0);
311 let vb: u64 = b.get(i).and_then(|s| s.parse().ok()).unwrap_or(0);
312 if va != vb {
313 return false;
314 }
315 }
316 true
317}
318
319fn re_or_split() -> &'static Regex {
322 static R: OnceLock<Regex> = OnceLock::new();
323 R.get_or_init(|| Regex::new(r"\s*\|\|?\s*").unwrap())
324}
325
326fn parse_intersection(s: &str) -> Result<Constraint, ParseError> {
331 if let Some(range) = parse_hyphen_range(s)? {
334 return Ok(range);
335 }
336 let tokens = tokenize_intersection(s);
337 let parsed: Vec<Constraint> = tokens
338 .iter()
339 .map(|t| parse_atom(t))
340 .collect::<Result<_, _>>()?;
341 if parsed.is_empty() {
342 return Err(ParseError::Invalid(s.to_owned()));
343 }
344 Ok(if parsed.len() == 1 {
345 parsed.into_iter().next().unwrap()
346 } else {
347 Constraint::And(parsed)
348 })
349}
350
351fn tokenize_intersection(s: &str) -> Vec<String> {
355 let mut out: Vec<String> = Vec::new();
356 let mut cur = String::new();
357 let mut chars = s.chars().peekable();
358 while let Some(ch) = chars.next() {
359 if ch == ',' || ch.is_whitespace() {
360 if !cur.is_empty() && is_pending_operator(&cur) {
365 while let Some(&peek) = chars.peek() {
367 if peek.is_whitespace() {
368 chars.next();
369 } else {
370 break;
371 }
372 }
373 continue;
374 }
375 if !cur.is_empty() {
376 out.push(std::mem::take(&mut cur));
377 }
378 continue;
379 }
380 cur.push(ch);
381 }
382 if !cur.is_empty() {
383 out.push(cur);
384 }
385 out
386}
387
388fn is_pending_operator(s: &str) -> bool {
392 matches!(s, ">" | ">=" | "<" | "<=" | "=" | "==" | "!=" | "<>")
393}
394
395fn re_hyphen() -> &'static Regex {
398 static R: OnceLock<Regex> = OnceLock::new();
399 R.get_or_init(|| {
400 Regex::new(r"^(?P<lo>[^\s]+)\s+-\s+(?P<hi>[^\s]+)$").unwrap()
405 })
406}
407
408fn parse_hyphen_range(s: &str) -> Result<Option<Constraint>, ParseError> {
409 let Some(caps) = re_hyphen().captures(s.trim()) else {
410 return Ok(None);
411 };
412 let lo_str = caps.name("lo").unwrap().as_str();
413 let hi_str = caps.name("hi").unwrap().as_str();
414 let lo = expand_partial_lower(lo_str)?;
415 let hi = expand_partial_upper(hi_str)?;
416 Ok(Some(Constraint::And(vec![
417 Constraint::Op {
418 op: CmpOp::Ge,
419 version: lo,
420 explicit_lower_bound: false,
421 },
422 Constraint::Op {
423 op: CmpOp::Le,
424 version: hi,
425 explicit_lower_bound: false,
426 },
427 ])))
428}
429
430fn expand_partial_lower(s: &str) -> Result<Version, ParseError> {
433 let cleaned = s.split_once('+').map_or(s, |(left, _)| left);
435 let padded = pad_partial(cleaned);
436 Version::parse(&padded).map_err(|e| ParseError::Invalid(e.to_string()))
437}
438
439fn expand_partial_upper(s: &str) -> Result<Version, ParseError> {
443 let cleaned = s.split_once('+').map_or(s, |(left, _)| left);
444 let segs = split_partial_segments(cleaned);
449 if segs.is_empty() {
450 return Err(ParseError::Invalid(s.to_owned()));
451 }
452 let padded = pad_segments_with(segs, "9999999", 4);
453 let candidate = padded.join(".");
454 Version::parse(&candidate).map_err(|e| ParseError::Invalid(e.to_string()))
455}
456
457fn parse_atom(token: &str) -> Result<Constraint, ParseError> {
460 let s = token.trim();
461 if s.is_empty() {
462 return Err(ParseError::Invalid(token.to_owned()));
463 }
464 let s = s.split_once('#').map_or(s, |(left, _)| left).trim_end();
470 if matches!(s, "*" | "x" | "X") {
472 return Ok(Constraint::Any);
473 }
474
475 let is_dev_branch = s.len() >= 4 && s.as_bytes()[..4].eq_ignore_ascii_case(b"dev-");
492 if composer_semver_is_branch_alias(s) || is_dev_branch {
493 let v = Version::parse(s).map_err(|e| ParseError::Invalid(format!("{token}: {e}")))?;
494 return Ok(Constraint::Op {
495 op: CmpOp::Eq,
496 version: v,
497 explicit_lower_bound: true,
498 });
499 }
500
501 if let Some(rest) = s.strip_prefix('^') {
503 return parse_caret(rest.trim());
504 }
505 if let Some(rest) = s.strip_prefix('~') {
507 return parse_tilde(rest.trim());
508 }
509
510 for prefix in &[">=", "<=", "==", "!=", "<>", ">", "<", "="] {
512 if let Some(rest) = s.strip_prefix(*prefix) {
513 let op = match *prefix {
514 ">=" => CmpOp::Ge,
515 "<=" => CmpOp::Le,
516 ">" => CmpOp::Gt,
517 "<" => CmpOp::Lt,
518 "==" | "=" => CmpOp::Eq,
519 "!=" | "<>" => CmpOp::Ne,
520 _ => unreachable!(),
521 };
522 let v = Version::parse(rest.trim())
523 .map_err(|e| ParseError::Invalid(format!("{token}: {e}")))?;
524 let explicit_lower_bound =
529 matches!(op, CmpOp::Ge | CmpOp::Gt) && wrote_full_version(rest.trim());
530 return Ok(Constraint::Op {
531 op,
532 version: v,
533 explicit_lower_bound,
534 });
535 }
536 }
537
538 if contains_wildcard(s) {
541 return parse_wildcard(s);
542 }
543
544 match parse_partial_or_exact(s) {
547 Ok(c) => Ok(c),
548 Err(e) => {
549 if let Some(body) = strip_dev_suffix(s) {
557 if is_name_safe(body) {
558 if let Ok(v) = Version::parse(&format!("dev-{body}")) {
559 return Ok(Constraint::Op {
560 op: CmpOp::Eq,
561 version: v,
562 explicit_lower_bound: true,
563 });
564 }
565 }
566 }
567 Err(e)
568 }
569 }
570}
571
572fn strip_dev_suffix(s: &str) -> Option<&str> {
573 if s.len() >= 5 && s.as_bytes()[s.len() - 4..].eq_ignore_ascii_case(b"-dev") {
574 Some(&s[..s.len() - 4])
575 } else {
576 None
577 }
578}
579
580fn is_name_safe(s: &str) -> bool {
582 !s.is_empty()
583 && s.bytes()
584 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'/'))
585}
586
587fn contains_wildcard(s: &str) -> bool {
588 s.split('.').any(|p| matches!(p, "*" | "x" | "X"))
589}
590
591fn parse_caret(rest: &str) -> Result<Constraint, ParseError> {
592 let segs = split_partial_numeric(rest)?;
608 if segs.is_empty() {
609 return Err(ParseError::Invalid(rest.to_owned()));
610 }
611 let major: u32 = segs[0].parse().unwrap_or(0);
612 let minor: u32 = segs.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
613 let minor_specified = segs.len() >= 2;
614 let patch_specified = segs.len() >= 3;
615 let lower = Version::parse(rest)
616 .or_else(|_| Version::parse(&pad_partial(rest)))
617 .map_err(|e| ParseError::Invalid(format!("^{rest}: {e}")))?;
618 let upper_str = if major > 0 || !minor_specified {
619 format!("{}.0.0.0", major + 1)
620 } else if minor > 0 || !patch_specified {
621 format!("0.{}.0.0", minor + 1)
622 } else {
623 let patch: u32 = segs.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
625 format!("0.0.{}.0", patch + 1)
626 };
627 let upper = Version::parse(&upper_str)
628 .map_err(|e| ParseError::Invalid(format!("^{rest} upper: {e}")))?;
629 let explicit_lower = wrote_full_version(rest);
632 Ok(Constraint::And(vec![
633 Constraint::Op {
634 op: CmpOp::Ge,
635 version: lower,
636 explicit_lower_bound: explicit_lower,
637 },
638 Constraint::Op {
639 op: CmpOp::Lt,
640 version: upper,
641 explicit_lower_bound: false,
642 },
643 ]))
644}
645
646fn parse_tilde(rest: &str) -> Result<Constraint, ParseError> {
647 let segs = split_partial_numeric(rest)?;
651 if segs.is_empty() {
652 return Err(ParseError::Invalid(rest.to_owned()));
653 }
654 let lower = Version::parse(rest)
655 .or_else(|_| Version::parse(&pad_partial(rest)))
656 .map_err(|e| ParseError::Invalid(format!("~{rest}: {e}")))?;
657 let upper_str = match segs.len() {
658 1 => {
659 let major: u32 = segs[0].parse().unwrap_or(0);
660 format!("{}.0.0.0", major + 1)
661 }
662 2 => {
663 let major: u32 = segs[0].parse().unwrap_or(0);
664 format!("{}.0.0.0", major + 1)
665 }
666 _ => {
667 let major: u32 = segs[0].parse().unwrap_or(0);
668 let minor: u32 = segs[1].parse().unwrap_or(0);
669 format!("{}.{}.0.0", major, minor + 1)
670 }
671 };
672 let upper = Version::parse(&upper_str)
673 .map_err(|e| ParseError::Invalid(format!("~{rest} upper: {e}")))?;
674 let explicit_lower = wrote_full_version(rest);
675 Ok(Constraint::And(vec![
676 Constraint::Op {
677 op: CmpOp::Ge,
678 version: lower,
679 explicit_lower_bound: explicit_lower,
680 },
681 Constraint::Op {
682 op: CmpOp::Lt,
683 version: upper,
684 explicit_lower_bound: false,
685 },
686 ]))
687}
688
689fn parse_wildcard(s: &str) -> Result<Constraint, ParseError> {
690 let parts: Vec<&str> = s.split('.').collect();
694 let first_wild = parts
696 .iter()
697 .position(|p| matches!(*p, "*" | "x" | "X"))
698 .ok_or_else(|| ParseError::Invalid(s.to_owned()))?;
699 let numeric_prefix: Vec<u32> = parts[..first_wild]
700 .iter()
701 .map(|p| p.parse().unwrap_or(0))
702 .collect();
703 let lower_segs: Vec<String> = numeric_prefix
704 .iter()
705 .map(u32::to_string)
706 .chain(std::iter::repeat_n(
707 "0".to_owned(),
708 4usize.saturating_sub(first_wild),
709 ))
710 .collect();
711 let lower = Version::parse(&lower_segs.join("."))
712 .map_err(|e| ParseError::Invalid(format!("{s} lower: {e}")))?;
713
714 let mut upper_segs = numeric_prefix.clone();
715 if first_wild == 0 {
716 return Ok(Constraint::Any);
718 }
719 *upper_segs.last_mut().unwrap() += 1;
720 let upper_str: String = upper_segs
721 .iter()
722 .map(u32::to_string)
723 .chain(std::iter::repeat_n(
724 "0".to_owned(),
725 4usize.saturating_sub(upper_segs.len()),
726 ))
727 .collect::<Vec<_>>()
728 .join(".");
729 let upper =
730 Version::parse(&upper_str).map_err(|e| ParseError::Invalid(format!("{s} upper: {e}")))?;
731 Ok(Constraint::And(vec![
732 Constraint::Op {
733 op: CmpOp::Ge,
734 version: lower,
735 explicit_lower_bound: false,
736 },
737 Constraint::Op {
738 op: CmpOp::Lt,
739 version: upper,
740 explicit_lower_bound: false,
741 },
742 ]))
743}
744
745fn parse_partial_or_exact(s: &str) -> Result<Constraint, ParseError> {
746 let cleaned = s.split_once('+').map_or(s, |(left, _)| left);
749 let segs = split_partial_numeric(cleaned)?;
750 if segs.len() >= 3 {
751 let v = Version::parse(cleaned).map_err(|e| ParseError::Invalid(format!("{s}: {e}")))?;
755 return Ok(Constraint::Op {
756 op: CmpOp::Eq,
757 version: v,
758 explicit_lower_bound: true,
759 });
760 }
761 let major: u32 = segs[0].parse().unwrap_or(0);
765 let lower_str = pad_partial(cleaned);
766 let lower =
767 Version::parse(&lower_str).map_err(|e| ParseError::Invalid(format!("{s} lower: {e}")))?;
768 let upper_str = if segs.len() == 1 {
769 format!("{}.0.0.0", major + 1)
770 } else {
771 let minor: u32 = segs[1].parse().unwrap_or(0);
772 format!("{}.{}.0.0", major, minor + 1)
773 };
774 let upper =
775 Version::parse(&upper_str).map_err(|e| ParseError::Invalid(format!("{s} upper: {e}")))?;
776 Ok(Constraint::And(vec![
777 Constraint::Op {
778 op: CmpOp::Ge,
779 version: lower,
780 explicit_lower_bound: false,
781 },
782 Constraint::Op {
783 op: CmpOp::Lt,
784 version: upper,
785 explicit_lower_bound: false,
786 },
787 ]))
788}
789
790fn wrote_full_version(s: &str) -> bool {
797 let s = s.trim();
798 let s = s.strip_prefix(['v', 'V']).unwrap_or(s);
799 let body = s.split_once(['-', '+']).map_or(s, |(left, _)| left);
800 let segs: Vec<&str> = body.split('.').filter(|p| !p.is_empty()).collect();
801 segs.len() >= 3 && segs.iter().all(|p| p.chars().all(|c| c.is_ascii_digit()))
802}
803
804fn split_partial_numeric(s: &str) -> Result<Vec<String>, ParseError> {
805 let cleaned = s
806 .strip_prefix(['v', 'V'])
807 .unwrap_or(s)
808 .split_once('-')
809 .map_or(s.strip_prefix(['v', 'V']).unwrap_or(s), |(left, _)| left);
810 let segs: Vec<String> = cleaned
811 .split('.')
812 .filter(|p| !p.is_empty())
813 .map(str::to_owned)
814 .collect();
815 if segs.is_empty() {
816 return Err(ParseError::Invalid(s.to_owned()));
817 }
818 if !segs[0].chars().all(|c| c.is_ascii_digit()) {
821 return Err(ParseError::Invalid(s.to_owned()));
822 }
823 Ok(segs)
824}
825
826fn split_partial_segments(s: &str) -> Vec<String> {
827 let stripped = s.strip_prefix(['v', 'V']).unwrap_or(s);
828 let cleaned = stripped.split_once('-').map_or(stripped, |(left, _)| left);
829 cleaned
830 .split('.')
831 .filter(|p| !p.is_empty())
832 .map(str::to_owned)
833 .collect()
834}
835
836fn pad_partial(s: &str) -> String {
839 let stripped = s.strip_prefix(['v', 'V']).unwrap_or(s);
840 let (body, tail) = match stripped.split_once('-') {
841 Some((b, t)) => (b, format!("-{t}")),
842 None => (stripped, String::new()),
843 };
844 let segs: Vec<&str> = body.split('.').filter(|p| !p.is_empty()).collect();
845 let mut padded: Vec<String> = segs.iter().map(|p| (*p).to_owned()).collect();
846 while padded.len() < 4 {
847 padded.push("0".to_owned());
848 }
849 format!("{}{}", padded.join("."), tail)
850}
851
852fn pad_segments_with(mut segs: Vec<String>, pad: &str, target: usize) -> Vec<String> {
853 while segs.len() < target {
854 segs.push(pad.to_owned());
855 }
856 segs
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862
863 #[test]
869 fn caret_zero_partial_versions_match_composer() {
870 let c = Constraint::parse("^0").unwrap();
872 assert!(c.matches(&Version::parse("0.0.1").unwrap()));
873 assert!(c.matches(&Version::parse("0.19.2").unwrap()));
874 assert!(!c.matches(&Version::parse("1.0.0").unwrap()));
875
876 let c = Constraint::parse("^0.0").unwrap();
878 assert!(c.matches(&Version::parse("0.0.9").unwrap()));
879 assert!(!c.matches(&Version::parse("0.1.0").unwrap()));
880
881 let c = Constraint::parse("^0.3").unwrap();
883 assert!(c.matches(&Version::parse("0.3.7").unwrap()));
884 assert!(!c.matches(&Version::parse("0.4.0").unwrap()));
885
886 let c = Constraint::parse("^0.0.3").unwrap();
888 assert!(c.matches(&Version::parse("0.0.3").unwrap()));
889 assert!(!c.matches(&Version::parse("0.0.4").unwrap()));
890
891 let c = Constraint::parse("^1").unwrap();
893 assert!(c.matches(&Version::parse("1.9.9").unwrap()));
894 assert!(!c.matches(&Version::parse("2.0.0").unwrap()));
895 }
896
897 #[test]
903 fn nx_dev_constraint_matches_same_branch_version() {
904 let c = Constraint::parse("3.x-dev").unwrap();
905 let v = Version::parse("3.x-dev").unwrap();
906 assert!(c.matches(&v), "got constraint {c:?}");
907 }
908
909 #[test]
910 fn nx_dev_constraint_rejects_other_major_branch() {
911 let c = Constraint::parse("3.x-dev").unwrap();
912 let other = Version::parse("2.x-dev").unwrap();
913 assert!(!c.matches(&other), "got constraint {c:?}");
914 }
915
916 #[test]
917 fn nx_dev_constraint_rejects_stable_release() {
918 let c = Constraint::parse("3.x-dev").unwrap();
921 let stable = Version::parse("3.0.0").unwrap();
922 assert!(!c.matches(&stable), "got constraint {c:?}");
923 }
924
925 #[test]
926 fn n_dot_x_dev_handles_two_segment_form() {
927 let c = Constraint::parse("1.x-dev").unwrap();
928 let v = Version::parse("1.x-dev").unwrap();
929 assert!(c.matches(&v));
930 }
931
932 #[test]
933 fn n_dot_m_dot_x_dev_handles_three_segment_form() {
934 let c = Constraint::parse("1.0.x-dev").unwrap();
936 let v = Version::parse("1.0.x-dev").unwrap();
937 assert!(c.matches(&v));
938 }
939
940 #[test]
941 fn dev_branch_constraint_matches_named_branch() {
942 let c = Constraint::parse("dev-main").unwrap();
946 let v = Version::parse("dev-main").unwrap();
947 assert!(c.matches(&v), "constraint {c:?} should match {v:?}");
948 }
949
950 #[test]
951 fn dev_branch_constraint_rejects_other_branches() {
952 let c = Constraint::parse("dev-main").unwrap();
953 let other = Version::parse("dev-feature-x").unwrap();
954 assert!(!c.matches(&other));
955 }
956
957 #[test]
958 fn name_dash_dev_suffix_is_synonym_for_dev_dash_name() {
959 let c = Constraint::parse("master-dev").unwrap();
963 let v = Version::parse("dev-master").unwrap();
964 assert!(c.matches(&v), "constraint {c:?} should match {v:?}");
965 }
966
967 #[test]
968 fn name_dash_dev_rejects_unsafe_chars() {
969 assert!(Constraint::parse("foo bar-dev").is_err());
972 assert!(Constraint::parse("1.0.0<1.0.5-dev").is_err());
973 }
974
975 #[test]
976 fn name_dash_dev_does_not_swallow_numeric_dev_versions() {
977 let c = Constraint::parse("1.0.0-dev").unwrap();
980 let v = Version::parse("1.0.0-dev").unwrap();
981 assert!(c.matches(&v));
982 }
983
984 fn lb(s: &str) -> (String, bool) {
987 let b = Constraint::parse(s).unwrap().lower_bound();
988 (b.version().to_owned(), b.is_inclusive())
989 }
990
991 #[test]
992 fn lower_bound_atomic_ops() {
993 assert_eq!(lb(">=8.1"), ("8.1.0.0".to_owned(), true));
994 assert_eq!(lb(">8.0"), ("8.0.0.0".to_owned(), false));
995 assert_eq!(lb("8.1.2"), ("8.1.2.0".to_owned(), true)); assert!(Constraint::parse("<8.0").unwrap().lower_bound().is_zero());
998 assert!(Constraint::parse("!=8.0").unwrap().lower_bound().is_zero());
999 assert!(Constraint::parse("*").unwrap().lower_bound().is_zero());
1000 }
1001
1002 #[test]
1003 fn lower_bound_caret_tilde_take_the_floor() {
1004 assert_eq!(lb("^8.1"), ("8.1.0.0".to_owned(), true));
1008 assert_eq!(lb("~8.2.3"), ("8.2.3.0".to_owned(), true));
1009 assert_eq!(lb("^8"), ("8.0.0.0".to_owned(), true));
1010 }
1011
1012 #[test]
1013 fn lower_bound_conjunction_takes_the_greater() {
1014 assert_eq!(lb(">=7.4 <8.3"), ("7.4.0.0".to_owned(), true));
1017 }
1018
1019 #[test]
1020 fn lower_bound_disjunction_takes_the_least() {
1021 assert_eq!(lb("^7.4 || ^8.0"), ("7.4.0.0".to_owned(), true));
1023 }
1024
1025 #[test]
1026 fn intersects_any_provider_matches_everything() {
1027 let provided = Constraint::parse("*").unwrap();
1029 assert!(provided.intersects(&Constraint::parse("^2.0").unwrap()));
1030 assert!(provided.intersects(&Constraint::parse("*").unwrap()));
1031 }
1032
1033 #[test]
1034 fn intersects_overlapping_and_disjoint_ranges() {
1035 let p = Constraint::parse("^1.0").unwrap();
1036 assert!(p.intersects(&Constraint::parse(">=1.5").unwrap())); assert!(!p.intersects(&Constraint::parse("^2.0").unwrap())); assert!(!p.intersects(&Constraint::parse(">=2.0").unwrap()));
1041 }
1042
1043 #[test]
1044 fn intersects_disjunction_member_overlap() {
1045 let provided = Constraint::parse("^1.0 || ^3.0").unwrap();
1046 assert!(provided.intersects(&Constraint::parse("^3.1").unwrap()));
1047 assert!(!provided.intersects(&Constraint::parse("^2.0").unwrap()));
1048 }
1049
1050 #[test]
1051 fn dev_branch_constraint_handles_slashed_branch_name() {
1052 let c = Constraint::parse("dev-fix/some-bug").unwrap();
1056 let v = Version::parse("dev-fix/some-bug").unwrap();
1057 assert!(c.matches(&v));
1058 }
1059}