Skip to content

feat(version): match version constraints with DLoad's own ordering - #155

Merged
roxblnfk merged 9 commits into
fix/exact-prereleasefrom
feat/own-constraint-matcher
Oct 11, 2026
Merged

roxblnfk merged 9 commits into
fix/exact-prereleasefrom
feat/own-constraint-matcher

Conversation

@roxblnfk

Copy link
Copy Markdown
Member

What was changed

  • Version constraints are matched by DLoad's own code (Range of OR-ed groups of AND-ed Comparisons against bound Versions) instead of composer/semver, so matching and release sorting share one order: numbered pre-releases and tails, numeric tails like 1.0.0-1, trailing zeros, build metadata and any count of number parts on both sides (>=1.2.3.4.5).
  • The Composer syntax keeps its meaning (^, ~, wildcards, ||, AND by space or comma), and more of it now works: hyphen ranges (1.0 - 2.0), !=/<>, a v prefix, a space after the operator, pre-release bounds inside multi-term constraints (>=1.0 <2.0-RC1, ^1.0 || ^2.0-beta.1), where the lowest bound's stability is the minimum. Malformed constraints fail when parsed, with a message, instead of on every match.
  • Stability keywords are read only as whole words (A word starting with a stability letter is read as a pre-release (-arm64 is alpha) #153): -arm64, -build.5, -ga are feature tails, not alpha or beta; -a1, -beta2, 2.0.0rc1, 1.0.0b2 stay pre-releases. A lone letter glued to the number (1.1.1b) is a patch letter, and OpenSSL 1.1.1b in --version output reads as stable 1.1.1.
  • composer/semver moves to require-dev, where RangeComposerTest checks the new matcher against it.

Why?

#150 had to bend versions to Composer's parser (at most four number parts, a major part of at most five digits) and decide pre-releases on its own, which left matching and sorting disagreeing, e.g. <3.5.0-preview.3 accepted 3.5.0-1 while sorting put it above 3.5.0. This targets the #150 branch so it lands together with it.

Review notes

  • A bound without a pre-release covers every build of its number, as in Composer: >=3.5.0-beta.1 <3.5.0 is empty; write <3.5.0-RC1.
  • Compatibility was checked by running old and new code over generated tables (about 300 constraints × 80 versions): ordinary constraints give the same results; the differences are versions Composer could not read (20250101.1.2.3), numeric tails, and constraints that used to throw.

Checklist

…stead of composer/semver

fix(version): read a stability keyword only as a whole word, so `-arm64`, `-build.5` or `-ga` is a feature tail, not alpha or beta, and a lone letter glued to the number, like `1.1.1b`, is a patch letter (#153)
fix(version): ignore build metadata when comparing versions, so `1.2.3+5` equals `1.2.3`

Constraints are parsed into alternatives of comparisons against bound versions and
checked with Version::compare(), so matching and sorting share one order: numbered
pre-releases and tails in bounds, a numeric tail like `3.5.0-1` (before, `<3.5.0-preview.3`
accepted it while sorting put it above `3.5.0`), any count of number parts (`>=1.2.3.4.5`)
and versions Composer could not read (`20250101.1.2.3`). The Composer syntax keeps its
meaning: `^`, `~`, wildcards, `||`, AND by space or comma; hyphen ranges, `!=`, a `v` prefix
and a space after an operator now work too. composer/semver moves to require-dev, where a
test compares the new matcher with it.

Closes #154
Closes #153

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(version): build metadata after one or two number parts, as in `1.2+5`, makes a stable release of `1.2`, not a preview of `1.2.5`

A pre-release bound in a range, like `>=1.0 <2.0-RC1`, `^1.0 || ^2.0-beta.1` or
`1.0.0-beta.1 - 2.0.0`, now bounds the range instead of turning into a feature suffix or an
error. The lowest stability among the bounds is the minimum stability; an explicit @stability
still wins. Feature suffixes, like `^1.0.0-experimental`, read as before.

Also: a lone patch letter in a binary's output, like OpenSSL's `1.1.1b`, reads as `1.1.1` again; `1.0.0-1` matches its release again, its numeric tail being a part of the bound; `<>`
is not equal; huge number parts, as in `^99999999999999999999`, no longer overflow; `> =1.0`
is rejected and an empty term, like `1.2,`, gets its own message. The README files show the
range syntax among the constraint examples.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added enhancement New feature or request tests labels Oct 10, 2026
@roxblnfk
roxblnfk requested a balanced review from Copilot October 10, 2026 20:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Oversized numeric components can compare incorrectly, and standard alphanumeric build metadata remains unsupported.

3 open findings
What changed in this PR

Replaces Composer-based constraint matching with DLoad’s native version ordering while expanding Composer-compatible syntax and stability parsing.

Changes:

  • Adds native range parsing and comparison primitives.
  • Improves pre-release, suffix, metadata, and binary-version handling.
  • Expands tests and documentation; moves composer/semver to development dependencies.
File Description
src/​Module/​Version/​Range.php Adds constraint range parsing.
src/​Module/​Version/​Comparison.php Adds bound comparisons.
src/​Module/​Version/​Operator.php Defines comparison operators.
src/​Module/​Version/​Constraint.php Uses native range matching.
src/​Module/​Version/​Version.php Updates parsing and ordering.
src/​Module/​Version/​PreRelease.php Adds whole-word stability matching.
src/​Module/​Binary/​BinaryVersion.php Refines binary output parsing.
tests/​Unit/​Module/​Version/​RangeTest.php Tests native range behavior.
tests/​Unit/​Module/​Version/​RangeComposerTest.php Compares behavior with Composer.
tests/​Unit/​Module/​Version/​ConstraintTest.php Expands constraint coverage.
tests/​Unit/​Module/​Version/​VersionTest.php Tests parsing and ordering.
tests/​Unit/​Module/​Binary/​BinaryVersionTest.php Tests binary version extraction.
composer.json Moves Semver to development dependencies.
composer.lock Updates dependency placement.
README.md Documents additional constraint syntax.
README-zh.md Updates Chinese documentation.
README-ru.md Updates Russian documentation.
README-es.md Updates Spanish documentation.
dload.xsd Expands schema examples.
skills/​dload-fetch-tool/​SKILL.md Documents extended version numbers.
skills/​dload-fetch-tool/​references/​troubleshooting.md Documents suffix stability behavior.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Module/Version/Range.php Outdated
private static function parseVersion(string $version): ?array
{
# Build metadata does not count, so the number goes without it
$pattern = '/^v?(\d+(?:\.\d+)*)(?:\+\d+)?(?:-(' . self::preReleasePattern() . '))?$/i';

/** A suffix starting with a letter may follow the number right away: `2.0.0rc1`, `1.2.3_beta2`. */
protected const VERSION_FALLBACK_PATTERN = 'v?(\d+(?:\.\d+(?:\.\d+(?:\.\d+)*(?:\+\d+)?)?)?)((?:[-+.]|(?=[a-z_]))[\w.-]+)?';
protected const VERSION_FALLBACK_PATTERN = 'v?(\d+(?:\.\d+)*(?:\+\d+)?)((?:[-+.]|(?=[a-z_]))[\w.-]+)?';
*/
public function compareNumber(self $other): int
{
return \version_compare($this->comparableNumber(), $other->comparableNumber());
`>2.0-beta` now accepts 2.0.0-RC1 and 2.0.0, `<=2.0-beta` rejects 2.0.0
and `!=2.0-beta` excludes only 2.0.0-beta, as in Composer and as the
same term already matched within a range. The keyword still sets the
minimum stability unless `@` overrides it.

A digit inside a feature suffix is no longer taken for a pre-release
bound, so `^1.0.0-x64-1`, `^2.0-php8-1` and `1.0.0-beta.1-2` parse again.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
@roxblnfk
roxblnfk added this pull request to stack #157 October 11, 2026 16:00
@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/Module/Binary/BinaryVersion.php 100.00% <100.00%> (ø)
src/Module/Version/Comparison.php 100.00% <100.00%> (ø)
src/Module/Version/Constraint.php 98.48% <100.00%> (+1.01%) ⬆️
src/Module/Version/Operator.php 100.00% <100.00%> (ø)
src/Module/Version/PreRelease.php 100.00% <ø> (ø)
src/Module/Version/Range.php 100.00% <100.00%> (ø)
src/Module/Version/Version.php 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

fix(version): read a bare stability keyword with an explicit `@stability` as a pre-release bound, so `^2.0-beta@alpha` is `>=2.0.0-beta <3` with alpha stability, not a `beta` feature suffix

A constraint of one version without an operator, or after `=` or `==`, that
carries a pre-release or another suffix now names one tag: `2.0-beta` takes
`2.0.0-beta` but not `2.0.0-beta.3`, `2.0.0-RC1` or `2.0.0`, and `2.12.0-hotfix`
no longer takes `2.12.0-hotfix.2` or `2.12.0-hotfix-linux`. A tag such as
`v1.0-arm64-beta.1` can now be pinned exactly. A bare number keeps covering
every build of its number that the stability allows.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
@roxblnfk
roxblnfk removed this pull request from stack #157 October 11, 2026 17:48
…#156)

Pin the branches a mutation review found unprotected: carrying an
incremented part of nines (`9.*`, `~9.9`, `^0.99`, `1.99.*`), a
pre-release word glued to a binary's version (`1.2.3beta2`), pre-release
bounds next to `||` without spaces, and the lowest stability when the
least stable bound comes last.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(version): match the feature suffix of a constraint in any case
fix(version): accept a pre-release glued to the number in version constraints

The suffix left after the pre-release was ordered by version_compare(), which reads `arm64` as alpha and `patch1` as a patch level and minds case, so the exact constraint `1.0.0-arm64` missed the tag `v1.0.0-ARM64`. DLoad now splits that suffix into runs of digits and of letters and compares them in turn: numbers by value, words alphabetically in any case, a word before a number, and a suffix that ends first before a longer one. Separators and leading zeros do not count. Builds of one number and pre-release thus sort by their suffix, `1.0.0-amd64 < 1.0.0-arm64 < 1.0.0-linux`, where they used to compare as equal and keep the order of the API; the previous commit started this ordering without saying so.

Constraints such as `^2.0.0rc1`, `>=1.21rc1`, `2.0.0rc1` and `1.1.1w` parse again, as in 1.18.0, and a glued pre-release now sets the minimum stability like a hyphenated one; a lone `a` or `b` glued to the number stays a patch letter, as in tags. The `^2.12.0-hotfix@rc` example of the docs took only tags like `2.13.0-RC1-hotfix`; it is now `^2.12.0-hotfix@alpha`.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Only `.x` is a wildcard, as in `1.x` or `1.0.x-dev`; `0.9.8x` now names that
tag like `0.9.8w` does. A test pins that a number glued to a word in a tail
counts as a number: `build9 < build10`.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
@roxblnfk
roxblnfk merged commit ca5f07b into fix/exact-prerelease Oct 11, 2026
5 checks passed
@roxblnfk
roxblnfk deleted the feat/own-constraint-matcher branch October 11, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request tests

Projects

None yet

2 participants