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
23 changes: 19 additions & 4 deletions src/Module/Version/Constraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* - Version ranges in the Composer syntax, see {@see Range}: ^2.12.0, ~1.20, >=1.0 <2.0 || ^3.0
* - Feature suffixes of ranges: ^2.12.0-feature, ~1.20.0-hotfix, ^1.0.0-my-feature
* - Minimum stability, explicit or taken from the pre-releases of the constraint: ^2.12.0@beta, ^2.12.0-beta
* - Combined constraints: ^2.12.0-feature@beta
* - Combined constraints: ^2.12.0-feature@beta, ^2.12.0-beta.1-feature
* - Exact releases, a pre-release or a tail included: 3.5.0-beta.1, 2.0-beta, 1.0.0-1, 2.12.0-hotfix, 2.0.0rc1
* - Pre-release bounds of ranges and comparisons: >=3.5.0-beta.1 <3.5.0-RC1, ^1.0 || ^2.0-beta.1, >2.0-beta, ^2.0.0rc1
* - Build metadata, which does not count: >=1.2.3+build.5, 3.5.0-beta.1+5
Expand All @@ -40,8 +40,9 @@ final class Constraint implements \Stringable

/**
* @var PreRelease|null $preRelease Pre-release of a single version the constraint is bound to,
* e.g. `beta.1` of `3.5.0-beta.1`. Null when there is none, when a tail follows it, or when
* the constraint has several terms: their pre-releases stay in {@see $versionConstraint}.
* e.g. `beta.1` of `3.5.0-beta.1` or of `^3.5.0-beta.1-feature`. Null when there is none, when a tail
* follows it in an exact version, or when the constraint has several terms: their pre-releases stay
* in {@see $versionConstraint}.
*/
public readonly ?PreRelease $preRelease;

Expand Down Expand Up @@ -79,6 +80,7 @@ private function __construct(
# Without a hyphen, a tail starts with a letter, as in "2.0.0rc1" or "1.0.0.Final"; ".x" is a wildcard, a glued "x" a patch letter.
$matcher = null;
$tail = '';
$feature = '';
$preRelease = null;
if (\preg_match(
'/^(?:==?)?\s*v?(\d+(?:\.\d+)*)(-[\w.-]+|[._]?(?=[a-z])(?!(?<=\.)x(?![a-z\d]))[\w.-]+)?$/i',
Expand All @@ -97,14 +99,27 @@ private function __construct(
$singleTerm = \preg_match('/[\s,|]/', (string) \preg_replace('/^[<>=!~^]*\s*/', '', $origin)) !== 1;
$singleTerm and $tail = $bounds[0];
$preRelease = PreRelease::fromString(\ltrim($tail, '-._'));
} elseif (\preg_match(
'/^([<>=!~^]*\s*v?\d+(?:\.\d+)*)(' . Range::preReleasePattern() . ')-([\w.-]+)$/i',
$origin,
$bound,
) === 1) {
# A suffix after the pre-release bound of a single range is a feature: "^3.5.0-beta.1-feature"
/** @var non-empty-string $tail */
$tail = $bound[2];
$origin = $bound[1] . $tail;
$feature = $bound[3];
$matcher = Range::fromString($origin);
$stability ??= self::lowestStability([$tail]);
$preRelease = PreRelease::fromString(\ltrim($tail, '-._'));
}

$preRelease === null or $origin = \substr($origin, 0, -\strlen($tail));
$this->preRelease = $preRelease;

# The hyphen of a range like "1.0 - 2.0" stands between spaces and does not start a suffix
[$version, $suffix] = $matcher !== null || \preg_match('/\s-\s/', $origin) === 1
? [$origin, '']
? [$origin, $feature]
: \explode('-', $origin, 2) + [1 => ''];
if ($suffix !== '') {
// Check if suffix is a stability keyword (only if no explicit stability provided)
Expand Down
44 changes: 42 additions & 2 deletions tests/Unit/Module/Version/ConstraintTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ public static function provideValidConstraints(): \Generator
yield 'base with multiple dashes' => [
'^2.12.0-alpha.1-feature',
'^2.12.0',
'alpha.1-feature',
Stability::Preview,
'feature',
Stability::Alpha,
'Base version with multiple dashes and feature suffix',
];

Expand Down Expand Up @@ -260,6 +260,17 @@ public static function provideValidConstraints(): \Generator
yield 'bare stability keyword of a tilde with an explicit stability is a bound' => ['~2.0-beta@alpha', '~2.0', null, Stability::Alpha, 'Keyword bound of a tilde'];
yield 'pre-release bound after a comma without a space' => ['>=1.0,<2.0-RC1', '>=1.0,<2.0-RC1', null, Stability::RC, 'Comma before a bound'];

// A feature after the pre-release bound of a single range
yield 'feature after a numbered pre-release of a caret' => ['^3.5.0-beta.1-feature', '^3.5.0', 'feature', Stability::Beta, 'Feature after a caret bound'];
yield 'feature after a numbered pre-release of a tilde' => ['~3.5.0-beta.1-feature', '~3.5.0', 'feature', Stability::Beta, 'Feature after a tilde bound'];
yield 'feature after a numbered pre-release of at least' => ['>=3.5.0-beta.1-feature', '>=3.5.0', 'feature', Stability::Beta, 'Feature after an at least bound'];
yield 'feature after a glued pre-release' => ['^3.5.0-rc1-feature', '^3.5.0', 'feature', Stability::RC, 'Feature after a glued bound'];
yield 'feature after a bare stability keyword' => ['^3.5.0-beta-feature', '^3.5.0', 'feature', Stability::Beta, 'Feature after a bare keyword'];
yield 'hyphenated feature after a pre-release' => ['^3.5.0-beta.1-my-feature', '^3.5.0', 'my-feature', Stability::Beta, 'Hyphenated feature after a bound'];
yield 'feature after a pre-release with an explicit stability' => ['^3.5.0-beta.1-feature@alpha', '^3.5.0', 'feature', Stability::Alpha, 'Explicit stability over a bound'];
yield 'feature after an uppercase pre-release' => ['^3.5.0-BETA.1-Feature', '^3.5.0', 'Feature', Stability::Beta, 'Uppercase bound'];
yield 'exact pre-release before a feature' => ['3.5.0-beta.1-feature', '3.5.0-beta.1-feature', null, Stability::Beta, 'Exact release'];

// A digit within a feature suffix is no pre-release bound
yield 'numeric tail of a feature suffix' => ['^1.0.0-x64-1', '^1.0.0', 'x64-1', Stability::Preview, 'Numeric tail of a feature'];
yield 'numbered pre-release after a feature' => ['^1.0-arm64-beta.1', '^1.0', 'arm64-beta.1', Stability::Preview, 'Pre-release after a feature'];
Expand Down Expand Up @@ -659,6 +670,35 @@ public static function provideComparableConstraints(): \Generator
yield 'caret from a bare keyword before build metadata on a release candidate' => ['^1.2.3-beta+5', '1.2.3-RC1', true];
yield 'caret with build metadata like a bare keyword on a release candidate' => ['^1.2.3+5-beta', '1.2.3-RC1', false];

// A pre-release followed by a feature: exact names one release, a range filters by the feature
yield 'exact pre-release with a feature on itself' => ['3.5.0-beta.1-feature', 'v3.5.0-beta.1-feature', true];
yield 'exact pre-release with a feature in another case' => ['3.5.0-BETA.1-FEATURE', 'v3.5.0-beta.1-feature', true];
yield 'exact pre-release with a feature in another spelling' => ['3.5.0-beta1-feature', 'v3.5.0-beta.1-feature', true];
yield 'exact pre-release with a feature on the next pre-release' => ['3.5.0-beta.1-feature', 'v3.5.0-beta.2-feature', false];
yield 'exact pre-release with a feature on the pre-release alone' => ['3.5.0-beta.1-feature', 'v3.5.0-beta.1', false];
yield 'exact pre-release with a feature on the feature alone' => ['3.5.0-beta.1-feature', 'v3.5.0-feature', false];
yield 'exact pre-release with a feature on another feature' => ['3.5.0-beta.1-feature', 'v3.5.0-beta.1-other', false];
yield 'exact pre-release with a feature and an explicit stability' => ['3.5.0-beta.1-feature@beta', 'v3.5.0-beta.1-feature', true];
yield 'caret from a pre-release with a feature on itself' => ['^3.5.0-beta.1-feature', 'v3.5.0-beta.1-feature', true];
yield 'caret from an alpha with a feature on itself' => ['^2.12.0-alpha.1-feature', '2.12.0-alpha.1-feature', true];
yield 'caret from a pre-release with a feature on the next pre-release' => ['^3.5.0-beta.1-feature', 'v3.5.0-rc.1-feature', true];
yield 'caret from a pre-release with a feature on a later version' => ['^3.5.0-beta.1-feature', 'v3.5.1-beta.1-feature', true];
yield 'caret from a pre-release with a feature in another case' => ['^3.5.0-beta.1-feature', 'v3.5.0-BETA.1-Feature', true];
yield 'caret from a pre-release with a feature below the bound' => ['^3.5.0-beta.1-feature', 'v3.5.0-alpha.1-feature', false];
yield 'caret from a pre-release with a feature on an older version' => ['^3.5.0-beta.1-feature', 'v3.4.0-beta.1-feature', false];
yield 'caret from a pre-release with a feature on another feature' => ['^3.5.0-beta.1-feature', 'v3.5.0-beta.2-other', false];
yield 'caret from a pre-release with a feature on a release without it' => ['^3.5.0-beta.1-feature', 'v3.5.0-beta.2', false];
yield 'caret from a pre-release with a feature on a feature build' => ['^3.5.0-beta.1-feature', 'v3.6.0-feature', false];
yield 'caret from a pre-release with a feature and a looser stability' => ['^3.5.0-beta.1-feature@alpha', 'v3.6.0-feature', true];
yield 'caret from a pre-release with a feature and a stricter stability' => ['^3.5.0-beta.1-feature@rc', 'v3.5.0-beta.2-feature', false];
yield 'tilde from a pre-release with a feature' => ['~3.5.0-beta.1-feature', 'v3.5.2-rc.1-feature', true];
yield 'tilde from a pre-release with a feature past the minor' => ['~3.5.0-beta.1-feature', 'v3.6.0-rc.1-feature', false];
yield 'at least a pre-release with a feature' => ['>=3.5.0-beta.1-feature', 'v4.0.0-beta.1-feature', true];
yield 'at least a pre-release with a feature below the bound' => ['>=3.5.0-beta.2-feature', 'v3.5.0-beta.1-feature', false];
yield 'at least a glued pre-release with a feature' => ['>=3.5.0rc1-feature', 'v3.5.0-RC2-feature', true];
yield 'feature with a stability instead of a pre-release' => ['^3.5.0-feature@beta', 'v3.5.0-beta.1-feature', true];
yield 'bare stability keyword with a feature below the keyword' => ['^3.5.0-beta-feature@alpha', 'v3.5.0-alpha.1-feature', false];

// A pre-release glued to the number
yield 'caret from a glued release candidate on itself' => ['^2.0.0rc1', '2.0.0rc1', true];
yield 'caret from a glued release candidate on the next one' => ['^2.0.0rc1', 'v2.0.0-RC2', true];
Expand Down
Loading