Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/05-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ refused offline.
[dependencies]
foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret, default)
bar = "~1.2.3" # >= 1.2.3, < 1.3.0 (tilde)
cat = "^0" # >= 0.0.0, < 1.0.0 (a segment not written is a wildcard)
dog = "~1" # >= 1.0.0, < 2.0.0
baz = "=1.2.3" # Exact match
qux = ">=1.0, <2.0" # Range combination
```
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/05-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ commit。`git =` 指向本地目录(或 `file://` URL)时,这两件事都
[dependencies]
foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret, default)
bar = "~1.2.3" # >= 1.2.3, < 1.3.0 (tilde)
cat = "^0" # >= 0.0.0, < 1.0.0 (a segment not written is a wildcard)
dog = "~1" # >= 1.0.0, < 2.0.0
baz = "=1.2.3" # Exact match
qux = ">=1.0, <2.0" # Range combination
```
Expand Down
27 changes: 19 additions & 8 deletions modules/versioning/src/version_req.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// "1.2.3" → caret-default: >=1.2.3, <2.0.0
// "^1.2.3" → explicit caret
// "~1.2.3" → tilde: >=1.2.3, <1.3.0
// "^0", "^0.0" → partial caret: <1.0.0, <0.1.0 (an unwritten segment is a wildcard)
// "~1" → partial tilde: >=1.0.0, <2.0.0
// "=1.2.3" → exact: ==1.2.3
// ">=1.2, <2.0" → comma-separated comparators (AND)
// "*" → any
Expand Down Expand Up @@ -372,8 +374,13 @@ bool matches(const Requirement& r, const Version& v) {
case Op::Le: if (!(v <= c.v)) return false; break;
case Op::Caret: {
// ^X.Y.Z = >=X.Y.Z, <(X+1).0.0 (leftmost-nonzero rule)
// For simplicity here: bump major; if major==0 bump minor; if both 0 bump patch.
// Every branch must also zero the segments AFTER the bumped one,
// The bound bumps the leftmost non-zero segment, looking no
// further than the patch. A segment that is not written is a
// wildcard rather than a zero, so the search also stops at the
// last one written: `^0` is <1.0.0 and `^0.0` is <0.1.0, as in
// Cargo, while `^0.0.0` stays <0.0.1. Reading the absent
// segments as zeros made `^0` and `^0.0` admit 0.0.0 alone.
// The bump must also zero the segments AFTER the bumped one,
// or the upper bound inherits the constraint's own tail and
// wrongly excludes releases below it (^2026.7.27.3 would cut off
// 2027.0.0.0..2). The bound is a release, never a pre-release:
Expand All @@ -385,19 +392,23 @@ bool matches(const Requirement& r, const Version& v) {
++upper.nums[idx];
upper.nums.resize(idx + 1);
};
if (c.v.major() != 0) bump_at(0);
else if (c.v.minor() != 0) bump_at(1);
else bump_at(2);
const std::size_t written = std::clamp<std::size_t>(c.v.components(), 1, 3);
std::size_t at = written - 1;
for (std::size_t i = 0; i + 1 < written; ++i)
if (c.v.seg(i) != 0) { at = i; break; }
bump_at(at);
if (!(v >= c.v && v < upper)) return false;
break;
}
case Op::Tilde: {
// ~X.Y.Z = >=X.Y.Z, <X.(Y+1).0
// ~X = >=X.0.0, <(X+1).0.0 -- with the minor not written
// there is no minor to hold, so only the major is (Cargo).
Version upper = c.v;
upper.prerelease.clear();
upper.nums.resize(std::max<std::size_t>(upper.nums.size(), 2), 0);
++upper.nums[1];
upper.nums.resize(2);
const std::size_t at = c.v.components() == 1 ? 0 : 1;
upper.nums.resize(at + 1);
++upper.nums[at];
if (!(v >= c.v && v < upper)) return false;
break;
}
Expand Down
50 changes: 50 additions & 0 deletions tests/unit/test_version_req.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,56 @@ TEST(VersionReq, MatchTilde) {
EXPECT_FALSE(matches(*r, *parse_version("1.2.2")));
}

// A segment that is not written is a wildcard, not a zero (Cargo's partial
// requirements). Reading `^0` as `^0.0.0` made it admit 0.0.0 alone.
TEST(VersionReq, PartialCaretBoundsFollowTheWrittenSegments) {
auto major = parse_req("^0"); ASSERT_TRUE(major);
EXPECT_TRUE (matches(*major, *parse_version("0.0.0")));
EXPECT_TRUE (matches(*major, *parse_version("0.0.5")));
EXPECT_TRUE (matches(*major, *parse_version("0.9.3")));
EXPECT_FALSE(matches(*major, *parse_version("1.0.0")));

auto minor = parse_req("^0.0"); ASSERT_TRUE(minor);
EXPECT_TRUE (matches(*minor, *parse_version("0.0.0")));
EXPECT_TRUE (matches(*minor, *parse_version("0.0.7")));
EXPECT_FALSE(matches(*minor, *parse_version("0.1.0")));

// Every segment written: the leftmost-nonzero rule is unchanged.
auto patch = parse_req("^0.0.0"); ASSERT_TRUE(patch);
EXPECT_TRUE (matches(*patch, *parse_version("0.0.0")));
EXPECT_FALSE(matches(*patch, *parse_version("0.0.1")));

auto exact = parse_req("^0.0.3"); ASSERT_TRUE(exact);
EXPECT_TRUE (matches(*exact, *parse_version("0.0.3")));
EXPECT_FALSE(matches(*exact, *parse_version("0.0.4")));

auto one = parse_req("^1"); ASSERT_TRUE(one);
EXPECT_TRUE (matches(*one, *parse_version("1.9.9")));
EXPECT_FALSE(matches(*one, *parse_version("2.0.0")));

auto zeroFive = parse_req("^0.5"); ASSERT_TRUE(zeroFive);
EXPECT_TRUE (matches(*zeroFive, *parse_version("0.5.9")));
EXPECT_FALSE(matches(*zeroFive, *parse_version("0.6.0")));
}

TEST(VersionReq, PartialTildeWithOnlyAMajorHoldsTheMajor) {
auto r = parse_req("~1"); ASSERT_TRUE(r);
EXPECT_TRUE (matches(*r, *parse_version("1.0.0")));
EXPECT_TRUE (matches(*r, *parse_version("1.1.0")));
EXPECT_TRUE (matches(*r, *parse_version("1.9.9")));
EXPECT_FALSE(matches(*r, *parse_version("2.0.0")));
EXPECT_FALSE(matches(*r, *parse_version("0.9.9")));

auto zero = parse_req("~0"); ASSERT_TRUE(zero);
EXPECT_TRUE (matches(*zero, *parse_version("0.4.0")));
EXPECT_FALSE(matches(*zero, *parse_version("1.0.0")));

// With the minor written, tilde holds it, as before.
auto minor = parse_req("~1.2"); ASSERT_TRUE(minor);
EXPECT_TRUE (matches(*minor, *parse_version("1.2.9")));
EXPECT_FALSE(matches(*minor, *parse_version("1.3.0")));
}

TEST(VersionReq, MatchRangeAnd) {
auto r = parse_req(">=1.2, <2.0"); ASSERT_TRUE(r);
EXPECT_TRUE (matches(*r, *parse_version("1.2.0")));
Expand Down
Loading