Auto merge of #14305 - linyihai:matches-prerelease-semantic, r=epage

feat: Add matches_prerelease semantic

### What does this PR try to resolve?
One implementaion for https://github.com/rust-lang/cargo/issues/13290

Thanks to `@Eh2406` feedback, a working version came out, and I think it should be able to test under the nightly feature.

This PR proposes a `matches_prerelease semantic`.

| req              | matches             | matches_prerelease     | matches_prerelease_mirror_node [<sub>2<sub>](#mirror-node) |
|------------------|---------------------|------------------------| ----------------------------------|
| `Op::Exact`      |                     |                        |                                   |
| =I.J.K           | =I.J.K              | >=I.J.K, <I.J.(K+1)-0  | >=I.J.K, <I.J.(K+1)-0             |
| =I.J             | >=I.J.0, <I.(J+1).0 | >=I.J.0, <I.(J+1).0-0  | >=I.J.0-0, <I.(J+1).0-0           |
| =I               | >=I.0.0, <(I+1).0.0 | >=I.0.0, <(I+1).0.0-0  | >=I.0.0-0, <(I+1).0.0-0           |
| `Op::Greater`    |                     |                        |                                   |
| >I.J.K           | >I.J.K              | >I.J.K                 | >I.J.K                            |
| >I.J             | >=I.(J+1).0         | >=I.(J+1).0-0          | >=I.(J+1).0-0                     |
| >I               | >=(I+1).0.0         | >=(I+1).0.0-0          | >=(I+1).0.0-0                     |
| `Op::GreaterEq`  |                     |                        |                                   |
| >=I.J.K          | >=I.J.K             | >=I.J.K                | >=I.J.K                           |
| >=I.J            | >=I.J.0             | >=I.J.0                | >=I.J.0-0                         |
| >=I              | >=I.0.0             | >=I.0.0                | >=I.0.0-0                         |
| `Op::Less`       |                     |                        |                                   |
| <I.J.K           | <I.J.K              | <I.J.K or I.J.K-0 depends [<sub>1<sub>](#op-less) | <I.J.K |
| <I.J             | <I.J.0              | <I.J.0-0               | <I.J.0-0                          |
| <I               | <I.0.0              | <I.0.0-0               | <I.0.0-0                          |
| `Op::LessEq`     |                     |                        |                                   |
| <=I.J.K          | <=I.J.K             | <=I.J.K                | <=I.J.K                           |
| <=I.J            | <I.(J+1).0          | <I.(J+1).0-0           | <I.(J+1).0-0                      |
| <=I              | <(I+1).0.0          | <(I+1).0.0-0           | <(I+1).0.0-0                      |
| `Op::Tilde`      |                     |                        |                                   |
| ~I.J.K           | >=I.J.K, <I.(J+1).0 | >=I.J.K, <I.(J+1).0-0  | >=I.J.K, <I.(J+1).0-0             |
| ~I.J             | =I.J                | >=I.J.0, <I.(J+1).0-0  | >=I.J.0, <I.(J+1).0-0             |
| ~I               | =I                  | >=I.0.0, <(I+1).0.0-0  | >=I.0.0, <(I+1).0.0-0             |
| `Op::Caret`      |                     |                        |                                   |
| ^I.J.K (for I>0) | >=I.J.K, <(I+1).0.0 | >=I.J.K, <(I+1).0.0-0  | >=I.J.K, <(I+1).0.0-0             |
| ^0.J.K (for J>0) | >=0.J.K, <0.(J+1).0 | >=0.J.K,  <0.(J+1).0-0 | >=0.J.K-0, <0.(J+1).0-0           |
| ^0.0.K           | =0.0.K              | >=0.0.K,  <0.0.(K+1)-0 | >=0.J.K-0, <0.(J+1).0-0           |
| ^I.J             | ^I.J.0              | >=I.J.0,  <(I+1).0.0-0 | >=I.J.0-0, <(I+1).0.0-0           |
| ^0.0             | =0.0                | >=0.0.0, <0.1.0-0      | >=0.0.0-0, <0.1.0-0               |
| ^I               | =I                  | >=I.0.0, <(I+1).0.0-0  | >=I.0.0-0, <(I+1).0.0-0           |
| `Op::Wildcard`   |                     |                        |                                   |
| `I.J.*`          | =I.J                | >=I.J.0, <I.(J+1).0-0  | >=I.J.0-0, <I.(J+1).0-0           |
| `I.*` or `I.*.*` | =I                  | >=I.0.0, <(I+1).0.0-0  | >=I.0.0-0, <(I+1).0.0-0           |

Notes:

<div id="op-less"></div>

- `<I.J.K`: This is equivalent to `<I.J.K-0` if no lower bound or the lower bound isn't pre-release, otherwise this is equivalent to `<I.J.K`.

<div id="mirror-node"></div>

- [matches_prerelease_mirror_node](3464fd136a) is yet another  implementation of [node semver compatibility](https://github.com/npm/node-semver?tab=readme-ov-file#semver1----the-semantic-versioner-for-npm) (with [includePrerelease=true](https://github.com/npm/node-semver?tab=readme-ov-file#functions)) test. This is extrapolated from the test cases and is not necessarily the same as the node implementation

Besides, the proposed semtantic left a [unsolved issuse ](https://github.com/dtolnay/semver/pull/321#issuecomment-2251058953), I don't have clear idea to handle it.

### How should we test and review this PR?
The tests in `src/cargo/util/semver_eval_ext.rs` are designed to reflect current semantic.

### Additional information
Migrated from https://github.com/dtolnay/semver/pull/321

TBO, this feature was not conceived in advance plus the semantic is unclear at first.  I need to experiment step by step and find out what I think makes sense.  My experiment can be seen this [comment](https://github.com/rust-lang/cargo/issues/13290#issuecomment-2250051428).
This commit is contained in:
bors 2024-08-23 15:59:34 +00:00
commit a0acc29f7b
4 changed files with 708 additions and 31 deletions

View File

@ -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;

View File

@ -0,0 +1,595 @@
//! 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 {
// 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, lower_bound_prerelease) {
return false;
}
}
true
}
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),
Op::GreaterEq => {
if matches_exact_prerelease(cmp, ver) {
return true;
}
matches_greater(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;
}
matches_less(&fill_partial_req(cmp), ver)
}
Op::Tilde => matches_tilde_prerelease(cmp, ver),
Op::Caret => matches_caret_prerelease(cmp, ver),
_ => unreachable!(),
}
}
// 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;
}
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
}
// 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;
}
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
}
// 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;
}
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 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;
}
// 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, <I.J.(K+1)-0
for r in &[req("=4.2.1"), req(">=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, <I.(J+1).0-0
for r in &[req("=4.2"), req(">=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() {
// <I.J.K equivalent to <I.J.K-0
for r in &[req("<4.2.1"), req("<4.2.1-0")] {
assert_match_all(r, &["0.0.0", "4.0.0"]);
assert_match_none(r, &["4.2.1-0", "4.2.2", "5.0.0-0", "5.0.0"]);
}
// <I.J equivalent to <I.J.0-0
for r in &[req("<4.2"), req("<4.2.0-0")] {
assert_match_all(r, &["0.0.0", "4.1.0"]);
assert_match_none(r, &["4.2.0-0", "4.2.0", "4.3.0-0", "4.3.0"]);
}
// <I equivalent to <I.0.0-0
for r in &[req("<4"), req("<4.0.0-0")] {
assert_match_all(r, &["0.0.0", "3.9.0"]);
assert_match_none(r, &["4.0.0-0", "4.0.0", "5.0.0-1", "5.0.0"]);
}
}
#[test]
fn test_less_upper_bound() {
// Lower bound without prerelase tag, so upper bound equivalent to <I.J.K-0
for r in &[
req(">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
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-0equivalent 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.Kequivalent 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.0equivalent 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"]);
}
// ^Iequivalent 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, <I.(J+1).0-0
for r in &[req("4.2.*"), req("=4.2")] {
// Match >= 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.*orI.*.*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+1).0-0
for r in &[req("<=4.2"), req("<4.3.0-0")] {
assert_match_all(r, &["0.0.0", "4.2.0-0"]);
assert_match_none(r, &["4.3.0-0", "4.3.0", "4.4.0"]);
}
// <=I equivalent to <(I+1).0.0-0
for r in &[req("<=4"), req("<5.0.0-0")] {
assert_match_all(r, &["0.0.0", "4.0.0-0", "4.0.0"]);
assert_match_none(r, &["5.0.0-1", "5.0.0"]);
}
}
#[test]
fn test_tilde() {
// ~I.J.K-0equivalent to >=I.J.K-0, <I.(J+1).0-0
for r in &[req("~1.2.3-0"), req(">= 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.Kequivalent to >=I.J.K, <I.(J+1).0-0
for r in &[req("~1.2.3"), req(">= 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.Jequivalent to >=I.J.0, <I.(J+1).0-0
for r in &[req("~0.24"), req(">=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"]);
}
}
}

View File

@ -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),
@ -226,17 +229,25 @@ mod matches_prerelease {
(">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.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.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.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", 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", 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();

View File

@ -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,25 +92,98 @@ 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\""));
// 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();
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();
// 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)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[UPDATING] my-dependency v0.1.2-pre.0 -> v0.1.2-pre.1
[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.1\""));
assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2\""));
}
#[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
[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.3-pre.1\""));
}