Skip to content

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

Open
roxblnfk wants to merge 3 commits into
fix/exact-prereleasefrom
feat/own-constraint-matcher
Open

roxblnfk wants to merge 3 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.

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

Development

Successfully merging this pull request may close these issues.

2 participants