cargo/util/
semver_eval_ext.rs

1//! Extend `semver::VersionReq` with  [`matches_prerelease`] which doesn't preclude pre-releases by default.
2//!
3//! Please refer to the semantic proposal, see [RFC 3493].
4//!
5//! [RFC 3493]: https://rust-lang.github.io/rfcs/3493-precise-pre-release-cargo-update.html
6
7use semver::{Comparator, Op, Prerelease, Version, VersionReq};
8
9pub(crate) fn matches_prerelease(req: &VersionReq, ver: &Version) -> bool {
10    // Whether there are pre release version can be as lower bound
11    let lower_bound_prerelease = &req.comparators.iter().any(|cmp| {
12        if matches!(cmp.op, Op::Greater | Op::GreaterEq) && !cmp.pre.is_empty() {
13            true
14        } else {
15            false
16        }
17    });
18    for cmp in &req.comparators {
19        if !matches_prerelease_impl(cmp, ver, lower_bound_prerelease) {
20            return false;
21        }
22    }
23
24    true
25}
26
27fn matches_prerelease_impl(cmp: &Comparator, ver: &Version, lower_bound_prerelease: &bool) -> bool {
28    match cmp.op {
29        Op::Exact | Op::Wildcard => matches_exact_prerelease(cmp, ver),
30        Op::Greater => matches_greater(cmp, ver),
31        Op::GreaterEq => {
32            if matches_exact_prerelease(cmp, ver) {
33                return true;
34            }
35            matches_greater(cmp, ver)
36        }
37        Op::Less => {
38            if *lower_bound_prerelease {
39                matches_less(&fill_partial_req(cmp), ver)
40            } else {
41                matches_less(&fill_partial_req_include_pre(cmp), ver)
42            }
43        }
44        Op::LessEq => {
45            if matches_exact_prerelease(cmp, ver) {
46                return true;
47            }
48            matches_less(&fill_partial_req(cmp), ver)
49        }
50        Op::Tilde => matches_tilde_prerelease(cmp, ver),
51        Op::Caret => matches_caret_prerelease(cmp, ver),
52        _ => unreachable!(),
53    }
54}
55
56// See https://github.com/dtolnay/semver/blob/69efd3cc770ead273a06ad1788477b3092996d29/src/eval.rs#L44-L62
57fn matches_exact(cmp: &Comparator, ver: &Version) -> bool {
58    if ver.major != cmp.major {
59        return false;
60    }
61
62    if let Some(minor) = cmp.minor {
63        if ver.minor != minor {
64            return false;
65        }
66    }
67
68    if let Some(patch) = cmp.patch {
69        if ver.patch != patch {
70            return false;
71        }
72    }
73
74    ver.pre == cmp.pre
75}
76
77// See https://github.com/dtolnay/semver/blob/69efd3cc770ead273a06ad1788477b3092996d29/src/eval.rs#L64-L88
78fn matches_greater(cmp: &Comparator, ver: &Version) -> bool {
79    if ver.major != cmp.major {
80        return ver.major > cmp.major;
81    }
82
83    match cmp.minor {
84        None => return false,
85        Some(minor) => {
86            if ver.minor != minor {
87                return ver.minor > minor;
88            }
89        }
90    }
91
92    match cmp.patch {
93        None => return false,
94        Some(patch) => {
95            if ver.patch != patch {
96                return ver.patch > patch;
97            }
98        }
99    }
100
101    ver.pre > cmp.pre
102}
103
104// See https://github.com/dtolnay/semver/blob/69efd3cc770ead273a06ad1788477b3092996d29/src/eval.rs#L90-L114
105fn matches_less(cmp: &Comparator, ver: &Version) -> bool {
106    if ver.major != cmp.major {
107        return ver.major < cmp.major;
108    }
109
110    match cmp.minor {
111        None => return false,
112        Some(minor) => {
113            if ver.minor != minor {
114                return ver.minor < minor;
115            }
116        }
117    }
118
119    match cmp.patch {
120        None => return false,
121        Some(patch) => {
122            if ver.patch != patch {
123                return ver.patch < patch;
124            }
125        }
126    }
127
128    ver.pre < cmp.pre
129}
130
131fn fill_partial_req(cmp: &Comparator) -> Comparator {
132    let mut cmp = cmp.clone();
133    if cmp.minor.is_none() {
134        cmp.minor = Some(0);
135        cmp.patch = Some(0);
136    } else if cmp.patch.is_none() {
137        cmp.patch = Some(0);
138    }
139    cmp
140}
141
142fn fill_partial_req_include_pre(cmp: &Comparator) -> Comparator {
143    let mut cmp = cmp.clone();
144    if cmp.minor.is_none() {
145        cmp.minor = Some(0);
146        cmp.patch = Some(0);
147        cmp.pre = Prerelease::new("0").unwrap();
148    } else if cmp.patch.is_none() {
149        cmp.patch = Some(0);
150    }
151    if cmp.pre.is_empty() {
152        cmp.pre = Prerelease::new("0").unwrap();
153    }
154    cmp
155}
156
157fn matches_exact_prerelease(cmp: &Comparator, ver: &Version) -> bool {
158    if matches_exact(cmp, ver) {
159        return true;
160    }
161
162    // If the comparator has a prerelease tag like =3.0.0-alpha.24,
163    // then it shoud be only exactly match 3.0.0-alpha.24.
164    if !cmp.pre.is_empty() {
165        return false;
166    }
167
168    if !matches_greater(&fill_partial_req(cmp), ver) {
169        return false;
170    }
171
172    let mut upper = Comparator {
173        op: Op::Less,
174        pre: Prerelease::new("0").unwrap(),
175        ..cmp.clone()
176    };
177
178    match (upper.minor.is_some(), upper.patch.is_some()) {
179        (true, true) => {
180            upper.patch = Some(upper.patch.unwrap() + 1);
181        }
182        (true, false) => {
183            // Partial Exact VersionReq eg. =0.24
184            upper.minor = Some(upper.minor.unwrap() + 1);
185            upper.patch = Some(0);
186        }
187        (false, false) => {
188            // Partial Exact VersionReq eg. =0
189            upper.major += 1;
190            upper.minor = Some(0);
191            upper.patch = Some(0);
192        }
193        _ => {}
194    }
195
196    matches_less(&upper, ver)
197}
198
199fn matches_tilde_prerelease(cmp: &Comparator, ver: &Version) -> bool {
200    if matches_exact(cmp, ver) {
201        return true;
202    }
203
204    if !matches_greater(&fill_partial_req(cmp), ver) {
205        return false;
206    }
207
208    let mut upper = Comparator {
209        op: Op::Less,
210        pre: Prerelease::new("0").unwrap(),
211        ..cmp.clone()
212    };
213
214    match (upper.minor.is_some(), upper.patch.is_some()) {
215        (true, _) => {
216            upper.minor = Some(upper.minor.unwrap() + 1);
217            upper.patch = Some(0);
218        }
219        (false, false) => {
220            upper.major += 1;
221            upper.minor = Some(0);
222            upper.patch = Some(0);
223        }
224        _ => {}
225    }
226
227    matches_less(&upper, ver)
228}
229
230fn matches_caret_prerelease(cmp: &Comparator, ver: &Version) -> bool {
231    if matches_exact(cmp, ver) {
232        return true;
233    }
234
235    if !matches_greater(&fill_partial_req(cmp), ver) {
236        return false;
237    }
238
239    let mut upper = Comparator {
240        op: Op::Less,
241        pre: Prerelease::new("0").unwrap(),
242        ..cmp.clone()
243    };
244
245    match (
246        upper.major > 0,
247        upper.minor.is_some(),
248        upper.patch.is_some(),
249    ) {
250        (true, _, _) | (_, false, false) => {
251            upper.major += 1;
252            upper.minor = Some(0);
253            upper.patch = Some(0);
254        }
255        (_, true, false) => {
256            upper.minor = Some(upper.minor.unwrap() + 1);
257            upper.patch = Some(0);
258        }
259        (_, true, _) if upper.minor.unwrap() > 0 => {
260            upper.minor = Some(upper.minor.unwrap() + 1);
261            upper.patch = Some(0);
262        }
263        (_, true, _) if upper.minor.unwrap() == 0 => {
264            if upper.patch.is_none() {
265                upper.patch = Some(1);
266            } else {
267                upper.patch = Some(upper.patch.unwrap() + 1);
268            }
269        }
270        _ => {}
271    }
272
273    matches_less(&upper, ver)
274}
275
276#[cfg(test)]
277mod matches_prerelease_semantic {
278    use crate::util::semver_ext::VersionReqExt;
279    use semver::{Version, VersionReq};
280
281    fn assert_match_all(req: &VersionReq, versions: &[&str]) {
282        for string in versions {
283            let parsed = Version::parse(string).unwrap();
284            assert!(
285                req.matches_prerelease(&parsed),
286                "{} did not match {}",
287                req,
288                string,
289            );
290        }
291    }
292
293    fn assert_match_none(req: &VersionReq, versions: &[&str]) {
294        for string in versions {
295            let parsed = Version::parse(string).unwrap();
296            assert!(
297                !req.matches_prerelease(&parsed),
298                "{} matched {}",
299                req,
300                string
301            );
302        }
303    }
304
305    pub(super) fn req(text: &str) -> VersionReq {
306        VersionReq::parse(text).unwrap()
307    }
308
309    #[test]
310    fn test_exact() {
311        // =I.J.K-pre only match I.J.K-pre
312        let ref r = req("=4.2.1-0");
313        // Only exactly match 4.2.1-0
314        assert_match_all(r, &["4.2.1-0"]);
315        // Not match others
316        assert_match_none(r, &["1.2.3", "4.2.0", "4.2.1-1", "4.2.2"]);
317
318        // =I.J.K equivalent to >=I.J.K, <I.J.(K+1)-0
319        for r in &[req("=4.2.1"), req(">=4.2.1, <4.2.2-0")] {
320            assert_match_all(r, &["4.2.1"]);
321            assert_match_none(r, &["1.2.3", "4.2.1-0", "4.2.2-0", "4.2.2"]);
322        }
323
324        // =I.J equivalent to >=I.J.0, <I.(J+1).0-0
325        for r in &[req("=4.2"), req(">=4.2.0, <4.3.0-0")] {
326            assert_match_all(r, &["4.2.0", "4.2.1", "4.2.9"]);
327            assert_match_none(r, &["0.0.1", "2.1.2-0", "4.2.0-0"]);
328            assert_match_none(r, &["4.3.0-0", "4.3.0", "5.0.0-0", "5.0.0"]);
329        }
330
331        // =I equivalent to >=I.0.0, <(I+1).0.0-0
332        for r in &[req("=4"), req(">=4.0.0, <5.0.0-0")] {
333            assert_match_all(r, &["4.0.0", "4.2.1", "4.2.4-0", "4.9.9"]);
334            assert_match_none(r, &["0.0.1", "2.1.2-0", "4.0.0-0"]);
335            assert_match_none(r, &["5.0.0-0", "5.0.0", "5.0.1"]);
336        }
337    }
338
339    #[test]
340    fn test_greater_eq() {
341        // >=I.J.K-0
342        let ref r = req(">=4.2.1-0");
343        assert_match_all(r, &["4.2.1-0", "4.2.1", "5.0.0"]);
344        assert_match_none(r, &["0.0.0", "1.2.3"]);
345
346        // >=I.J.K
347        let ref r = req(">=4.2.1");
348        assert_match_all(r, &["4.2.1", "5.0.0"]);
349        assert_match_none(r, &["0.0.0", "4.2.1-0"]);
350
351        // >=I.J equivalent to >=I.J.0
352        for r in &[req(">=4.2"), req(">=4.2.0")] {
353            assert_match_all(r, &["4.2.1-0", "4.2.0", "4.3.0"]);
354            assert_match_none(r, &["0.0.0", "4.1.1", "4.2.0-0"]);
355        }
356
357        // >=I equivalent to >=I.0.0
358        for r in &[req(">=4"), req(">=4.0.0")] {
359            assert_match_all(r, &["4.0.0", "4.1.0-1", "5.0.0"]);
360            assert_match_none(r, &["0.0.0", "1.2.3", "4.0.0-0"]);
361        }
362    }
363
364    #[test]
365    fn test_less() {
366        // <I.J.K equivalent to <I.J.K-0
367        for r in &[req("<4.2.1"), req("<4.2.1-0")] {
368            assert_match_all(r, &["0.0.0", "4.0.0"]);
369            assert_match_none(r, &["4.2.1-0", "4.2.2", "5.0.0-0", "5.0.0"]);
370        }
371
372        // <I.J equivalent to <I.J.0-0
373        for r in &[req("<4.2"), req("<4.2.0-0")] {
374            assert_match_all(r, &["0.0.0", "4.1.0"]);
375            assert_match_none(r, &["4.2.0-0", "4.2.0", "4.3.0-0", "4.3.0"]);
376        }
377
378        // <I equivalent to <I.0.0-0
379        for r in &[req("<4"), req("<4.0.0-0")] {
380            assert_match_all(r, &["0.0.0", "3.9.0"]);
381            assert_match_none(r, &["4.0.0-0", "4.0.0", "5.0.0-1", "5.0.0"]);
382        }
383    }
384
385    #[test]
386    fn test_less_upper_bound() {
387        // Lower bound without prerelase tag, so upper bound equivalent to <I.J.K-0
388        for r in &[
389            req(">1.2.3, <2"),
390            req(">1.2.3, <2.0"),
391            req(">1.2.3, <2.0.0"),
392            req(">=1.2.3, <2.0.0"),
393            req(">1.2.3, <2.0.0-0"),
394        ] {
395            assert_match_all(r, &["1.2.4", "1.9.9"]);
396            assert_match_none(r, &["2.0.0-0", "2.0.0", "2.1.2"]);
397        }
398
399        // Lower bound has prerelase tag, so upper bound doesn't change.
400        for r in &[
401            req(">1.2.3-0, <2"),
402            req(">1.2.3-0, <2.0"),
403            req(">1.2.3-0, <2.0.0"),
404            req(">=1.2.3-0, <2.0.0"),
405        ] {
406            assert_match_all(r, &["1.2.4", "1.9.9", "2.0.0-0"]);
407            assert_match_none(r, &["2.0.0", "2.1.2"]);
408        }
409
410        for r in &[
411            req(">=2.0.0-0, <2"),
412            req(">=2.0.0-0, <2.0"),
413            req(">=2.0.0-0, <2.0.0"),
414        ] {
415            assert_match_all(r, &["2.0.0-0", "2.0.0-11"]);
416            assert_match_none(r, &["0.0.9", "2.0.0"]);
417        }
418
419        // There is no intersection between lower bound and upper bound, in this case nothing matches
420        let ref r = req(">5.0.0, <2.0.0");
421        assert_match_none(r, &["1.2.3", "3.0.0", "6.0.0"]);
422        let ref r = req(">5.0.0-0, <2.0.0");
423        assert_match_none(r, &["1.2.3", "3.0.0", "6.0.0"]);
424    }
425
426    #[test]
427    fn test_caret() {
428        // ^I.J.K.0 (for I>0) — equivalent to >=I.J.K-0, <(I+1).0.0-0
429        for r in &[req("^1.2.3-0"), req(">=1.2.3-0, <2.0.0-0")] {
430            assert_match_all(r, &["1.2.3-0", "1.2.3-1", "1.2.3", "1.9.9"]);
431            assert_match_none(r, &["0.0.9", "1.1.1-0", "2.0.0-0", "2.1.1"]);
432        }
433
434        // ^I.J.K (for I>0) — equivalent to >=I.J.K, <(I+1).0.0-0
435        for r in &[req("^1.2.3"), req(">=1.2.3, <2.0.0-0")] {
436            assert_match_all(r, &["1.2.3", "1.9.9"]);
437            assert_match_none(
438                r,
439                &["0.0.9", "1.1.1-0", "1.2.3-0", "1.2.3-1", "2.0.0-0", "2.1.1"],
440            );
441        }
442
443        // ^0.J.K-0 (for J>0) — equivalent to >=0.J.K-0, <0.(J+1).0-0
444        for r in &[req("^0.2.3-0"), req(">=0.2.3-0, <0.3.0-0")] {
445            assert_match_all(r, &["0.2.3-0", "0.2.3", "0.2.9-0", "0.2.9"]);
446            assert_match_none(r, &["0.0.9", "0.3.0-0", "0.3.11", "1.1.1"]);
447        }
448
449        // ^0.J.K (for J>0) — equivalent to >=0.J.K-0, <0.(J+1).0-0
450        for r in &[req("^0.2.3"), req(">=0.2.3, <0.3.0-0")] {
451            assert_match_all(r, &["0.2.3", "0.2.9-0", "0.2.9"]);
452            assert_match_none(r, &["0.0.9", "0.2.3-0", "0.3.0-0", "0.3.11", "1.1.1"]);
453        }
454
455        // ^0.0.K-0 — equivalent to >=0.0.K-0, <0.0.(K+1)-0
456        for r in &[req("^0.0.3-0"), req(">=0.0.3-0, <0.1.0-0")] {
457            assert_match_all(r, &["0.0.3-0", "0.0.3-1", "0.0.3"]);
458            assert_match_none(r, &["0.0.1", "0.3.0-0", "0.4.0-0", "1.1.1"]);
459        }
460
461        // ^0.0.K — equivalent to >=0.0.K, <0.0.(K+1)-0
462        for r in &[req("^0.0.3"), req(">=0.0.3, <0.1.0-0")] {
463            assert_match_all(r, &["0.0.3"]);
464            assert_match_none(
465                r,
466                &["0.0.1", "0.0.3-0", "0.3.0-0", "0.0.3-1", "0.4.0-0", "1.1.1"],
467            );
468        }
469
470        // ^I.J (for I>0 or J>0) — equivalent to >=I.J.0, <(I+1).0.0-0)
471        for r in &[req("^1.2"), req(">=1.2.0, <2.0.0-0")] {
472            assert_match_all(r, &["1.2.0", "1.9.0-0", "1.9.9"]);
473            assert_match_none(r, &["0.0.1", "0.0.4-0", "1.2.0-0", "2.0.0-0", "4.0.1"]);
474        }
475
476        // ^0.0 — equivalent to >=0.0.0, <0.1.0-0
477        for r in &[req("^0.0"), req(">=0.0.0, <0.1.0-0")] {
478            assert_match_all(r, &["0.0.0", "0.0.1", "0.0.4-0"]);
479            assert_match_none(r, &["0.0.0-0", "0.1.0-0", "0.1.0", "1.1.1"]);
480        }
481
482        // ^I — equivalent to >=I.0.0, <(I+1).0.0-0
483        for r in &[req("^1"), req(">=1.0.0, <2.0.0-0")] {
484            assert_match_all(r, &["1.0.0", "1.0.1"]);
485            assert_match_none(r, &["0.1.0-0", "0.1.0", "1.0.0-0", "2.0.0-0", "3.1.2"]);
486        }
487    }
488
489    #[test]
490    fn test_wildcard() {
491        // I.J.* — equivalent to =I.J
492        //
493        // =I.J equivalent to >=I.J.0, <I.(J+1).0-0
494        for r in &[req("4.2.*"), req("=4.2")] {
495            // Match >= 4.2.0, < 4.3.0-0
496            assert_match_all(r, &["4.2.0", "4.2.1", "4.2.9"]);
497            // Not Match < 4.2.0
498            assert_match_none(r, &["0.0.1", "2.1.2-0", "4.2.0-0"]);
499            // Not Match >= 4.3.0-0
500            assert_match_none(r, &["4.3.0-0", "4.3.0", "5.0.0", "5.0.1"]);
501        }
502
503        // I.* or I.*.* — equivalent to =I
504        //
505        // =I equivalent to >=I.0.0, <(I+1).0.0-0
506        for r in &[req("4.*"), req("4.*.*"), req("=4")] {
507            // Match >= 4.0.0, < 5.0.0-0
508            assert_match_all(r, &["4.0.0", "4.2.1", "4.9.9"]);
509            // Not Match < 4.0.0
510            assert_match_none(r, &["0.0.1", "2.1.2-0", "4.0.0-0"]);
511            // Not Match >= 5.0.0-0
512            assert_match_none(r, &["5.0.0-0", "5.0.0", "5.0.1"]);
513        }
514    }
515
516    #[test]
517    fn test_greater() {
518        // >I.J.K-0
519        let ref r = req(">4.2.1-0");
520        assert_match_all(r, &["4.2.1", "4.2.2", "5.0.0"]);
521        assert_match_none(r, &["0.0.0", "4.2.1-0"]);
522
523        // >I.J.K
524        let ref r = req(">4.2.1");
525        assert_match_all(r, &["4.2.2", "5.0.0-0", "5.0.0"]);
526        assert_match_none(r, &["0.0.0", "4.2.1-0", "4.2.1"]);
527
528        // >I.J equivalent to >=I.(J+1).0-0
529        for r in &[req(">4.2"), req(">=4.3.0-0")] {
530            assert_match_all(r, &["4.3.0-0", "4.3.0", "5.0.0"]);
531            assert_match_none(r, &["0.0.0", "4.2.1"]);
532        }
533
534        // >I equivalent to >=(I+1).0.0-0
535        for r in &[req(">4"), req(">=5.0.0-0")] {
536            assert_match_all(r, &["5.0.0-0", "5.0.0"]);
537            assert_match_none(r, &["0.0.0", "4.2.1"]);
538        }
539    }
540
541    #[test]
542    fn test_less_eq() {
543        // <=I.J.K
544        let ref r = req("<=4.2.1");
545        assert_match_all(r, &["0.0.0", "4.2.1-0", "4.2.1"]);
546        assert_match_none(r, &["4.2.2", "5.0.0-0", "5.0.0"]);
547        // <=I.J.K-0
548        let ref r = req("<=4.2.1-0");
549        assert_match_all(r, &["0.0.0", "4.2.1-0"]);
550        assert_match_none(r, &["4.2.1", "4.2.2", "5.0.0-0", "5.0.0"]);
551
552        // <=I.J equivalent to <I.(J+1).0-0
553        for r in &[req("<=4.2"), req("<4.3.0-0")] {
554            assert_match_all(r, &["0.0.0", "4.2.0-0"]);
555            assert_match_none(r, &["4.3.0-0", "4.3.0", "4.4.0"]);
556        }
557
558        // <=I equivalent to <(I+1).0.0-0
559        for r in &[req("<=4"), req("<5.0.0-0")] {
560            assert_match_all(r, &["0.0.0", "4.0.0-0", "4.0.0"]);
561            assert_match_none(r, &["5.0.0-1", "5.0.0"]);
562        }
563    }
564
565    #[test]
566    fn test_tilde() {
567        // ~I.J.K-0 — equivalent to >=I.J.K-0, <I.(J+1).0-0
568        for r in &[req("~1.2.3-0"), req(">= 1.2.3-0, < 1.3.0-0")] {
569            assert_match_all(r, &["1.2.3-0", "1.2.3", "1.2.4-0", "1.2.4"]);
570            assert_match_none(r, &["0.0.1", "1.1.0-0"]);
571            assert_match_none(r, &["1.3.0-0", "1.3.0", "1.3.1", "2.0.0"]);
572        }
573
574        // ~I.J.K — equivalent to >=I.J.K, <I.(J+1).0-0
575        for r in &[req("~1.2.3"), req(">= 1.2.3, < 1.3.0-0")] {
576            assert_match_all(r, &["1.2.3", "1.2.4-0", "1.2.4"]);
577            assert_match_none(r, &["0.0.1", "1.1.0-0", "1.2.3-0"]);
578            assert_match_none(r, &["1.3.0-0", "1.3.0", "1.3.1", "2.0.0"]);
579        }
580
581        // ~I.J — equivalent to >=I.J.0, <I.(J+1).0-0
582        for r in &[req("~0.24"), req(">=0.24.0, <0.25.0-0")] {
583            assert_match_all(r, &["0.24.0", "0.24.1-0", "0.24.1", "0.24.9"]);
584            assert_match_none(r, &["0.0.1", "0.9.9", "0.24.0-0"]);
585            assert_match_none(r, &["0.25.0-0", "1.1.0", "1.2.3", "2.0.0"]);
586        }
587
588        // ~I — >=I.0.0, <(I+1).0.0-0
589        for r in &[req("~1"), req(">=1.0.0, <2.0.0-0")] {
590            assert_match_all(r, &["1.0.0", "1.1.0-0", "1.1.0"]);
591            assert_match_none(r, &["0.0.1", "0.9.9", "1.0.0-0"]);
592            assert_match_none(r, &["2.0.0-0", "2.0.0", "2.0.1"]);
593        }
594    }
595}