From 174ec3f2de1ec3935fe0b41db16de9b93c2ac355 Mon Sep 17 00:00:00 2001 From: Lin Yihai Date: Mon, 8 Jul 2024 19:30:33 +0800 Subject: [PATCH] Add `matches_prerelease` to allow to match `Semver Compatible` prerelease --- src/eval.rs | 52 +++++++++++++-- src/lib.rs | 7 +- tests/test_matches_prerelease.rs | 109 +++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 tests/test_matches_prerelease.rs diff --git a/src/eval.rs b/src/eval.rs index e6e38949..18bb5547 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,13 +1,13 @@ -use crate::{Comparator, Op, Version, VersionReq}; +use crate::{Comparator, Op, Prerelease, Version, VersionReq}; -pub(crate) fn matches_req(req: &VersionReq, ver: &Version) -> bool { +pub(crate) fn matches_req(req: &VersionReq, ver: &Version, prerelease_matches: bool) -> bool { for cmp in &req.comparators { - if !matches_impl(cmp, ver) { + if !matches_impl(cmp, ver, prerelease_matches) { return false; } } - if ver.pre.is_empty() { + if ver.pre.is_empty() || prerelease_matches { return true; } @@ -24,10 +24,10 @@ pub(crate) fn matches_req(req: &VersionReq, ver: &Version) -> bool { } pub(crate) fn matches_comparator(cmp: &Comparator, ver: &Version) -> bool { - matches_impl(cmp, ver) && (ver.pre.is_empty() || pre_is_compatible(cmp, ver)) + matches_impl(cmp, ver, false) && (ver.pre.is_empty() || pre_is_compatible(cmp, ver)) } -fn matches_impl(cmp: &Comparator, ver: &Version) -> bool { +fn matches_impl(cmp: &Comparator, ver: &Version, prerelease_matches: bool) -> bool { match cmp.op { Op::Exact | Op::Wildcard => matches_exact(cmp, ver), Op::Greater => matches_greater(cmp, ver), @@ -35,7 +35,12 @@ fn matches_impl(cmp: &Comparator, ver: &Version) -> bool { Op::Less => matches_less(cmp, ver), Op::LessEq => matches_exact(cmp, ver) || matches_less(cmp, ver), Op::Tilde => matches_tilde(cmp, ver), - Op::Caret => matches_caret(cmp, ver), + Op::Caret => { + if prerelease_matches { + return matches_caret_prerelease(cmp, ver); + } + matches_caret(cmp, ver) + } #[cfg(no_non_exhaustive)] Op::__NonExhaustive => unreachable!(), } @@ -133,6 +138,39 @@ fn matches_tilde(cmp: &Comparator, ver: &Version) -> bool { ver.pre >= cmp.pre } +fn matches_caret_prerelease(cmp: &Comparator, ver: &Version) -> bool { + if matches_exact(cmp, ver) { + return true; + } + if !matches_greater(cmp, ver) { + return false; + } + + let mut pre_cmp = Comparator { + op: Op::LessEq, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + let Comparator { + major, + minor, + patch, + .. + } = *cmp; + + if major > 0 { + pre_cmp.major += 1; + pre_cmp.minor = Some(0); + pre_cmp.patch = Some(0); + } else if minor.is_some() && minor.unwrap() > 0 { + pre_cmp.minor = Some(minor.unwrap() + 1); + pre_cmp.patch = Some(0); + } else { + pre_cmp.patch = Some(patch.unwrap() + 1); + } + matches_less(&pre_cmp, ver) +} + fn matches_caret(cmp: &Comparator, ver: &Version) -> bool { if ver.major != cmp.major { return false; diff --git a/src/lib.rs b/src/lib.rs index de7b6906..42f1b09a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -521,7 +521,12 @@ impl VersionReq { /// Evaluate whether the given `Version` satisfies the version requirement /// described by `self`. pub fn matches(&self, version: &Version) -> bool { - eval::matches_req(self, version) + eval::matches_req(self, version, false) + } + + /// Simliar to [`Self::matches`], it allows to match any "Semver-compatible" pre-release version. + pub fn matches_prerelease(&self, version: &Version) -> bool { + eval::matches_req(self, version, true) } } diff --git a/tests/test_matches_prerelease.rs b/tests/test_matches_prerelease.rs new file mode 100644 index 00000000..9d2fef56 --- /dev/null +++ b/tests/test_matches_prerelease.rs @@ -0,0 +1,109 @@ +#![cfg(not(test_node_semver))] +#![allow( + clippy::missing_panics_doc, + clippy::shadow_unrelated, + clippy::toplevel_ref_arg, + clippy::wildcard_imports +)] + +mod util; + +use crate::util::*; + +use semver::VersionReq; +#[cfg_attr(not(no_track_caller), track_caller)] +fn assert_prerelease_match_all(req: &VersionReq, versions: &[&str]) { + for string in versions { + let parsed = version(string); + assert!(req.matches_prerelease(&parsed), "did not match {}", string); + } +} + +#[cfg_attr(not(no_track_caller), track_caller)] +fn assert_prerelease_match_none(req: &VersionReq, versions: &[&str]) { + for string in versions { + let parsed = version(string); + assert!(!req.matches_prerelease(&parsed), "matched {}", string); + } +} + +#[test] +fn patch_caret() { + let ref r = req("0.0.7"); + // Match >=0.0.7, <0.0.8-0, that's only match 0.0.7 + assert_prerelease_match_all(r, &["0.0.7"]); + // Not Match <0.7.0 + assert_prerelease_match_none(r, &["0.0.6", "0.0.7-0", "0.0.7-pre"]); + // Not Match >=0.8.0-0 + assert_prerelease_match_none(r, &["0.0.8-0", "0.0.8-pre.1", "0.0.8"]); + + let ref r = req("0.0.7-0"); + // Match >=0.0.7-0, <0.0.8-0 + assert_prerelease_match_all(r, &["0.0.7-0", "0.0.7-pre", "0.0.7"]); + // Not Match <0.7.0-0 + assert_prerelease_match_none(r, &["0.0.6-0", "0.0.6-pre", "0.0.6"]); + // Not Match >=0.0.8-0 + assert_prerelease_match_none(r, &["0.0.8-0", "0.0.8-pre.1", "0.0.8"]); +} + +#[test] +fn minor_caret() { + let ref r = req("0.8.0"); + // Match >=0.8.0, <0.9.0-0 + assert_prerelease_match_all(r, &["0.8.0", "0.8.1-0", "0.8.1-pre", "0.8.1"]); + // Not Match <0.8.0 + assert_prerelease_match_none(r, &["0.1.0", "0.8.0-pre"]); + // Not Match >=0.9.0 + + let ref r = req("0.8.0-0"); + // Match >=0.8.0-0, <0.9.0-0 + assert_prerelease_match_all(r, &["0.8.0-0", "0.8.0", "0.8.1-pre", "0.8.1"]); + // Not Match <0.8.0-0 + assert_prerelease_match_none(r, &["0.1.0", "0.7.7-pre", "0.7.7"]); + // Not Match >=0.9.0 + assert_prerelease_match_none(r, &["0.9.0-0", "0.9.0", "1.0.0-pre", "2.0.0-0"]); +} + +#[test] +fn major_caret() { + let ref r = req("1.2.3"); + // Match >=1.2.3, <2.0.0-0 + assert_prerelease_match_all(r, &["1.2.3", "1.2.4-0", "1.2.4", "1.8.8"]); + // Not Match < 1.2.3 + assert_prerelease_match_none(r, &["0.8.8", "1.2.0", "1.2.3-pre"]); + // Not Match >= 2.0.0-0 + assert_prerelease_match_none(r, &["2.0.0-0", "2.0.0", "2.0.1"]); + + let ref r = req("1.2.3-0"); + // Match >=1.2.3-0, <2.0.0-0 + assert_prerelease_match_all(r, &["1.2.3-pre", "1.2.3", "1.2.4-0", "1.2.4"]); + // Not Match < 1.2.3-0, >= 2.0.0-0 + assert_prerelease_match_none(r, &["1.2.0", "2.0.0-0", "2.0.0"]); +} + +#[test] +fn test_non_caret() { + let ref r: VersionReq = req(">=1.0.0"); + assert_prerelease_match_all(r, &["1.0.0", "1.0.1-pre", "2.0.0-0", "2.0.0"]); + assert_prerelease_match_none(r, &["0.9.9", "0.10.0", "1.0.0-pre.0"]); + + let ref r: VersionReq = req(">=1.0.0-pre.1"); + assert_prerelease_match_all(r, &["1.0.0-pre.1", "1.0.0", "1.0.1-pre", "2.0.0-0"]); + assert_prerelease_match_none(r, &["0.9.9", "0.10.0", "1.0.0-pre.0"]); + + let ref r: VersionReq = req("<=1.0.0"); + assert_prerelease_match_all(r, &["0.9.9", "0.10.0", "1.0.0-pre", "1.0.0"]); + assert_prerelease_match_none(r, &["1.0.1", "1.0.1-pre", "1.1.0", "2.0.0-0"]); + + let ref r: VersionReq = req("<=1.0.0-0"); + assert_prerelease_match_all(r, &["0.9.9", "0.1.0", "0.10.0", "1.0.0-0"]); + assert_prerelease_match_none(r, &["1.0.0-pre", "1.0.1-pre", "1.1.0", "2.0.0-0"]); + + let ref r: VersionReq = req(">1.0.0-0,<=1.1.0-0"); + assert_prerelease_match_all(r, &["1.0.0-pre", "1.0.0", "1.0.9", "1.1.0-0"]); + assert_prerelease_match_none(r, &["0.0.1", "1.0.0-0", "1.2.3-pre", "2.0.0"]); + + let ref r: VersionReq = req(">=1.0.0,<1.1.0-0"); + assert_prerelease_match_all(r, &["1.0.0", "1.0.0", "1.0.9"]); + assert_prerelease_match_none(r, &["0.0.1", "1.0.0-pre", "1.1.0-0", "1.1.0", "2.0.0"]); +}