From e62c271b3f16063bd68fdaedfd026e957175eea9 Mon Sep 17 00:00:00 2001 From: Lin Yihai Date: Wed, 21 Aug 2024 15:02:46 +0800 Subject: [PATCH 1/4] test: add more test for pre-release matches semantic --- src/cargo/util/semver_ext.rs | 8 +++ tests/testsuite/precise_pre_release.rs | 77 +++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/cargo/util/semver_ext.rs b/src/cargo/util/semver_ext.rs index 11b39408389..b2d8a92b671 100644 --- a/src/cargo/util/semver_ext.rs +++ b/src/cargo/util/semver_ext.rs @@ -237,6 +237,14 @@ mod matches_prerelease { (">=1.2.3-0, <1.2.3", "1.2.3-0", false), // upper bound semantic (">=1.2.3-0, <1.2.3", "1.2.3-1", false), // upper bound semantic (">=1.2.3-0, <1.2.3", "1.2.4-0", false), + // + ("1.2.3", "2.0.0-0", false), // upper bound semantics + ("=1.2.3-0", "1.2.3", false), + ("=1.2.3-0", "1.2.3-0", false), // bug, must be true + ("=1.2.3-0", "1.2.4", false), + (">=1.2.3-2, <1.2.3-4", "1.2.3-0", false), + (">=1.2.3-2, <1.2.3-4", "1.2.3-3", false), // bug, must be true + (">=1.2.3-2, <1.2.3-4", "1.2.3-5", false), // upper bound semantics ]; for (req, ver, expected) in cases { let version_req = req.parse().unwrap(); diff --git a/tests/testsuite/precise_pre_release.rs b/tests/testsuite/precise_pre_release.rs index 70e5e3fa5f0..610cc6d284d 100644 --- a/tests/testsuite/precise_pre_release.rs +++ b/tests/testsuite/precise_pre_release.rs @@ -76,13 +76,10 @@ fn update_pre_release() { } #[cargo_test] -fn update_pre_release_differ() { +fn pre_release_should_unmatched() { cargo_test_support::registry::init(); - for version in ["0.1.2", "0.1.2-pre.0", "0.1.2-pre.1"] { - cargo_test_support::registry::Package::new("my-dependency", version).publish(); - } - + cargo_test_support::registry::Package::new("my-dependency", "0.1.2").publish(); let p = project() .file( "Cargo.toml", @@ -95,7 +92,11 @@ fn update_pre_release_differ() { ) .file("src/lib.rs", "") .build(); + p.cargo("generate-lockfile").run(); + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2\"")); + cargo_test_support::registry::Package::new("my-dependency", "0.1.2-pre.0").publish(); p.cargo("update -p my-dependency --precise 0.1.2-pre.0 -Zunstable-options") .masquerade_as_nightly_cargo(&["precise-pre-release"]) .with_stderr_data(str![[r#" @@ -105,15 +106,75 @@ fn update_pre_release_differ() { "#]]) .run(); - p.cargo("update -p my-dependency --precise 0.1.2-pre.1 -Zunstable-options") + cargo_test_support::registry::Package::new("my-dependency", "0.2.0-0").publish(); + p.cargo("update -p my-dependency --precise 0.2.0-0 -Zunstable-options") + .masquerade_as_nightly_cargo(&["precise-pre-release"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] failed to select a version for the requirement `my-dependency = "^0.1.2"` +candidate versions found which didn't match: 0.2.0-0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `package v0.0.0 ([ROOT]/foo)` +if you are looking for the prerelease package it needs to be specified explicitly + my-dependency = { version = "0.2.0-0" } +perhaps a crate was updated and forgotten to be re-vendored? + +"#]]) + .run(); + + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2-pre.0\"")); +} + +#[cargo_test] +fn pre_release_should_matched() { + cargo_test_support::registry::init(); + + cargo_test_support::registry::Package::new("my-dependency", "0.1.2").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "package" + [dependencies] + my-dependency = "0.1.2" + "#, + ) + .file("src/lib.rs", "") + .build(); + p.cargo("generate-lockfile").run(); + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2\"")); + + // Test upgrade + // 0.1.3 is in the range, so it match + cargo_test_support::registry::Package::new("my-dependency", "0.1.3").publish(); + p.cargo("update -p my-dependency --precise 0.1.3 -Zunstable-options") + .masquerade_as_nightly_cargo(&["precise-pre-release"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] my-dependency v0.1.2 -> v0.1.3 + +"#]]) + .run(); + + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.3\"")); + + // Test downgrade + // v0.1.3-pre.1 is in the range, so it match + cargo_test_support::registry::Package::new("my-dependency", "0.1.3-pre.1").publish(); + p.cargo("update -p my-dependency --precise 0.1.3-pre.1 -Zunstable-options") .masquerade_as_nightly_cargo(&["precise-pre-release"]) .with_stderr_data(str![[r#" [UPDATING] `dummy-registry` index -[UPDATING] my-dependency v0.1.2-pre.0 -> v0.1.2-pre.1 +[DOWNGRADING] my-dependency v0.1.3 -> v0.1.3-pre.1 "#]]) .run(); let lockfile = p.read_lockfile(); - assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2-pre.1\"")); + assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.3-pre.1\"")); } From 7c4236ee890e35daf83e10b8a494459a1175757f Mon Sep 17 00:00:00 2001 From: Lin Yihai Date: Fri, 26 Jul 2024 16:43:30 +0800 Subject: [PATCH 2/4] feat: Add matches_prerelease semantic --- src/cargo/util/mod.rs | 1 + src/cargo/util/semver_eval_ext.rs | 520 +++++++++++++++++++++++++ src/cargo/util/semver_ext.rs | 55 +-- tests/testsuite/precise_pre_release.rs | 13 +- 4 files changed, 561 insertions(+), 28 deletions(-) create mode 100644 src/cargo/util/semver_eval_ext.rs diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index dcbf4f0323f..3236920da61 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -59,6 +59,7 @@ mod progress; mod queue; pub mod restricted_names; pub mod rustc; +mod semver_eval_ext; mod semver_ext; pub mod sqlite; pub mod style; diff --git a/src/cargo/util/semver_eval_ext.rs b/src/cargo/util/semver_eval_ext.rs new file mode 100644 index 00000000000..4fe913f3736 --- /dev/null +++ b/src/cargo/util/semver_eval_ext.rs @@ -0,0 +1,520 @@ +use semver::{Comparator, Op, Prerelease, Version, VersionReq}; + +pub(crate) fn matches_prerelease(req: &VersionReq, ver: &Version) -> bool { + for cmp in &req.comparators { + if !matches_prerelease_impl(cmp, ver) { + return false; + } + } + + true +} + +fn matches_prerelease_impl(cmp: &Comparator, ver: &Version) -> bool { + match cmp.op { + Op::Exact | Op::Wildcard => matches_exact_prerelease(cmp, ver), + Op::Greater => matches_greater(cmp, ver), + Op::GreaterEq => { + if matches_exact_prerelease(cmp, ver) { + return true; + } + matches_greater(cmp, ver) + } + Op::Less => matches_less(&fill_partial_req(cmp), ver), + Op::LessEq => { + if matches_exact_prerelease(cmp, ver) { + return true; + } + matches_less(&fill_partial_req(cmp), ver) + } + Op::Tilde => matches_tilde_prerelease(cmp, ver), + Op::Caret => matches_caret_prerelease(cmp, ver), + _ => unreachable!(), + } +} + +fn matches_exact(cmp: &Comparator, ver: &Version) -> bool { + if ver.major != cmp.major { + return false; + } + + if let Some(minor) = cmp.minor { + if ver.minor != minor { + return false; + } + } + + if let Some(patch) = cmp.patch { + if ver.patch != patch { + return false; + } + } + + ver.pre == cmp.pre +} + +fn matches_greater(cmp: &Comparator, ver: &Version) -> bool { + if ver.major != cmp.major { + return ver.major > cmp.major; + } + + match cmp.minor { + None => return false, + Some(minor) => { + if ver.minor != minor { + return ver.minor > minor; + } + } + } + + match cmp.patch { + None => return false, + Some(patch) => { + if ver.patch != patch { + return ver.patch > patch; + } + } + } + + ver.pre > cmp.pre +} + +fn matches_less(cmp: &Comparator, ver: &Version) -> bool { + if ver.major != cmp.major { + return ver.major < cmp.major; + } + + match cmp.minor { + None => return false, + Some(minor) => { + if ver.minor != minor { + return ver.minor < minor; + } + } + } + + match cmp.patch { + None => return false, + Some(patch) => { + if ver.patch != patch { + return ver.patch < patch; + } + } + } + + ver.pre < cmp.pre +} + +fn fill_partial_req(cmp: &Comparator) -> Comparator { + let mut cmp = cmp.clone(); + if cmp.minor.is_none() { + cmp.minor = Some(0); + cmp.patch = Some(0); + } else if cmp.patch.is_none() { + cmp.patch = Some(0); + } + cmp +} + +fn matches_exact_prerelease(cmp: &Comparator, ver: &Version) -> bool { + if matches_exact(cmp, ver) { + return true; + } + + // If the comparator has a prerelease tag like =3.0.0-alpha.24, + // then it shoud be only exactly match 3.0.0-alpha.24. + if !cmp.pre.is_empty() { + return false; + } + + if !matches_greater(&fill_partial_req(cmp), ver) { + return false; + } + + let mut upper = Comparator { + op: Op::Less, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + + match (upper.minor.is_some(), upper.patch.is_some()) { + (true, true) => { + upper.patch = Some(upper.patch.unwrap() + 1); + } + (true, false) => { + // Partial Exact VersionReq eg. =0.24 + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (false, false) => { + // Partial Exact VersionReq eg. =0 + upper.major += 1; + upper.minor = Some(0); + upper.patch = Some(0); + } + _ => {} + } + + matches_less(&upper, ver) +} + +fn matches_tilde_prerelease(cmp: &Comparator, ver: &Version) -> bool { + if matches_exact(cmp, ver) { + return true; + } + + if !matches_greater(&fill_partial_req(cmp), ver) { + return false; + } + + let mut upper = Comparator { + op: Op::Less, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + + match (upper.minor.is_some(), upper.patch.is_some()) { + (true, _) => { + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (false, false) => { + upper.major += 1; + upper.minor = Some(0); + upper.patch = Some(0); + } + _ => {} + } + + matches_less(&upper, ver) +} + +fn matches_caret_prerelease(cmp: &Comparator, ver: &Version) -> bool { + if matches_exact(cmp, ver) { + return true; + } + + if !matches_greater(&fill_partial_req(cmp), ver) { + return false; + } + + let mut upper = Comparator { + op: Op::Less, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + + match ( + upper.major > 0, + upper.minor.is_some(), + upper.patch.is_some(), + ) { + (true, _, _) | (_, false, false) => { + upper.major += 1; + upper.minor = Some(0); + upper.patch = Some(0); + } + (_, true, false) => { + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (_, true, _) if upper.minor.unwrap() > 0 => { + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (_, true, _) if upper.minor.unwrap() == 0 => { + if upper.patch.is_none() { + upper.patch = Some(1); + } else { + upper.patch = Some(upper.patch.unwrap() + 1); + } + } + _ => {} + } + + matches_less(&upper, ver) +} + +#[cfg(test)] +mod matches_prerelease_semantic { + use crate::util::semver_ext::VersionReqExt; + use semver::{Version, VersionReq}; + + fn assert_match_all(req: &VersionReq, versions: &[&str]) { + for string in versions { + let parsed = Version::parse(string).unwrap(); + assert!( + req.matches_prerelease(&parsed), + "{} did not match {}", + req, + string, + ); + } + } + + fn assert_match_none(req: &VersionReq, versions: &[&str]) { + for string in versions { + let parsed = Version::parse(string).unwrap(); + assert!( + !req.matches_prerelease(&parsed), + "{} matched {}", + req, + string + ); + } + } + + pub(super) fn req(text: &str) -> VersionReq { + VersionReq::parse(text).unwrap() + } + + #[test] + fn test_exact() { + // =I.J.K-pre only match I.J.K-pre + let ref r = req("=4.2.1-0"); + // Only exactly match 4.2.1-0 + assert_match_all(r, &["4.2.1-0"]); + // Not match others + assert_match_none(r, &["1.2.3", "4.2.0", "4.2.1-1", "4.2.2"]); + + // =I.J.K equivalent to >=I.J.K, =4.2.1, <4.2.2-0")] { + assert_match_all(r, &["4.2.1"]); + assert_match_none(r, &["1.2.3", "4.2.1-0", "4.2.2-0", "4.2.2"]); + } + + // =I.J equivalent to >=I.J.0, =4.2.0, <4.3.0-0")] { + assert_match_all(r, &["4.2.0", "4.2.1", "4.2.9"]); + assert_match_none(r, &["0.0.1", "2.1.2-0", "4.2.0-0"]); + assert_match_none(r, &["4.3.0-0", "4.3.0", "5.0.0-0", "5.0.0"]); + } + + // =I equivalent to >=I.0.0, <(I+1).0.0-0 + for r in &[req("=4"), req(">=4.0.0, <5.0.0-0")] { + assert_match_all(r, &["4.0.0", "4.2.1", "4.2.4-0", "4.9.9"]); + assert_match_none(r, &["0.0.1", "2.1.2-0", "4.0.0-0"]); + assert_match_none(r, &["5.0.0-0", "5.0.0", "5.0.1"]); + } + } + + #[test] + fn test_greater_eq() { + // >=I.J.K-0 + let ref r = req(">=4.2.1-0"); + assert_match_all(r, &["4.2.1-0", "4.2.1", "5.0.0"]); + assert_match_none(r, &["0.0.0", "1.2.3"]); + + // >=I.J.K + let ref r = req(">=4.2.1"); + assert_match_all(r, &["4.2.1", "5.0.0"]); + assert_match_none(r, &["0.0.0", "4.2.1-0"]); + + // >=I.J equivalent to >=I.J.0 + for r in &[req(">=4.2"), req(">=4.2.0")] { + assert_match_all(r, &["4.2.1-0", "4.2.0", "4.3.0"]); + assert_match_none(r, &["0.0.0", "4.1.1", "4.2.0-0"]); + } + + // >=I equivalent to >=I.0.0 + for r in &[req(">=4"), req(">=4.0.0")] { + assert_match_all(r, &["4.0.0", "4.1.0-1", "5.0.0"]); + assert_match_none(r, &["0.0.0", "1.2.3", "4.0.0-0"]); + } + } + + #[test] + fn test_less() { + // 0) — equivalent to >=I.J.K-0, <(I+1).0.0-0 + for r in &[req("^1.2.3-0"), req(">=1.2.3-0, <2.0.0-0")] { + assert_match_all(r, &["1.2.3-0", "1.2.3-1", "1.2.3", "1.9.9"]); + assert_match_none(r, &["0.0.9", "1.1.1-0", "2.0.0-0", "2.1.1"]); + } + + // ^I.J.K (for I>0) — equivalent to >=I.J.K, <(I+1).0.0-0 + for r in &[req("^1.2.3"), req(">=1.2.3, <2.0.0-0")] { + assert_match_all(r, &["1.2.3", "1.9.9"]); + assert_match_none( + r, + &["0.0.9", "1.1.1-0", "1.2.3-0", "1.2.3-1", "2.0.0-0", "2.1.1"], + ); + } + + // ^0.J.K-0 (for J>0) — equivalent to >=0.J.K-0, <0.(J+1).0-0 + for r in &[req("^0.2.3-0"), req(">=0.2.3-0, <0.3.0-0")] { + assert_match_all(r, &["0.2.3-0", "0.2.3", "0.2.9-0", "0.2.9"]); + assert_match_none(r, &["0.0.9", "0.3.0-0", "0.3.11", "1.1.1"]); + } + + // ^0.J.K (for J>0) — equivalent to >=0.J.K-0, <0.(J+1).0-0 + for r in &[req("^0.2.3"), req(">=0.2.3, <0.3.0-0")] { + assert_match_all(r, &["0.2.3", "0.2.9-0", "0.2.9"]); + assert_match_none(r, &["0.0.9", "0.2.3-0", "0.3.0-0", "0.3.11", "1.1.1"]); + } + + // ^0.0.K-0 — equivalent to >=0.0.K-0, <0.0.(K+1)-0 + for r in &[req("^0.0.3-0"), req(">=0.0.3-0, <0.1.0-0")] { + assert_match_all(r, &["0.0.3-0", "0.0.3-1", "0.0.3"]); + assert_match_none(r, &["0.0.1", "0.3.0-0", "0.4.0-0", "1.1.1"]); + } + + // ^0.0.K — equivalent to >=0.0.K, <0.0.(K+1)-0 + for r in &[req("^0.0.3"), req(">=0.0.3, <0.1.0-0")] { + assert_match_all(r, &["0.0.3"]); + assert_match_none( + r, + &["0.0.1", "0.0.3-0", "0.3.0-0", "0.0.3-1", "0.4.0-0", "1.1.1"], + ); + } + + // ^I.J (for I>0 or J>0) — equivalent to >=I.J.0, <(I+1).0.0-0) + for r in &[req("^1.2"), req(">=1.2.0, <2.0.0-0")] { + assert_match_all(r, &["1.2.0", "1.9.0-0", "1.9.9"]); + assert_match_none(r, &["0.0.1", "0.0.4-0", "1.2.0-0", "2.0.0-0", "4.0.1"]); + } + + // ^0.0 — equivalent to >=0.0.0, <0.1.0-0 + for r in &[req("^0.0"), req(">=0.0.0, <0.1.0-0")] { + assert_match_all(r, &["0.0.0", "0.0.1", "0.0.4-0"]); + assert_match_none(r, &["0.0.0-0", "0.1.0-0", "0.1.0", "1.1.1"]); + } + + // ^I — equivalent to >=I.0.0, <(I+1).0.0-0 + for r in &[req("^1"), req(">=1.0.0, <2.0.0-0")] { + assert_match_all(r, &["1.0.0", "1.0.1"]); + assert_match_none(r, &["0.1.0-0", "0.1.0", "1.0.0-0", "2.0.0-0", "3.1.2"]); + } + } + + #[test] + fn test_wildcard() { + // I.J.* — equivalent to =I.J + // + // =I.J equivalent to >=I.J.0, = 4.2.0, < 4.3.0-0 + assert_match_all(r, &["4.2.0", "4.2.1", "4.2.9"]); + // Not Match < 4.2.0 + assert_match_none(r, &["0.0.1", "2.1.2-0", "4.2.0-0"]); + // Not Match >= 4.3.0-0 + assert_match_none(r, &["4.3.0-0", "4.3.0", "5.0.0", "5.0.1"]); + } + + // I.* or I.*.* — equivalent to =I + // + // =I equivalent to >=I.0.0, <(I+1).0.0-0 + for r in &[req("4.*"), req("4.*.*"), req("=4")] { + // Match >= 4.0.0, < 5.0.0-0 + assert_match_all(r, &["4.0.0", "4.2.1", "4.9.9"]); + // Not Match < 4.0.0 + assert_match_none(r, &["0.0.1", "2.1.2-0", "4.0.0-0"]); + // Not Match >= 5.0.0-0 + assert_match_none(r, &["5.0.0-0", "5.0.0", "5.0.1"]); + } + } + + #[test] + fn test_greater() { + // >I.J.K-0 + let ref r = req(">4.2.1-0"); + assert_match_all(r, &["4.2.1", "4.2.2", "5.0.0"]); + assert_match_none(r, &["0.0.0", "4.2.1-0"]); + + // >I.J.K + let ref r = req(">4.2.1"); + assert_match_all(r, &["4.2.2", "5.0.0-0", "5.0.0"]); + assert_match_none(r, &["0.0.0", "4.2.1-0", "4.2.1"]); + + // >I.J equivalent to >=I.(J+1).0-0 + for r in &[req(">4.2"), req(">=4.3.0-0")] { + assert_match_all(r, &["4.3.0-0", "4.3.0", "5.0.0"]); + assert_match_none(r, &["0.0.0", "4.2.1"]); + } + + // >I equivalent to >=(I+1).0.0-0 + for r in &[req(">4"), req(">=5.0.0-0")] { + assert_match_all(r, &["5.0.0-0", "5.0.0"]); + assert_match_none(r, &["0.0.0", "4.2.1"]); + } + } + + #[test] + fn test_less_eq() { + // <=I.J.K + let ref r = req("<=4.2.1"); + assert_match_all(r, &["0.0.0", "4.2.1-0", "4.2.1"]); + assert_match_none(r, &["4.2.2", "5.0.0-0", "5.0.0"]); + // <=I.J.K-0 + let ref r = req("<=4.2.1-0"); + assert_match_all(r, &["0.0.0", "4.2.1-0"]); + assert_match_none(r, &["4.2.1", "4.2.2", "5.0.0-0", "5.0.0"]); + + // <=I.J equivalent to =I.J.K-0, = 1.2.3-0, < 1.3.0-0")] { + assert_match_all(r, &["1.2.3-0", "1.2.3", "1.2.4-0", "1.2.4"]); + assert_match_none(r, &["0.0.1", "1.1.0-0"]); + assert_match_none(r, &["1.3.0-0", "1.3.0", "1.3.1", "2.0.0"]); + } + + // ~I.J.K — equivalent to >=I.J.K, = 1.2.3, < 1.3.0-0")] { + assert_match_all(r, &["1.2.3", "1.2.4-0", "1.2.4"]); + assert_match_none(r, &["0.0.1", "1.1.0-0", "1.2.3-0"]); + assert_match_none(r, &["1.3.0-0", "1.3.0", "1.3.1", "2.0.0"]); + } + + // ~I.J — equivalent to >=I.J.0, =0.24.0, <0.25.0-0")] { + assert_match_all(r, &["0.24.0", "0.24.1-0", "0.24.1", "0.24.9"]); + assert_match_none(r, &["0.0.1", "0.9.9", "0.24.0-0"]); + assert_match_none(r, &["0.25.0-0", "1.1.0", "1.2.3", "2.0.0"]); + } + + // ~I — >=I.0.0, <(I+1).0.0-0 + for r in &[req("~1"), req(">=1.0.0, <2.0.0-0")] { + assert_match_all(r, &["1.0.0", "1.1.0-0", "1.1.0"]); + assert_match_none(r, &["0.0.1", "0.9.9", "1.0.0-0"]); + assert_match_none(r, &["2.0.0-0", "2.0.0", "2.0.1"]); + } + } +} diff --git a/src/cargo/util/semver_ext.rs b/src/cargo/util/semver_ext.rs index b2d8a92b671..7bd148c3007 100644 --- a/src/cargo/util/semver_ext.rs +++ b/src/cargo/util/semver_ext.rs @@ -1,6 +1,6 @@ -use std::fmt::{self, Display}; - +use super::semver_eval_ext; use semver::{Comparator, Op, Version, VersionReq}; +use std::fmt::{self, Display}; pub trait VersionExt { fn is_prerelease(&self) -> bool; @@ -26,6 +26,16 @@ impl VersionExt for Version { } } +pub trait VersionReqExt { + fn matches_prerelease(&self, version: &Version) -> bool; +} + +impl VersionReqExt for VersionReq { + fn matches_prerelease(&self, version: &Version) -> bool { + semver_eval_ext::matches_prerelease(self, version) + } +} + #[derive(PartialEq, Eq, Hash, Clone, Debug)] pub enum OptVersionReq { Any, @@ -111,21 +121,14 @@ impl OptVersionReq { } } - /// An interim approach allows to update to SemVer-Compatible prerelease version. + /// Allows to match pre-release in SemVer-Compatible way. + /// See [`semver_eval_ext`] for matches_prerelease semantics. pub fn matches_prerelease(&self, version: &Version) -> bool { - // Others Non `OptVersionReq::Req` have their own implementation. - if !matches!(self, OptVersionReq::Req(_)) { + if let OptVersionReq::Req(req) = self { + return req.matches_prerelease(version); + } else { return self.matches(version); } - - // TODO: In the future we have a prerelease semantic to be implemented. - if version.is_prerelease() { - let mut version: Version = version.clone(); - // Ignores the Prerelease tag to unlock the limit of non prerelease unpdate to prerelease. - version.pre = semver::Prerelease::EMPTY; - return self.matches(&version); - } - self.matches(version) } pub fn matches(&self, version: &Version) -> bool { @@ -210,12 +213,12 @@ mod matches_prerelease { // https://rust-lang.github.io/rfcs/3493-precise-pre-release-cargo-update.html#version-ranges-with-pre-release-upper-bounds let cases = [ // - ("1.2.3", "1.2.3-0", true), // bug, must be false - ("1.2.3", "1.2.3-1", true), // bug, must be false + ("1.2.3", "1.2.3-0", false), + ("1.2.3", "1.2.3-1", false), ("1.2.3", "1.2.4-0", true), // - (">=1.2.3", "1.2.3-0", true), // bug, must be false - (">=1.2.3", "1.2.3-1", true), // bug, must be false + (">=1.2.3", "1.2.3-0", false), + (">=1.2.3", "1.2.3-1", false), (">=1.2.3", "1.2.4-0", true), // (">1.2.3", "1.2.3-0", false), @@ -224,26 +227,26 @@ mod matches_prerelease { // (">1.2.3, <1.2.4", "1.2.3-0", false), (">1.2.3, <1.2.4", "1.2.3-1", false), - (">1.2.3, <1.2.4", "1.2.4-0", false), // upper bound semantic + (">1.2.3, <1.2.4", "1.2.4-0", true), // upper bound semantic // - (">=1.2.3, <1.2.4", "1.2.3-0", true), // bug, must be false - (">=1.2.3, <1.2.4", "1.2.3-1", true), // bug, must be false - (">=1.2.3, <1.2.4", "1.2.4-0", false), // upper bound semantic + (">=1.2.3, <1.2.4", "1.2.3-0", false), + (">=1.2.3, <1.2.4", "1.2.3-1", false), + (">=1.2.3, <1.2.4", "1.2.4-0", true), // upper bound semantic // (">1.2.3, <=1.2.4", "1.2.3-0", false), (">1.2.3, <=1.2.4", "1.2.3-1", false), (">1.2.3, <=1.2.4", "1.2.4-0", true), // - (">=1.2.3-0, <1.2.3", "1.2.3-0", false), // upper bound semantic - (">=1.2.3-0, <1.2.3", "1.2.3-1", false), // upper bound semantic + (">=1.2.3-0, <1.2.3", "1.2.3-0", true), // upper bound semantic + (">=1.2.3-0, <1.2.3", "1.2.3-1", true), // upper bound semantic (">=1.2.3-0, <1.2.3", "1.2.4-0", false), // ("1.2.3", "2.0.0-0", false), // upper bound semantics ("=1.2.3-0", "1.2.3", false), - ("=1.2.3-0", "1.2.3-0", false), // bug, must be true + ("=1.2.3-0", "1.2.3-0", true), ("=1.2.3-0", "1.2.4", false), (">=1.2.3-2, <1.2.3-4", "1.2.3-0", false), - (">=1.2.3-2, <1.2.3-4", "1.2.3-3", false), // bug, must be true + (">=1.2.3-2, <1.2.3-4", "1.2.3-3", true), (">=1.2.3-2, <1.2.3-4", "1.2.3-5", false), // upper bound semantics ]; for (req, ver, expected) in cases { diff --git a/tests/testsuite/precise_pre_release.rs b/tests/testsuite/precise_pre_release.rs index 610cc6d284d..6c6b6cc55ff 100644 --- a/tests/testsuite/precise_pre_release.rs +++ b/tests/testsuite/precise_pre_release.rs @@ -96,17 +96,26 @@ fn pre_release_should_unmatched() { let lockfile = p.read_lockfile(); assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2\"")); + // 0.1.2-pre.0 < 0.1.2 so it doesn't match cargo_test_support::registry::Package::new("my-dependency", "0.1.2-pre.0").publish(); p.cargo("update -p my-dependency --precise 0.1.2-pre.0 -Zunstable-options") .masquerade_as_nightly_cargo(&["precise-pre-release"]) + .with_status(101) .with_stderr_data(str![[r#" [UPDATING] `dummy-registry` index -[DOWNGRADING] my-dependency v0.1.2 -> v0.1.2-pre.0 +[ERROR] failed to select a version for the requirement `my-dependency = "^0.1.2"` +candidate versions found which didn't match: 0.1.2-pre.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `package v0.0.0 ([ROOT]/foo)` +if you are looking for the prerelease package it needs to be specified explicitly + my-dependency = { version = "0.1.2-pre.0" } +perhaps a crate was updated and forgotten to be re-vendored? "#]]) .run(); cargo_test_support::registry::Package::new("my-dependency", "0.2.0-0").publish(); + // 0.2.0-0 is the upper bound we exclude, so it doesn't match p.cargo("update -p my-dependency --precise 0.2.0-0 -Zunstable-options") .masquerade_as_nightly_cargo(&["precise-pre-release"]) .with_status(101) @@ -124,7 +133,7 @@ perhaps a crate was updated and forgotten to be re-vendored? .run(); let lockfile = p.read_lockfile(); - assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2-pre.0\"")); + assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2\"")); } #[cargo_test] From c4d7d7428fdd9e1fc82ebb22ad04db50212b8d53 Mon Sep 17 00:00:00 2001 From: Lin Yihai Date: Mon, 29 Jul 2024 10:36:47 +0800 Subject: [PATCH 3/4] doc: Add mod level documents --- src/cargo/util/semver_eval_ext.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/cargo/util/semver_eval_ext.rs b/src/cargo/util/semver_eval_ext.rs index 4fe913f3736..b34ba486ab2 100644 --- a/src/cargo/util/semver_eval_ext.rs +++ b/src/cargo/util/semver_eval_ext.rs @@ -1,3 +1,9 @@ +//! Extend `semver::VersionReq` with [`matches_prerelease`] which doesn't preclude pre-releases by default. +//! +//! Please refer to the semantic proposal, see [RFC 3493]. +//! +//! [RFC 3493]: https://rust-lang.github.io/rfcs/3493-precise-pre-release-cargo-update.html + use semver::{Comparator, Op, Prerelease, Version, VersionReq}; pub(crate) fn matches_prerelease(req: &VersionReq, ver: &Version) -> bool { @@ -33,6 +39,7 @@ fn matches_prerelease_impl(cmp: &Comparator, ver: &Version) -> bool { } } +// See https://github.com/dtolnay/semver/blob/69efd3cc770ead273a06ad1788477b3092996d29/src/eval.rs#L44-L62 fn matches_exact(cmp: &Comparator, ver: &Version) -> bool { if ver.major != cmp.major { return false; @@ -53,6 +60,7 @@ fn matches_exact(cmp: &Comparator, ver: &Version) -> bool { ver.pre == cmp.pre } +// See https://github.com/dtolnay/semver/blob/69efd3cc770ead273a06ad1788477b3092996d29/src/eval.rs#L64-L88 fn matches_greater(cmp: &Comparator, ver: &Version) -> bool { if ver.major != cmp.major { return ver.major > cmp.major; @@ -79,6 +87,7 @@ fn matches_greater(cmp: &Comparator, ver: &Version) -> bool { ver.pre > cmp.pre } +// See https://github.com/dtolnay/semver/blob/69efd3cc770ead273a06ad1788477b3092996d29/src/eval.rs#L90-L114 fn matches_less(cmp: &Comparator, ver: &Version) -> bool { if ver.major != cmp.major { return ver.major < cmp.major; From bdf19e054477bb2c7a1d0fc3c1a9b1e1fd814a9f Mon Sep 17 00:00:00 2001 From: Lin Yihai Date: Tue, 30 Jul 2024 17:14:09 +0800 Subject: [PATCH 4/4] feat: add upper bound semantic for `Less Op` --- src/cargo/util/semver_eval_ext.rs | 106 ++++++++++++++++++++++++------ src/cargo/util/semver_ext.rs | 4 +- 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/src/cargo/util/semver_eval_ext.rs b/src/cargo/util/semver_eval_ext.rs index b34ba486ab2..a402f454412 100644 --- a/src/cargo/util/semver_eval_ext.rs +++ b/src/cargo/util/semver_eval_ext.rs @@ -7,8 +7,16 @@ use semver::{Comparator, Op, Prerelease, Version, VersionReq}; pub(crate) fn matches_prerelease(req: &VersionReq, ver: &Version) -> bool { + // Whether there are pre release version can be as lower bound + let lower_bound_prerelease = &req.comparators.iter().any(|cmp| { + if matches!(cmp.op, Op::Greater | Op::GreaterEq) && !cmp.pre.is_empty() { + true + } else { + false + } + }); for cmp in &req.comparators { - if !matches_prerelease_impl(cmp, ver) { + if !matches_prerelease_impl(cmp, ver, lower_bound_prerelease) { return false; } } @@ -16,7 +24,7 @@ pub(crate) fn matches_prerelease(req: &VersionReq, ver: &Version) -> bool { true } -fn matches_prerelease_impl(cmp: &Comparator, ver: &Version) -> bool { +fn matches_prerelease_impl(cmp: &Comparator, ver: &Version, lower_bound_prerelease: &bool) -> bool { match cmp.op { Op::Exact | Op::Wildcard => matches_exact_prerelease(cmp, ver), Op::Greater => matches_greater(cmp, ver), @@ -26,7 +34,13 @@ fn matches_prerelease_impl(cmp: &Comparator, ver: &Version) -> bool { } matches_greater(cmp, ver) } - Op::Less => matches_less(&fill_partial_req(cmp), ver), + Op::Less => { + if *lower_bound_prerelease { + matches_less(&fill_partial_req(cmp), ver) + } else { + matches_less(&fill_partial_req_include_pre(cmp), ver) + } + } Op::LessEq => { if matches_exact_prerelease(cmp, ver) { return true; @@ -125,6 +139,21 @@ fn fill_partial_req(cmp: &Comparator) -> Comparator { cmp } +fn fill_partial_req_include_pre(cmp: &Comparator) -> Comparator { + let mut cmp = cmp.clone(); + if cmp.minor.is_none() { + cmp.minor = Some(0); + cmp.patch = Some(0); + cmp.pre = Prerelease::new("0").unwrap(); + } else if cmp.patch.is_none() { + cmp.patch = Some(0); + } + if cmp.pre.is_empty() { + cmp.pre = Prerelease::new("0").unwrap(); + } + cmp +} + fn matches_exact_prerelease(cmp: &Comparator, ver: &Version) -> bool { if matches_exact(cmp, ver) { return true; @@ -334,29 +363,66 @@ mod matches_prerelease_semantic { #[test] fn test_less() { - // 1.2.3, <2"), + req(">1.2.3, <2.0"), + req(">1.2.3, <2.0.0"), + req(">=1.2.3, <2.0.0"), + req(">1.2.3, <2.0.0-0"), + ] { + assert_match_all(r, &["1.2.4", "1.9.9"]); + assert_match_none(r, &["2.0.0-0", "2.0.0", "2.1.2"]); + } + + // Lower bound has prerelase tag, so upper bound doesn't change. + for r in &[ + req(">1.2.3-0, <2"), + req(">1.2.3-0, <2.0"), + req(">1.2.3-0, <2.0.0"), + req(">=1.2.3-0, <2.0.0"), + ] { + assert_match_all(r, &["1.2.4", "1.9.9", "2.0.0-0"]); + assert_match_none(r, &["2.0.0", "2.1.2"]); + } + + for r in &[ + req(">=2.0.0-0, <2"), + req(">=2.0.0-0, <2.0"), + req(">=2.0.0-0, <2.0.0"), + ] { + assert_match_all(r, &["2.0.0-0", "2.0.0-11"]); + assert_match_none(r, &["0.0.9", "2.0.0"]); + } + + // There is no intersection between lower bound and upper bound, in this case nothing matches + let ref r = req(">5.0.0, <2.0.0"); + assert_match_none(r, &["1.2.3", "3.0.0", "6.0.0"]); + let ref r = req(">5.0.0-0, <2.0.0"); + assert_match_none(r, &["1.2.3", "3.0.0", "6.0.0"]); + } + #[test] fn test_caret() { // ^I.J.K.0 (for I>0) — equivalent to >=I.J.K-0, <(I+1).0.0-0 diff --git a/src/cargo/util/semver_ext.rs b/src/cargo/util/semver_ext.rs index 7bd148c3007..86bb4199a99 100644 --- a/src/cargo/util/semver_ext.rs +++ b/src/cargo/util/semver_ext.rs @@ -227,11 +227,11 @@ mod matches_prerelease { // (">1.2.3, <1.2.4", "1.2.3-0", false), (">1.2.3, <1.2.4", "1.2.3-1", false), - (">1.2.3, <1.2.4", "1.2.4-0", true), // upper bound semantic + (">1.2.3, <1.2.4", "1.2.4-0", false), // upper bound semantic // (">=1.2.3, <1.2.4", "1.2.3-0", false), (">=1.2.3, <1.2.4", "1.2.3-1", false), - (">=1.2.3, <1.2.4", "1.2.4-0", true), // upper bound semantic + (">=1.2.3, <1.2.4", "1.2.4-0", false), // upper bound semantic // (">1.2.3, <=1.2.4", "1.2.3-0", false), (">1.2.3, <=1.2.4", "1.2.3-1", false),