├── LICENSE ├── composer.json ├── src ├── Constraint │ ├── MatchNoneConstraint.php │ ├── MatchAllConstraint.php │ ├── ConstraintInterface.php │ ├── Bound.php │ ├── MultiConstraint.php │ └── Constraint.php ├── Interval.php ├── Comparator.php ├── CompilingMatcher.php ├── Semver.php ├── Intervals.php └── VersionParser.php ├── README.md └── CHANGELOG.md /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Composer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "composer/semver", 3 | "description": "Version comparison library that offers utilities, version constraint parsing and validation.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "semver", 8 | "semantic", 9 | "versioning", 10 | "validation" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Nils Adermann", 15 | "email": "naderman@naderman.de", 16 | "homepage": "http://www.naderman.de" 17 | }, 18 | { 19 | "name": "Jordi Boggiano", 20 | "email": "j.boggiano@seld.be", 21 | "homepage": "http://seld.be" 22 | }, 23 | { 24 | "name": "Rob Bast", 25 | "email": "rob.bast@gmail.com", 26 | "homepage": "http://robbast.nl" 27 | } 28 | ], 29 | "support": { 30 | "irc": "ircs://irc.libera.chat:6697/composer", 31 | "issues": "https://github.com/composer/semver/issues" 32 | }, 33 | "require": { 34 | "php": "^5.3.2 || ^7.0 || ^8.0" 35 | }, 36 | "require-dev": { 37 | "symfony/phpunit-bridge": "^3 || ^7", 38 | "phpstan/phpstan": "^1.11" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Composer\\Semver\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Composer\\Semver\\": "tests" 48 | } 49 | }, 50 | "extra": { 51 | "branch-alias": { 52 | "dev-main": "3.x-dev" 53 | } 54 | }, 55 | "scripts": { 56 | "test": "SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1 vendor/bin/simple-phpunit", 57 | "phpstan": "@php vendor/bin/phpstan analyse" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Constraint/MatchNoneConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver\Constraint; 13 | 14 | /** 15 | * Blackhole of constraints, nothing escapes it 16 | */ 17 | class MatchNoneConstraint implements ConstraintInterface 18 | { 19 | /** @var string|null */ 20 | protected $prettyString; 21 | 22 | /** 23 | * @param ConstraintInterface $provider 24 | * 25 | * @return bool 26 | */ 27 | public function matches(ConstraintInterface $provider) 28 | { 29 | return false; 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function compile($otherOperator) 36 | { 37 | return 'false'; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function setPrettyString($prettyString) 44 | { 45 | $this->prettyString = $prettyString; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function getPrettyString() 52 | { 53 | if ($this->prettyString) { 54 | return $this->prettyString; 55 | } 56 | 57 | return (string) $this; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | */ 63 | public function __toString() 64 | { 65 | return '[]'; 66 | } 67 | 68 | /** 69 | * {@inheritDoc} 70 | */ 71 | public function getUpperBound() 72 | { 73 | return new Bound('0.0.0.0-dev', false); 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | public function getLowerBound() 80 | { 81 | return new Bound('0.0.0.0-dev', false); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Constraint/MatchAllConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver\Constraint; 13 | 14 | /** 15 | * Defines the absence of a constraint. 16 | * 17 | * This constraint matches everything. 18 | */ 19 | class MatchAllConstraint implements ConstraintInterface 20 | { 21 | /** @var string|null */ 22 | protected $prettyString; 23 | 24 | /** 25 | * @param ConstraintInterface $provider 26 | * 27 | * @return bool 28 | */ 29 | public function matches(ConstraintInterface $provider) 30 | { 31 | return true; 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function compile($otherOperator) 38 | { 39 | return 'true'; 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function setPrettyString($prettyString) 46 | { 47 | $this->prettyString = $prettyString; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function getPrettyString() 54 | { 55 | if ($this->prettyString) { 56 | return $this->prettyString; 57 | } 58 | 59 | return (string) $this; 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function __toString() 66 | { 67 | return '*'; 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public function getUpperBound() 74 | { 75 | return Bound::positiveInfinity(); 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | */ 81 | public function getLowerBound() 82 | { 83 | return Bound::zero(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Constraint/ConstraintInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver\Constraint; 13 | 14 | /** 15 | * DO NOT IMPLEMENT this interface. It is only meant for usage as a type hint 16 | * in libraries relying on composer/semver but creating your own constraint class 17 | * that implements this interface is not a supported use case and will cause the 18 | * composer/semver components to return unexpected results. 19 | */ 20 | interface ConstraintInterface 21 | { 22 | /** 23 | * Checks whether the given constraint intersects in any way with this constraint 24 | * 25 | * @param ConstraintInterface $provider 26 | * 27 | * @return bool 28 | */ 29 | public function matches(ConstraintInterface $provider); 30 | 31 | /** 32 | * Provides a compiled version of the constraint for the given operator 33 | * The compiled version must be a PHP expression. 34 | * Executor of compile version must provide 2 variables: 35 | * - $v = the string version to compare with 36 | * - $b = whether or not the version is a non-comparable branch (starts with "dev-") 37 | * 38 | * @see Constraint::OP_* for the list of available operators. 39 | * @example return '!$b && version_compare($v, '1.0', '>')'; 40 | * 41 | * @param int $otherOperator one Constraint::OP_* 42 | * 43 | * @return string 44 | * 45 | * @phpstan-param Constraint::OP_* $otherOperator 46 | */ 47 | public function compile($otherOperator); 48 | 49 | /** 50 | * @return Bound 51 | */ 52 | public function getUpperBound(); 53 | 54 | /** 55 | * @return Bound 56 | */ 57 | public function getLowerBound(); 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getPrettyString(); 63 | 64 | /** 65 | * @param string|null $prettyString 66 | * 67 | * @return void 68 | */ 69 | public function setPrettyString($prettyString); 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function __toString(); 75 | } 76 | -------------------------------------------------------------------------------- /src/Interval.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver; 13 | 14 | use Composer\Semver\Constraint\Constraint; 15 | 16 | class Interval 17 | { 18 | /** @var Constraint */ 19 | private $start; 20 | /** @var Constraint */ 21 | private $end; 22 | 23 | public function __construct(Constraint $start, Constraint $end) 24 | { 25 | $this->start = $start; 26 | $this->end = $end; 27 | } 28 | 29 | /** 30 | * @return Constraint 31 | */ 32 | public function getStart() 33 | { 34 | return $this->start; 35 | } 36 | 37 | /** 38 | * @return Constraint 39 | */ 40 | public function getEnd() 41 | { 42 | return $this->end; 43 | } 44 | 45 | /** 46 | * @return Constraint 47 | */ 48 | public static function fromZero() 49 | { 50 | static $zero; 51 | 52 | if (null === $zero) { 53 | $zero = new Constraint('>=', '0.0.0.0-dev'); 54 | } 55 | 56 | return $zero; 57 | } 58 | 59 | /** 60 | * @return Constraint 61 | */ 62 | public static function untilPositiveInfinity() 63 | { 64 | static $positiveInfinity; 65 | 66 | if (null === $positiveInfinity) { 67 | $positiveInfinity = new Constraint('<', PHP_INT_MAX.'.0.0.0'); 68 | } 69 | 70 | return $positiveInfinity; 71 | } 72 | 73 | /** 74 | * @return self 75 | */ 76 | public static function any() 77 | { 78 | return new self(self::fromZero(), self::untilPositiveInfinity()); 79 | } 80 | 81 | /** 82 | * @return array{'names': string[], 'exclude': bool} 83 | */ 84 | public static function anyDev() 85 | { 86 | // any == exclude nothing 87 | return array('names' => array(), 'exclude' => true); 88 | } 89 | 90 | /** 91 | * @return array{'names': string[], 'exclude': bool} 92 | */ 93 | public static function noDev() 94 | { 95 | // nothing == no names included 96 | return array('names' => array(), 'exclude' => false); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Comparator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver; 13 | 14 | use Composer\Semver\Constraint\Constraint; 15 | 16 | class Comparator 17 | { 18 | /** 19 | * Evaluates the expression: $version1 > $version2. 20 | * 21 | * @param string $version1 22 | * @param string $version2 23 | * 24 | * @return bool 25 | */ 26 | public static function greaterThan($version1, $version2) 27 | { 28 | return self::compare($version1, '>', $version2); 29 | } 30 | 31 | /** 32 | * Evaluates the expression: $version1 >= $version2. 33 | * 34 | * @param string $version1 35 | * @param string $version2 36 | * 37 | * @return bool 38 | */ 39 | public static function greaterThanOrEqualTo($version1, $version2) 40 | { 41 | return self::compare($version1, '>=', $version2); 42 | } 43 | 44 | /** 45 | * Evaluates the expression: $version1 < $version2. 46 | * 47 | * @param string $version1 48 | * @param string $version2 49 | * 50 | * @return bool 51 | */ 52 | public static function lessThan($version1, $version2) 53 | { 54 | return self::compare($version1, '<', $version2); 55 | } 56 | 57 | /** 58 | * Evaluates the expression: $version1 <= $version2. 59 | * 60 | * @param string $version1 61 | * @param string $version2 62 | * 63 | * @return bool 64 | */ 65 | public static function lessThanOrEqualTo($version1, $version2) 66 | { 67 | return self::compare($version1, '<=', $version2); 68 | } 69 | 70 | /** 71 | * Evaluates the expression: $version1 == $version2. 72 | * 73 | * @param string $version1 74 | * @param string $version2 75 | * 76 | * @return bool 77 | */ 78 | public static function equalTo($version1, $version2) 79 | { 80 | return self::compare($version1, '==', $version2); 81 | } 82 | 83 | /** 84 | * Evaluates the expression: $version1 != $version2. 85 | * 86 | * @param string $version1 87 | * @param string $version2 88 | * 89 | * @return bool 90 | */ 91 | public static function notEqualTo($version1, $version2) 92 | { 93 | return self::compare($version1, '!=', $version2); 94 | } 95 | 96 | /** 97 | * Evaluates the expression: $version1 $operator $version2. 98 | * 99 | * @param string $version1 100 | * @param string $operator 101 | * @param string $version2 102 | * 103 | * @return bool 104 | * 105 | * @phpstan-param Constraint::STR_OP_* $operator 106 | */ 107 | public static function compare($version1, $operator, $version2) 108 | { 109 | $constraint = new Constraint($operator, $version2); 110 | 111 | return $constraint->matchSpecific(new Constraint('==', $version1), true); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/CompilingMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver; 13 | 14 | use Composer\Semver\Constraint\Constraint; 15 | use Composer\Semver\Constraint\ConstraintInterface; 16 | 17 | /** 18 | * Helper class to evaluate constraint by compiling and reusing the code to evaluate 19 | */ 20 | class CompilingMatcher 21 | { 22 | /** 23 | * @var array 24 | * @phpstan-var array 25 | */ 26 | private static $compiledCheckerCache = array(); 27 | /** 28 | * @var array 29 | * @phpstan-var array 30 | */ 31 | private static $resultCache = array(); 32 | 33 | /** @var bool */ 34 | private static $enabled; 35 | 36 | /** 37 | * @phpstan-var array 38 | */ 39 | private static $transOpInt = array( 40 | Constraint::OP_EQ => Constraint::STR_OP_EQ, 41 | Constraint::OP_LT => Constraint::STR_OP_LT, 42 | Constraint::OP_LE => Constraint::STR_OP_LE, 43 | Constraint::OP_GT => Constraint::STR_OP_GT, 44 | Constraint::OP_GE => Constraint::STR_OP_GE, 45 | Constraint::OP_NE => Constraint::STR_OP_NE, 46 | ); 47 | 48 | /** 49 | * Clears the memoization cache once you are done 50 | * 51 | * @return void 52 | */ 53 | public static function clear() 54 | { 55 | self::$resultCache = array(); 56 | self::$compiledCheckerCache = array(); 57 | } 58 | 59 | /** 60 | * Evaluates the expression: $constraint match $operator $version 61 | * 62 | * @param ConstraintInterface $constraint 63 | * @param int $operator 64 | * @phpstan-param Constraint::OP_* $operator 65 | * @param string $version 66 | * 67 | * @return bool 68 | */ 69 | public static function match(ConstraintInterface $constraint, $operator, $version) 70 | { 71 | $resultCacheKey = $operator.$constraint.';'.$version; 72 | 73 | if (isset(self::$resultCache[$resultCacheKey])) { 74 | return self::$resultCache[$resultCacheKey]; 75 | } 76 | 77 | if (self::$enabled === null) { 78 | self::$enabled = !\in_array('eval', explode(',', (string) ini_get('disable_functions')), true); 79 | } 80 | if (!self::$enabled) { 81 | return self::$resultCache[$resultCacheKey] = $constraint->matches(new Constraint(self::$transOpInt[$operator], $version)); 82 | } 83 | 84 | $cacheKey = $operator.$constraint; 85 | if (!isset(self::$compiledCheckerCache[$cacheKey])) { 86 | $code = $constraint->compile($operator); 87 | self::$compiledCheckerCache[$cacheKey] = $function = eval('return function($v, $b){return '.$code.';};'); 88 | } else { 89 | $function = self::$compiledCheckerCache[$cacheKey]; 90 | } 91 | 92 | return self::$resultCache[$resultCacheKey] = $function($version, strpos($version, 'dev-') === 0); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Constraint/Bound.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver\Constraint; 13 | 14 | class Bound 15 | { 16 | /** 17 | * @var string 18 | */ 19 | private $version; 20 | 21 | /** 22 | * @var bool 23 | */ 24 | private $isInclusive; 25 | 26 | /** 27 | * @param string $version 28 | * @param bool $isInclusive 29 | */ 30 | public function __construct($version, $isInclusive) 31 | { 32 | $this->version = $version; 33 | $this->isInclusive = $isInclusive; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getVersion() 40 | { 41 | return $this->version; 42 | } 43 | 44 | /** 45 | * @return bool 46 | */ 47 | public function isInclusive() 48 | { 49 | return $this->isInclusive; 50 | } 51 | 52 | /** 53 | * @return bool 54 | */ 55 | public function isZero() 56 | { 57 | return $this->getVersion() === '0.0.0.0-dev' && $this->isInclusive(); 58 | } 59 | 60 | /** 61 | * @return bool 62 | */ 63 | public function isPositiveInfinity() 64 | { 65 | return $this->getVersion() === PHP_INT_MAX.'.0.0.0' && !$this->isInclusive(); 66 | } 67 | 68 | /** 69 | * Compares a bound to another with a given operator. 70 | * 71 | * @param Bound $other 72 | * @param string $operator 73 | * 74 | * @return bool 75 | */ 76 | public function compareTo(Bound $other, $operator) 77 | { 78 | if (!\in_array($operator, array('<', '>'), true)) { 79 | throw new \InvalidArgumentException('Does not support any other operator other than > or <.'); 80 | } 81 | 82 | // If they are the same it doesn't matter 83 | if ($this == $other) { 84 | return false; 85 | } 86 | 87 | $compareResult = version_compare($this->getVersion(), $other->getVersion()); 88 | 89 | // Not the same version means we don't need to check if the bounds are inclusive or not 90 | if (0 !== $compareResult) { 91 | return (('>' === $operator) ? 1 : -1) === $compareResult; 92 | } 93 | 94 | // Question we're answering here is "am I higher than $other?" 95 | return '>' === $operator ? $other->isInclusive() : !$other->isInclusive(); 96 | } 97 | 98 | public function __toString() 99 | { 100 | return sprintf( 101 | '%s [%s]', 102 | $this->getVersion(), 103 | $this->isInclusive() ? 'inclusive' : 'exclusive' 104 | ); 105 | } 106 | 107 | /** 108 | * @return self 109 | */ 110 | public static function zero() 111 | { 112 | return new Bound('0.0.0.0-dev', true); 113 | } 114 | 115 | /** 116 | * @return self 117 | */ 118 | public static function positiveInfinity() 119 | { 120 | return new Bound(PHP_INT_MAX.'.0.0.0', false); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Semver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver; 13 | 14 | use Composer\Semver\Constraint\Constraint; 15 | 16 | class Semver 17 | { 18 | const SORT_ASC = 1; 19 | const SORT_DESC = -1; 20 | 21 | /** @var VersionParser */ 22 | private static $versionParser; 23 | 24 | /** 25 | * Determine if given version satisfies given constraints. 26 | * 27 | * @param string $version 28 | * @param string $constraints 29 | * 30 | * @return bool 31 | */ 32 | public static function satisfies($version, $constraints) 33 | { 34 | if (null === self::$versionParser) { 35 | self::$versionParser = new VersionParser(); 36 | } 37 | 38 | $versionParser = self::$versionParser; 39 | $provider = new Constraint('==', $versionParser->normalize($version)); 40 | $parsedConstraints = $versionParser->parseConstraints($constraints); 41 | 42 | return $parsedConstraints->matches($provider); 43 | } 44 | 45 | /** 46 | * Return all versions that satisfy given constraints. 47 | * 48 | * @param string[] $versions 49 | * @param string $constraints 50 | * 51 | * @return list 52 | */ 53 | public static function satisfiedBy(array $versions, $constraints) 54 | { 55 | $versions = array_filter($versions, function ($version) use ($constraints) { 56 | return Semver::satisfies($version, $constraints); 57 | }); 58 | 59 | return array_values($versions); 60 | } 61 | 62 | /** 63 | * Sort given array of versions. 64 | * 65 | * @param string[] $versions 66 | * 67 | * @return list 68 | */ 69 | public static function sort(array $versions) 70 | { 71 | return self::usort($versions, self::SORT_ASC); 72 | } 73 | 74 | /** 75 | * Sort given array of versions in reverse. 76 | * 77 | * @param string[] $versions 78 | * 79 | * @return list 80 | */ 81 | public static function rsort(array $versions) 82 | { 83 | return self::usort($versions, self::SORT_DESC); 84 | } 85 | 86 | /** 87 | * @param string[] $versions 88 | * @param int $direction 89 | * 90 | * @return list 91 | */ 92 | private static function usort(array $versions, $direction) 93 | { 94 | if (null === self::$versionParser) { 95 | self::$versionParser = new VersionParser(); 96 | } 97 | 98 | $versionParser = self::$versionParser; 99 | $normalized = array(); 100 | 101 | // Normalize outside of usort() scope for minor performance increase. 102 | // Creates an array of arrays: [[normalized, key], ...] 103 | foreach ($versions as $key => $version) { 104 | $normalizedVersion = $versionParser->normalize($version); 105 | $normalizedVersion = $versionParser->normalizeDefaultBranch($normalizedVersion); 106 | $normalized[] = array($normalizedVersion, $key); 107 | } 108 | 109 | usort($normalized, function (array $left, array $right) use ($direction) { 110 | if ($left[0] === $right[0]) { 111 | return 0; 112 | } 113 | 114 | if (Comparator::lessThan($left[0], $right[0])) { 115 | return -$direction; 116 | } 117 | 118 | return $direction; 119 | }); 120 | 121 | // Recreate input array, using the original indexes which are now in sorted order. 122 | $sorted = array(); 123 | foreach ($normalized as $item) { 124 | $sorted[] = $versions[$item[1]]; 125 | } 126 | 127 | return $sorted; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | composer/semver 2 | =============== 3 | 4 | Version comparison library that offers utilities, version constraint parsing and validation. 5 | 6 | It follows semver (semantic versioning) where possible but is also constrained by 7 | `version_compare` and backwards compatibility and as such cannot implement semver strictly. 8 | 9 | Originally written as part of [composer/composer](https://github.com/composer/composer), 10 | now extracted and made available as a stand-alone library. 11 | 12 | [![Continuous Integration](https://github.com/composer/semver/actions/workflows/continuous-integration.yml/badge.svg?branch=main)](https://github.com/composer/semver/actions/workflows/continuous-integration.yml) 13 | [![PHP Lint](https://github.com/composer/semver/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/composer/semver/actions/workflows/lint.yml) 14 | [![PHPStan](https://github.com/composer/semver/actions/workflows/phpstan.yml/badge.svg?branch=main)](https://github.com/composer/semver/actions/workflows/phpstan.yml) 15 | 16 | Installation 17 | ------------ 18 | 19 | Install the latest version with: 20 | 21 | ```bash 22 | composer require composer/semver 23 | ``` 24 | 25 | 26 | Requirements 27 | ------------ 28 | 29 | * PHP 5.3.2 is required but using the latest version of PHP is highly recommended. 30 | 31 | 32 | Version Comparison 33 | ------------------ 34 | 35 | For details on how versions are compared, refer to the [Versions](https://getcomposer.org/doc/articles/versions.md) 36 | article in the documentation section of the [getcomposer.org](https://getcomposer.org) website. 37 | 38 | 39 | Basic usage 40 | ----------- 41 | 42 | ### Validation / Normalization 43 | 44 | The [`Composer\Semver\VersionParser`](https://github.com/composer/semver/blob/main/src/VersionParser.php) 45 | class provides the following methods for parsing, normalizing and validating versions and constraints. 46 | 47 | Numeric versions are normalized to a 4 component versions (e.g. `1.2.3` is normalized to `1.2.3.0`) 48 | for internal consistency and compatibility with `version_compare`. Normalized versions are used for 49 | constraints internally but should not be shown to end users. 50 | 51 | For versions: 52 | 53 | * isValid($version) 54 | * normalize($version, $fullVersion = null) 55 | * normalizeBranch($name) 56 | * normalizeDefaultBranch($name) 57 | 58 | For constraints: 59 | 60 | * parseConstraints($constraints) 61 | 62 | For stabilities: 63 | 64 | * parseStability($version) 65 | * normalizeStability($stability) 66 | 67 | ### Comparison 68 | 69 | The [`Composer\Semver\Comparator`](https://github.com/composer/semver/blob/main/src/Comparator.php) class provides the following methods for comparing versions: 70 | 71 | * greaterThan($v1, $v2) 72 | * greaterThanOrEqualTo($v1, $v2) 73 | * lessThan($v1, $v2) 74 | * lessThanOrEqualTo($v1, $v2) 75 | * equalTo($v1, $v2) 76 | * notEqualTo($v1, $v2) 77 | 78 | Each function takes two version strings as arguments and returns a boolean. For example: 79 | 80 | ```php 81 | use Composer\Semver\Comparator; 82 | 83 | Comparator::greaterThan('1.25.0', '1.24.0'); // 1.25.0 > 1.24.0 84 | ``` 85 | 86 | ### Semver 87 | 88 | The [`Composer\Semver\Semver`](https://github.com/composer/semver/blob/main/src/Semver.php) class provides the following methods: 89 | 90 | * satisfies($version, $constraints) 91 | * satisfiedBy(array $versions, $constraint) 92 | * sort($versions) 93 | * rsort($versions) 94 | 95 | ### Intervals 96 | 97 | The [`Composer\Semver\Intervals`](https://github.com/composer/semver/blob/main/src/Intervals.php) static class provides 98 | a few utilities to work with complex constraints or read version intervals from a constraint: 99 | 100 | ```php 101 | use Composer\Semver\Intervals; 102 | 103 | // Checks whether $candidate is a subset of $constraint 104 | Intervals::isSubsetOf(ConstraintInterface $candidate, ConstraintInterface $constraint); 105 | 106 | // Checks whether $a and $b have any intersection, equivalent to $a->matches($b) 107 | Intervals::haveIntersections(ConstraintInterface $a, ConstraintInterface $b); 108 | 109 | // Optimizes a complex multi constraint by merging all intervals down to the smallest 110 | // possible multi constraint. The drawbacks are this is not very fast, and the resulting 111 | // multi constraint will have no human readable prettyConstraint configured on it 112 | Intervals::compactConstraint(ConstraintInterface $constraint); 113 | 114 | // Creates an array of numeric intervals and branch constraints representing a given constraint 115 | Intervals::get(ConstraintInterface $constraint); 116 | 117 | // Clears the memoization cache when you are done processing constraints 118 | Intervals::clear() 119 | ``` 120 | 121 | See the class docblocks for more details. 122 | 123 | 124 | License 125 | ------- 126 | 127 | composer/semver is licensed under the MIT License, see the LICENSE file for details. 128 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ### [3.4.3] 2024-09-19 7 | 8 | * Fixed some type annotations 9 | 10 | ### [3.4.2] 2024-07-12 11 | 12 | * Fixed PHP 5.3 syntax error 13 | 14 | ### [3.4.1] 2024-07-12 15 | 16 | * Fixed normalizeStability's return type to enforce valid stabilities 17 | 18 | ### [3.4.0] 2023-08-31 19 | 20 | * Support larger major version numbers (#149) 21 | 22 | ### [3.3.2] 2022-04-01 23 | 24 | * Fixed handling of non-string values (#134) 25 | 26 | ### [3.3.1] 2022-03-16 27 | 28 | * Fixed possible cache key clash in the CompilingMatcher memoization (#132) 29 | 30 | ### [3.3.0] 2022-03-15 31 | 32 | * Improved performance of CompilingMatcher by memoizing more (#131) 33 | * Added CompilingMatcher::clear to clear all memoization caches 34 | 35 | ### [3.2.9] 2022-02-04 36 | 37 | * Revert #129 (Fixed MultiConstraint with MatchAllConstraint) which caused regressions 38 | 39 | ### [3.2.8] 2022-02-04 40 | 41 | * Updates to latest phpstan / CI by @Seldaek in https://github.com/composer/semver/pull/130 42 | * Fixed MultiConstraint with MatchAllConstraint by @Toflar in https://github.com/composer/semver/pull/129 43 | 44 | ### [3.2.7] 2022-01-04 45 | 46 | * Fixed: typo in type definition of Intervals class causing issues with Psalm scanning vendors 47 | 48 | ### [3.2.6] 2021-10-25 49 | 50 | * Fixed: type improvements to parseStability 51 | 52 | ### [3.2.5] 2021-05-24 53 | 54 | * Fixed: issue comparing disjunctive MultiConstraints to conjunctive ones (#127) 55 | * Fixed: added complete type information using phpstan annotations 56 | 57 | ### [3.2.4] 2020-11-13 58 | 59 | * Fixed: code clean-up 60 | 61 | ### [3.2.3] 2020-11-12 62 | 63 | * Fixed: constraints in the form of `X || Y, >=Y.1` and other such complex constructs were in some cases being optimized into a more restrictive constraint 64 | 65 | ### [3.2.2] 2020-10-14 66 | 67 | * Fixed: internal code cleanups 68 | 69 | ### [3.2.1] 2020-09-27 70 | 71 | * Fixed: accidental validation of broken constraints combining ^/~ and wildcards, and -dev suffix allowing weird cases 72 | * Fixed: normalization of beta0 and such which was dropping the 0 73 | 74 | ### [3.2.0] 2020-09-09 75 | 76 | * Added: support for `x || @dev`, not very useful but seen in the wild and failed to validate with 1.5.2/1.6.0 77 | * Added: support for `foobar-dev` being equal to `dev-foobar`, dev-foobar is the official way to write it but we need to support the other for BC and convenience 78 | 79 | ### [3.1.0] 2020-09-08 80 | 81 | * Added: support for constraints like `^2.x-dev` and `~2.x-dev`, not very useful but seen in the wild and failed to validate with 3.0.1 82 | * Fixed: invalid aliases will no longer throw, unless explicitly validated by Composer in the root package 83 | 84 | ### [3.0.1] 2020-09-08 85 | 86 | * Fixed: handling of some invalid -dev versions which were seen as valid 87 | 88 | ### [3.0.0] 2020-05-26 89 | 90 | * Break: Renamed `EmptyConstraint`, replace it with `MatchAllConstraint` 91 | * Break: Unlikely to affect anyone but strictly speaking a breaking change, `*.*` and such variants will not match all `dev-*` versions anymore, only `*` does 92 | * Break: ConstraintInterface is now considered internal/private and not meant to be implemented by third parties anymore 93 | * Added `Intervals` class to check if a constraint is a subsets of another one, and allow compacting complex MultiConstraints into simpler ones 94 | * Added `CompilingMatcher` class to speed up constraint matching against simple Constraint instances 95 | * Added `MatchAllConstraint` and `MatchNoneConstraint` which match everything and nothing 96 | * Added more advanced optimization of contiguous constraints inside MultiConstraint 97 | * Added tentative support for PHP 8 98 | * Fixed ConstraintInterface::matches to be commutative in all cases 99 | 100 | ### [2.0.0] 2020-04-21 101 | 102 | * Break: `dev-master`, `dev-trunk` and `dev-default` now normalize to `dev-master`, `dev-trunk` and `dev-default` instead of `9999999-dev` in 1.x 103 | * Break: Removed the deprecated `AbstractConstraint` 104 | * Added `getUpperBound` and `getLowerBound` to ConstraintInterface. They return `Composer\Semver\Constraint\Bound` instances 105 | * Added `MultiConstraint::create` to create the most-optimal form of ConstraintInterface from an array of constraint strings 106 | 107 | ### [1.7.2] 2020-12-03 108 | 109 | * Fixed: Allow installing on php 8 110 | 111 | ### [1.7.1] 2020-09-27 112 | 113 | * Fixed: accidental validation of broken constraints combining ^/~ and wildcards, and -dev suffix allowing weird cases 114 | * Fixed: normalization of beta0 and such which was dropping the 0 115 | 116 | ### [1.7.0] 2020-09-09 117 | 118 | * Added: support for `x || @dev`, not very useful but seen in the wild and failed to validate with 1.5.2/1.6.0 119 | * Added: support for `foobar-dev` being equal to `dev-foobar`, dev-foobar is the official way to write it but we need to support the other for BC and convenience 120 | 121 | ### [1.6.0] 2020-09-08 122 | 123 | * Added: support for constraints like `^2.x-dev` and `~2.x-dev`, not very useful but seen in the wild and failed to validate with 1.5.2 124 | * Fixed: invalid aliases will no longer throw, unless explicitly validated by Composer in the root package 125 | 126 | ### [1.5.2] 2020-09-08 127 | 128 | * Fixed: handling of some invalid -dev versions which were seen as valid 129 | * Fixed: some doctypes 130 | 131 | ### [1.5.1] 2020-01-13 132 | 133 | * Fixed: Parsing of aliased version was not validating the alias to be a valid version 134 | 135 | ### [1.5.0] 2019-03-19 136 | 137 | * Added: some support for date versions (e.g. 201903) in `~` operator 138 | * Fixed: support for stabilities in `~` operator was inconsistent 139 | 140 | ### [1.4.2] 2016-08-30 141 | 142 | * Fixed: collapsing of complex constraints lead to buggy constraints 143 | 144 | ### [1.4.1] 2016-06-02 145 | 146 | * Changed: branch-like requirements no longer strip build metadata - [composer/semver#38](https://github.com/composer/semver/pull/38). 147 | 148 | ### [1.4.0] 2016-03-30 149 | 150 | * Added: getters on MultiConstraint - [composer/semver#35](https://github.com/composer/semver/pull/35). 151 | 152 | ### [1.3.0] 2016-02-25 153 | 154 | * Fixed: stability parsing - [composer/composer#1234](https://github.com/composer/composer/issues/4889). 155 | * Changed: collapse contiguous constraints when possible. 156 | 157 | ### [1.2.0] 2015-11-10 158 | 159 | * Changed: allow multiple numerical identifiers in 'pre-release' version part. 160 | * Changed: add more 'v' prefix support. 161 | 162 | ### [1.1.0] 2015-11-03 163 | 164 | * Changed: dropped redundant `test` namespace. 165 | * Changed: minor adjustment in datetime parsing normalization. 166 | * Changed: `ConstraintInterface` relaxed, setPrettyString is not required anymore. 167 | * Changed: `AbstractConstraint` marked deprecated, will be removed in 2.0. 168 | * Changed: `Constraint` is now extensible. 169 | 170 | ### [1.0.0] 2015-09-21 171 | 172 | * Break: `VersionConstraint` renamed to `Constraint`. 173 | * Break: `SpecificConstraint` renamed to `AbstractConstraint`. 174 | * Break: `LinkConstraintInterface` renamed to `ConstraintInterface`. 175 | * Break: `VersionParser::parseNameVersionPairs` was removed. 176 | * Changed: `VersionParser::parseConstraints` allows (but ignores) build metadata now. 177 | * Changed: `VersionParser::parseConstraints` allows (but ignores) prefixing numeric versions with a 'v' now. 178 | * Changed: Fixed namespace(s) of test files. 179 | * Changed: `Comparator::compare` no longer throws `InvalidArgumentException`. 180 | * Changed: `Constraint` now throws `InvalidArgumentException`. 181 | 182 | ### [0.1.0] 2015-07-23 183 | 184 | * Added: `Composer\Semver\Comparator`, various methods to compare versions. 185 | * Added: various documents such as README.md, LICENSE, etc. 186 | * Added: configuration files for Git, Travis, php-cs-fixer, phpunit. 187 | * Break: the following namespaces were renamed: 188 | - Namespace: `Composer\Package\Version` -> `Composer\Semver` 189 | - Namespace: `Composer\Package\LinkConstraint` -> `Composer\Semver\Constraint` 190 | - Namespace: `Composer\Test\Package\Version` -> `Composer\Test\Semver` 191 | - Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint` 192 | * Changed: code style using php-cs-fixer. 193 | 194 | [3.4.3]: https://github.com/composer/semver/compare/3.4.2...3.4.3 195 | [3.4.2]: https://github.com/composer/semver/compare/3.4.1...3.4.2 196 | [3.4.1]: https://github.com/composer/semver/compare/3.4.0...3.4.1 197 | [3.4.0]: https://github.com/composer/semver/compare/3.3.2...3.4.0 198 | [3.3.2]: https://github.com/composer/semver/compare/3.3.1...3.3.2 199 | [3.3.1]: https://github.com/composer/semver/compare/3.3.0...3.3.1 200 | [3.3.0]: https://github.com/composer/semver/compare/3.2.9...3.3.0 201 | [3.2.9]: https://github.com/composer/semver/compare/3.2.8...3.2.9 202 | [3.2.8]: https://github.com/composer/semver/compare/3.2.7...3.2.8 203 | [3.2.7]: https://github.com/composer/semver/compare/3.2.6...3.2.7 204 | [3.2.6]: https://github.com/composer/semver/compare/3.2.5...3.2.6 205 | [3.2.5]: https://github.com/composer/semver/compare/3.2.4...3.2.5 206 | [3.2.4]: https://github.com/composer/semver/compare/3.2.3...3.2.4 207 | [3.2.3]: https://github.com/composer/semver/compare/3.2.2...3.2.3 208 | [3.2.2]: https://github.com/composer/semver/compare/3.2.1...3.2.2 209 | [3.2.1]: https://github.com/composer/semver/compare/3.2.0...3.2.1 210 | [3.2.0]: https://github.com/composer/semver/compare/3.1.0...3.2.0 211 | [3.1.0]: https://github.com/composer/semver/compare/3.0.1...3.1.0 212 | [3.0.1]: https://github.com/composer/semver/compare/3.0.0...3.0.1 213 | [3.0.0]: https://github.com/composer/semver/compare/2.0.0...3.0.0 214 | [2.0.0]: https://github.com/composer/semver/compare/1.5.1...2.0.0 215 | [1.7.2]: https://github.com/composer/semver/compare/1.7.1...1.7.2 216 | [1.7.1]: https://github.com/composer/semver/compare/1.7.0...1.7.1 217 | [1.7.0]: https://github.com/composer/semver/compare/1.6.0...1.7.0 218 | [1.6.0]: https://github.com/composer/semver/compare/1.5.2...1.6.0 219 | [1.5.2]: https://github.com/composer/semver/compare/1.5.1...1.5.2 220 | [1.5.1]: https://github.com/composer/semver/compare/1.5.0...1.5.1 221 | [1.5.0]: https://github.com/composer/semver/compare/1.4.2...1.5.0 222 | [1.4.2]: https://github.com/composer/semver/compare/1.4.1...1.4.2 223 | [1.4.1]: https://github.com/composer/semver/compare/1.4.0...1.4.1 224 | [1.4.0]: https://github.com/composer/semver/compare/1.3.0...1.4.0 225 | [1.3.0]: https://github.com/composer/semver/compare/1.2.0...1.3.0 226 | [1.2.0]: https://github.com/composer/semver/compare/1.1.0...1.2.0 227 | [1.1.0]: https://github.com/composer/semver/compare/1.0.0...1.1.0 228 | [1.0.0]: https://github.com/composer/semver/compare/0.1.0...1.0.0 229 | [0.1.0]: https://github.com/composer/semver/compare/5e0b9a4da...0.1.0 230 | -------------------------------------------------------------------------------- /src/Constraint/MultiConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver\Constraint; 13 | 14 | /** 15 | * Defines a conjunctive or disjunctive set of constraints. 16 | */ 17 | class MultiConstraint implements ConstraintInterface 18 | { 19 | /** 20 | * @var ConstraintInterface[] 21 | * @phpstan-var non-empty-array 22 | */ 23 | protected $constraints; 24 | 25 | /** @var string|null */ 26 | protected $prettyString; 27 | 28 | /** @var string|null */ 29 | protected $string; 30 | 31 | /** @var bool */ 32 | protected $conjunctive; 33 | 34 | /** @var Bound|null */ 35 | protected $lowerBound; 36 | 37 | /** @var Bound|null */ 38 | protected $upperBound; 39 | 40 | /** 41 | * @param ConstraintInterface[] $constraints A set of constraints 42 | * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive 43 | * 44 | * @throws \InvalidArgumentException If less than 2 constraints are passed 45 | */ 46 | public function __construct(array $constraints, $conjunctive = true) 47 | { 48 | if (\count($constraints) < 2) { 49 | throw new \InvalidArgumentException( 50 | 'Must provide at least two constraints for a MultiConstraint. Use '. 51 | 'the regular Constraint class for one constraint only or MatchAllConstraint for none. You may use '. 52 | 'MultiConstraint::create() which optimizes and handles those cases automatically.' 53 | ); 54 | } 55 | 56 | $this->constraints = $constraints; 57 | $this->conjunctive = $conjunctive; 58 | } 59 | 60 | /** 61 | * @return ConstraintInterface[] 62 | */ 63 | public function getConstraints() 64 | { 65 | return $this->constraints; 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | public function isConjunctive() 72 | { 73 | return $this->conjunctive; 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function isDisjunctive() 80 | { 81 | return !$this->conjunctive; 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | public function compile($otherOperator) 88 | { 89 | $parts = array(); 90 | foreach ($this->constraints as $constraint) { 91 | $code = $constraint->compile($otherOperator); 92 | if ($code === 'true') { 93 | if (!$this->conjunctive) { 94 | return 'true'; 95 | } 96 | } elseif ($code === 'false') { 97 | if ($this->conjunctive) { 98 | return 'false'; 99 | } 100 | } else { 101 | $parts[] = '('.$code.')'; 102 | } 103 | } 104 | 105 | if (!$parts) { 106 | return $this->conjunctive ? 'true' : 'false'; 107 | } 108 | 109 | return $this->conjunctive ? implode('&&', $parts) : implode('||', $parts); 110 | } 111 | 112 | /** 113 | * @param ConstraintInterface $provider 114 | * 115 | * @return bool 116 | */ 117 | public function matches(ConstraintInterface $provider) 118 | { 119 | if (false === $this->conjunctive) { 120 | foreach ($this->constraints as $constraint) { 121 | if ($provider->matches($constraint)) { 122 | return true; 123 | } 124 | } 125 | 126 | return false; 127 | } 128 | 129 | // when matching a conjunctive and a disjunctive multi constraint we have to iterate over the disjunctive one 130 | // otherwise we'd return true if different parts of the disjunctive constraint match the conjunctive one 131 | // which would lead to incorrect results, e.g. [>1 and <2] would match [<1 or >2] although they do not intersect 132 | if ($provider instanceof MultiConstraint && $provider->isDisjunctive()) { 133 | return $provider->matches($this); 134 | } 135 | 136 | foreach ($this->constraints as $constraint) { 137 | if (!$provider->matches($constraint)) { 138 | return false; 139 | } 140 | } 141 | 142 | return true; 143 | } 144 | 145 | /** 146 | * {@inheritDoc} 147 | */ 148 | public function setPrettyString($prettyString) 149 | { 150 | $this->prettyString = $prettyString; 151 | } 152 | 153 | /** 154 | * {@inheritDoc} 155 | */ 156 | public function getPrettyString() 157 | { 158 | if ($this->prettyString) { 159 | return $this->prettyString; 160 | } 161 | 162 | return (string) $this; 163 | } 164 | 165 | /** 166 | * {@inheritDoc} 167 | */ 168 | public function __toString() 169 | { 170 | if ($this->string !== null) { 171 | return $this->string; 172 | } 173 | 174 | $constraints = array(); 175 | foreach ($this->constraints as $constraint) { 176 | $constraints[] = (string) $constraint; 177 | } 178 | 179 | return $this->string = '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']'; 180 | } 181 | 182 | /** 183 | * {@inheritDoc} 184 | */ 185 | public function getLowerBound() 186 | { 187 | $this->extractBounds(); 188 | 189 | if (null === $this->lowerBound) { 190 | throw new \LogicException('extractBounds should have populated the lowerBound property'); 191 | } 192 | 193 | return $this->lowerBound; 194 | } 195 | 196 | /** 197 | * {@inheritDoc} 198 | */ 199 | public function getUpperBound() 200 | { 201 | $this->extractBounds(); 202 | 203 | if (null === $this->upperBound) { 204 | throw new \LogicException('extractBounds should have populated the upperBound property'); 205 | } 206 | 207 | return $this->upperBound; 208 | } 209 | 210 | /** 211 | * Tries to optimize the constraints as much as possible, meaning 212 | * reducing/collapsing congruent constraints etc. 213 | * Does not necessarily return a MultiConstraint instance if 214 | * things can be reduced to a simple constraint 215 | * 216 | * @param ConstraintInterface[] $constraints A set of constraints 217 | * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive 218 | * 219 | * @return ConstraintInterface 220 | */ 221 | public static function create(array $constraints, $conjunctive = true) 222 | { 223 | if (0 === \count($constraints)) { 224 | return new MatchAllConstraint(); 225 | } 226 | 227 | if (1 === \count($constraints)) { 228 | return $constraints[0]; 229 | } 230 | 231 | $optimized = self::optimizeConstraints($constraints, $conjunctive); 232 | if ($optimized !== null) { 233 | list($constraints, $conjunctive) = $optimized; 234 | if (\count($constraints) === 1) { 235 | return $constraints[0]; 236 | } 237 | } 238 | 239 | return new self($constraints, $conjunctive); 240 | } 241 | 242 | /** 243 | * @param ConstraintInterface[] $constraints 244 | * @param bool $conjunctive 245 | * @return ?array 246 | * 247 | * @phpstan-return array{0: list, 1: bool}|null 248 | */ 249 | private static function optimizeConstraints(array $constraints, $conjunctive) 250 | { 251 | // parse the two OR groups and if they are contiguous we collapse 252 | // them into one constraint 253 | // [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4] 254 | if (!$conjunctive) { 255 | $left = $constraints[0]; 256 | $mergedConstraints = array(); 257 | $optimized = false; 258 | for ($i = 1, $l = \count($constraints); $i < $l; $i++) { 259 | $right = $constraints[$i]; 260 | if ( 261 | $left instanceof self 262 | && $left->conjunctive 263 | && $right instanceof self 264 | && $right->conjunctive 265 | && \count($left->constraints) === 2 266 | && \count($right->constraints) === 2 267 | && ($left0 = (string) $left->constraints[0]) 268 | && $left0[0] === '>' && $left0[1] === '=' 269 | && ($left1 = (string) $left->constraints[1]) 270 | && $left1[0] === '<' 271 | && ($right0 = (string) $right->constraints[0]) 272 | && $right0[0] === '>' && $right0[1] === '=' 273 | && ($right1 = (string) $right->constraints[1]) 274 | && $right1[0] === '<' 275 | && substr($left1, 2) === substr($right0, 3) 276 | ) { 277 | $optimized = true; 278 | $left = new MultiConstraint( 279 | array( 280 | $left->constraints[0], 281 | $right->constraints[1], 282 | ), 283 | true); 284 | } else { 285 | $mergedConstraints[] = $left; 286 | $left = $right; 287 | } 288 | } 289 | if ($optimized) { 290 | $mergedConstraints[] = $left; 291 | return array($mergedConstraints, false); 292 | } 293 | } 294 | 295 | // TODO: Here's the place to put more optimizations 296 | 297 | return null; 298 | } 299 | 300 | /** 301 | * @return void 302 | */ 303 | private function extractBounds() 304 | { 305 | if (null !== $this->lowerBound) { 306 | return; 307 | } 308 | 309 | foreach ($this->constraints as $constraint) { 310 | if (null === $this->lowerBound || null === $this->upperBound) { 311 | $this->lowerBound = $constraint->getLowerBound(); 312 | $this->upperBound = $constraint->getUpperBound(); 313 | continue; 314 | } 315 | 316 | if ($constraint->getLowerBound()->compareTo($this->lowerBound, $this->isConjunctive() ? '>' : '<')) { 317 | $this->lowerBound = $constraint->getLowerBound(); 318 | } 319 | 320 | if ($constraint->getUpperBound()->compareTo($this->upperBound, $this->isConjunctive() ? '<' : '>')) { 321 | $this->upperBound = $constraint->getUpperBound(); 322 | } 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/Constraint/Constraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver\Constraint; 13 | 14 | /** 15 | * Defines a constraint. 16 | */ 17 | class Constraint implements ConstraintInterface 18 | { 19 | /* operator integer values */ 20 | const OP_EQ = 0; 21 | const OP_LT = 1; 22 | const OP_LE = 2; 23 | const OP_GT = 3; 24 | const OP_GE = 4; 25 | const OP_NE = 5; 26 | 27 | /* operator string values */ 28 | const STR_OP_EQ = '=='; 29 | const STR_OP_EQ_ALT = '='; 30 | const STR_OP_LT = '<'; 31 | const STR_OP_LE = '<='; 32 | const STR_OP_GT = '>'; 33 | const STR_OP_GE = '>='; 34 | const STR_OP_NE = '!='; 35 | const STR_OP_NE_ALT = '<>'; 36 | 37 | /** 38 | * Operator to integer translation table. 39 | * 40 | * @var array 41 | * @phpstan-var array 42 | */ 43 | private static $transOpStr = array( 44 | '=' => self::OP_EQ, 45 | '==' => self::OP_EQ, 46 | '<' => self::OP_LT, 47 | '<=' => self::OP_LE, 48 | '>' => self::OP_GT, 49 | '>=' => self::OP_GE, 50 | '<>' => self::OP_NE, 51 | '!=' => self::OP_NE, 52 | ); 53 | 54 | /** 55 | * Integer to operator translation table. 56 | * 57 | * @var array 58 | * @phpstan-var array 59 | */ 60 | private static $transOpInt = array( 61 | self::OP_EQ => '==', 62 | self::OP_LT => '<', 63 | self::OP_LE => '<=', 64 | self::OP_GT => '>', 65 | self::OP_GE => '>=', 66 | self::OP_NE => '!=', 67 | ); 68 | 69 | /** 70 | * @var int 71 | * @phpstan-var self::OP_* 72 | */ 73 | protected $operator; 74 | 75 | /** @var string */ 76 | protected $version; 77 | 78 | /** @var string|null */ 79 | protected $prettyString; 80 | 81 | /** @var Bound */ 82 | protected $lowerBound; 83 | 84 | /** @var Bound */ 85 | protected $upperBound; 86 | 87 | /** 88 | * Sets operator and version to compare with. 89 | * 90 | * @param string $operator 91 | * @param string $version 92 | * 93 | * @throws \InvalidArgumentException if invalid operator is given. 94 | * 95 | * @phpstan-param self::STR_OP_* $operator 96 | */ 97 | public function __construct($operator, $version) 98 | { 99 | if (!isset(self::$transOpStr[$operator])) { 100 | throw new \InvalidArgumentException(sprintf( 101 | 'Invalid operator "%s" given, expected one of: %s', 102 | $operator, 103 | implode(', ', self::getSupportedOperators()) 104 | )); 105 | } 106 | 107 | $this->operator = self::$transOpStr[$operator]; 108 | $this->version = $version; 109 | } 110 | 111 | /** 112 | * @return string 113 | */ 114 | public function getVersion() 115 | { 116 | return $this->version; 117 | } 118 | 119 | /** 120 | * @return string 121 | * 122 | * @phpstan-return self::STR_OP_* 123 | */ 124 | public function getOperator() 125 | { 126 | return self::$transOpInt[$this->operator]; 127 | } 128 | 129 | /** 130 | * @param ConstraintInterface $provider 131 | * 132 | * @return bool 133 | */ 134 | public function matches(ConstraintInterface $provider) 135 | { 136 | if ($provider instanceof self) { 137 | return $this->matchSpecific($provider); 138 | } 139 | 140 | // turn matching around to find a match 141 | return $provider->matches($this); 142 | } 143 | 144 | /** 145 | * {@inheritDoc} 146 | */ 147 | public function setPrettyString($prettyString) 148 | { 149 | $this->prettyString = $prettyString; 150 | } 151 | 152 | /** 153 | * {@inheritDoc} 154 | */ 155 | public function getPrettyString() 156 | { 157 | if ($this->prettyString) { 158 | return $this->prettyString; 159 | } 160 | 161 | return $this->__toString(); 162 | } 163 | 164 | /** 165 | * Get all supported comparison operators. 166 | * 167 | * @return array 168 | * 169 | * @phpstan-return list 170 | */ 171 | public static function getSupportedOperators() 172 | { 173 | return array_keys(self::$transOpStr); 174 | } 175 | 176 | /** 177 | * @param string $operator 178 | * @return int 179 | * 180 | * @phpstan-param self::STR_OP_* $operator 181 | * @phpstan-return self::OP_* 182 | */ 183 | public static function getOperatorConstant($operator) 184 | { 185 | return self::$transOpStr[$operator]; 186 | } 187 | 188 | /** 189 | * @param string $a 190 | * @param string $b 191 | * @param string $operator 192 | * @param bool $compareBranches 193 | * 194 | * @throws \InvalidArgumentException if invalid operator is given. 195 | * 196 | * @return bool 197 | * 198 | * @phpstan-param self::STR_OP_* $operator 199 | */ 200 | public function versionCompare($a, $b, $operator, $compareBranches = false) 201 | { 202 | if (!isset(self::$transOpStr[$operator])) { 203 | throw new \InvalidArgumentException(sprintf( 204 | 'Invalid operator "%s" given, expected one of: %s', 205 | $operator, 206 | implode(', ', self::getSupportedOperators()) 207 | )); 208 | } 209 | 210 | $aIsBranch = strpos($a, 'dev-') === 0; 211 | $bIsBranch = strpos($b, 'dev-') === 0; 212 | 213 | if ($operator === '!=' && ($aIsBranch || $bIsBranch)) { 214 | return $a !== $b; 215 | } 216 | 217 | if ($aIsBranch && $bIsBranch) { 218 | return $operator === '==' && $a === $b; 219 | } 220 | 221 | // when branches are not comparable, we make sure dev branches never match anything 222 | if (!$compareBranches && ($aIsBranch || $bIsBranch)) { 223 | return false; 224 | } 225 | 226 | return \version_compare($a, $b, $operator); 227 | } 228 | 229 | /** 230 | * {@inheritDoc} 231 | */ 232 | public function compile($otherOperator) 233 | { 234 | if (strpos($this->version, 'dev-') === 0) { 235 | if (self::OP_EQ === $this->operator) { 236 | if (self::OP_EQ === $otherOperator) { 237 | return sprintf('$b && $v === %s', \var_export($this->version, true)); 238 | } 239 | if (self::OP_NE === $otherOperator) { 240 | return sprintf('!$b || $v !== %s', \var_export($this->version, true)); 241 | } 242 | return 'false'; 243 | } 244 | 245 | if (self::OP_NE === $this->operator) { 246 | if (self::OP_EQ === $otherOperator) { 247 | return sprintf('!$b || $v !== %s', \var_export($this->version, true)); 248 | } 249 | if (self::OP_NE === $otherOperator) { 250 | return 'true'; 251 | } 252 | return '!$b'; 253 | } 254 | 255 | return 'false'; 256 | } 257 | 258 | if (self::OP_EQ === $this->operator) { 259 | if (self::OP_EQ === $otherOperator) { 260 | return sprintf('\version_compare($v, %s, \'==\')', \var_export($this->version, true)); 261 | } 262 | if (self::OP_NE === $otherOperator) { 263 | return sprintf('$b || \version_compare($v, %s, \'!=\')', \var_export($this->version, true)); 264 | } 265 | 266 | return sprintf('!$b && \version_compare(%s, $v, \'%s\')', \var_export($this->version, true), self::$transOpInt[$otherOperator]); 267 | } 268 | 269 | if (self::OP_NE === $this->operator) { 270 | if (self::OP_EQ === $otherOperator) { 271 | return sprintf('$b || (!$b && \version_compare($v, %s, \'!=\'))', \var_export($this->version, true)); 272 | } 273 | 274 | if (self::OP_NE === $otherOperator) { 275 | return 'true'; 276 | } 277 | return '!$b'; 278 | } 279 | 280 | if (self::OP_LT === $this->operator || self::OP_LE === $this->operator) { 281 | if (self::OP_LT === $otherOperator || self::OP_LE === $otherOperator) { 282 | return '!$b'; 283 | } 284 | } else { // $this->operator must be self::OP_GT || self::OP_GE here 285 | if (self::OP_GT === $otherOperator || self::OP_GE === $otherOperator) { 286 | return '!$b'; 287 | } 288 | } 289 | 290 | if (self::OP_NE === $otherOperator) { 291 | return 'true'; 292 | } 293 | 294 | $codeComparison = sprintf('\version_compare($v, %s, \'%s\')', \var_export($this->version, true), self::$transOpInt[$this->operator]); 295 | if ($this->operator === self::OP_LE) { 296 | if ($otherOperator === self::OP_GT) { 297 | return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison; 298 | } 299 | } elseif ($this->operator === self::OP_GE) { 300 | if ($otherOperator === self::OP_LT) { 301 | return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison; 302 | } 303 | } 304 | 305 | return sprintf('!$b && %s', $codeComparison); 306 | } 307 | 308 | /** 309 | * @param Constraint $provider 310 | * @param bool $compareBranches 311 | * 312 | * @return bool 313 | */ 314 | public function matchSpecific(Constraint $provider, $compareBranches = false) 315 | { 316 | $noEqualOp = str_replace('=', '', self::$transOpInt[$this->operator]); 317 | $providerNoEqualOp = str_replace('=', '', self::$transOpInt[$provider->operator]); 318 | 319 | $isEqualOp = self::OP_EQ === $this->operator; 320 | $isNonEqualOp = self::OP_NE === $this->operator; 321 | $isProviderEqualOp = self::OP_EQ === $provider->operator; 322 | $isProviderNonEqualOp = self::OP_NE === $provider->operator; 323 | 324 | // '!=' operator is match when other operator is not '==' operator or version is not match 325 | // these kinds of comparisons always have a solution 326 | if ($isNonEqualOp || $isProviderNonEqualOp) { 327 | if ($isNonEqualOp && !$isProviderNonEqualOp && !$isProviderEqualOp && strpos($provider->version, 'dev-') === 0) { 328 | return false; 329 | } 330 | 331 | if ($isProviderNonEqualOp && !$isNonEqualOp && !$isEqualOp && strpos($this->version, 'dev-') === 0) { 332 | return false; 333 | } 334 | 335 | if (!$isEqualOp && !$isProviderEqualOp) { 336 | return true; 337 | } 338 | return $this->versionCompare($provider->version, $this->version, '!=', $compareBranches); 339 | } 340 | 341 | // an example for the condition is <= 2.0 & < 1.0 342 | // these kinds of comparisons always have a solution 343 | if ($this->operator !== self::OP_EQ && $noEqualOp === $providerNoEqualOp) { 344 | return !(strpos($this->version, 'dev-') === 0 || strpos($provider->version, 'dev-') === 0); 345 | } 346 | 347 | $version1 = $isEqualOp ? $this->version : $provider->version; 348 | $version2 = $isEqualOp ? $provider->version : $this->version; 349 | $operator = $isEqualOp ? $provider->operator : $this->operator; 350 | 351 | if ($this->versionCompare($version1, $version2, self::$transOpInt[$operator], $compareBranches)) { 352 | // special case, e.g. require >= 1.0 and provide < 1.0 353 | // 1.0 >= 1.0 but 1.0 is outside of the provided interval 354 | 355 | return !(self::$transOpInt[$provider->operator] === $providerNoEqualOp 356 | && self::$transOpInt[$this->operator] !== $noEqualOp 357 | && \version_compare($provider->version, $this->version, '==')); 358 | } 359 | 360 | return false; 361 | } 362 | 363 | /** 364 | * @return string 365 | */ 366 | public function __toString() 367 | { 368 | return self::$transOpInt[$this->operator] . ' ' . $this->version; 369 | } 370 | 371 | /** 372 | * {@inheritDoc} 373 | */ 374 | public function getLowerBound() 375 | { 376 | $this->extractBounds(); 377 | 378 | return $this->lowerBound; 379 | } 380 | 381 | /** 382 | * {@inheritDoc} 383 | */ 384 | public function getUpperBound() 385 | { 386 | $this->extractBounds(); 387 | 388 | return $this->upperBound; 389 | } 390 | 391 | /** 392 | * @return void 393 | */ 394 | private function extractBounds() 395 | { 396 | if (null !== $this->lowerBound) { 397 | return; 398 | } 399 | 400 | // Branches 401 | if (strpos($this->version, 'dev-') === 0) { 402 | $this->lowerBound = Bound::zero(); 403 | $this->upperBound = Bound::positiveInfinity(); 404 | 405 | return; 406 | } 407 | 408 | switch ($this->operator) { 409 | case self::OP_EQ: 410 | $this->lowerBound = new Bound($this->version, true); 411 | $this->upperBound = new Bound($this->version, true); 412 | break; 413 | case self::OP_LT: 414 | $this->lowerBound = Bound::zero(); 415 | $this->upperBound = new Bound($this->version, false); 416 | break; 417 | case self::OP_LE: 418 | $this->lowerBound = Bound::zero(); 419 | $this->upperBound = new Bound($this->version, true); 420 | break; 421 | case self::OP_GT: 422 | $this->lowerBound = new Bound($this->version, false); 423 | $this->upperBound = Bound::positiveInfinity(); 424 | break; 425 | case self::OP_GE: 426 | $this->lowerBound = new Bound($this->version, true); 427 | $this->upperBound = Bound::positiveInfinity(); 428 | break; 429 | case self::OP_NE: 430 | $this->lowerBound = Bound::zero(); 431 | $this->upperBound = Bound::positiveInfinity(); 432 | break; 433 | } 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /src/Intervals.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver; 13 | 14 | use Composer\Semver\Constraint\Constraint; 15 | use Composer\Semver\Constraint\ConstraintInterface; 16 | use Composer\Semver\Constraint\MatchAllConstraint; 17 | use Composer\Semver\Constraint\MatchNoneConstraint; 18 | use Composer\Semver\Constraint\MultiConstraint; 19 | 20 | /** 21 | * Helper class generating intervals from constraints 22 | * 23 | * This contains utilities for: 24 | * 25 | * - compacting an existing constraint which can be used to combine several into one 26 | * by creating a MultiConstraint out of the many constraints you have. 27 | * 28 | * - checking whether one subset is a subset of another. 29 | * 30 | * Note: You should call clear to free memoization memory usage when you are done using this class 31 | */ 32 | class Intervals 33 | { 34 | /** 35 | * @phpstan-var array 36 | */ 37 | private static $intervalsCache = array(); 38 | 39 | /** 40 | * @phpstan-var array 41 | */ 42 | private static $opSortOrder = array( 43 | '>=' => -3, 44 | '<' => -2, 45 | '>' => 2, 46 | '<=' => 3, 47 | ); 48 | 49 | /** 50 | * Clears the memoization cache once you are done 51 | * 52 | * @return void 53 | */ 54 | public static function clear() 55 | { 56 | self::$intervalsCache = array(); 57 | } 58 | 59 | /** 60 | * Checks whether $candidate is a subset of $constraint 61 | * 62 | * @return bool 63 | */ 64 | public static function isSubsetOf(ConstraintInterface $candidate, ConstraintInterface $constraint) 65 | { 66 | if ($constraint instanceof MatchAllConstraint) { 67 | return true; 68 | } 69 | 70 | if ($candidate instanceof MatchNoneConstraint || $constraint instanceof MatchNoneConstraint) { 71 | return false; 72 | } 73 | 74 | $intersectionIntervals = self::get(new MultiConstraint(array($candidate, $constraint), true)); 75 | $candidateIntervals = self::get($candidate); 76 | if (\count($intersectionIntervals['numeric']) !== \count($candidateIntervals['numeric'])) { 77 | return false; 78 | } 79 | 80 | foreach ($intersectionIntervals['numeric'] as $index => $interval) { 81 | if (!isset($candidateIntervals['numeric'][$index])) { 82 | return false; 83 | } 84 | 85 | if ((string) $candidateIntervals['numeric'][$index]->getStart() !== (string) $interval->getStart()) { 86 | return false; 87 | } 88 | 89 | if ((string) $candidateIntervals['numeric'][$index]->getEnd() !== (string) $interval->getEnd()) { 90 | return false; 91 | } 92 | } 93 | 94 | if ($intersectionIntervals['branches']['exclude'] !== $candidateIntervals['branches']['exclude']) { 95 | return false; 96 | } 97 | if (\count($intersectionIntervals['branches']['names']) !== \count($candidateIntervals['branches']['names'])) { 98 | return false; 99 | } 100 | foreach ($intersectionIntervals['branches']['names'] as $index => $name) { 101 | if ($name !== $candidateIntervals['branches']['names'][$index]) { 102 | return false; 103 | } 104 | } 105 | 106 | return true; 107 | } 108 | 109 | /** 110 | * Checks whether $a and $b have any intersection, equivalent to $a->matches($b) 111 | * 112 | * @return bool 113 | */ 114 | public static function haveIntersections(ConstraintInterface $a, ConstraintInterface $b) 115 | { 116 | if ($a instanceof MatchAllConstraint || $b instanceof MatchAllConstraint) { 117 | return true; 118 | } 119 | 120 | if ($a instanceof MatchNoneConstraint || $b instanceof MatchNoneConstraint) { 121 | return false; 122 | } 123 | 124 | $intersectionIntervals = self::generateIntervals(new MultiConstraint(array($a, $b), true), true); 125 | 126 | return \count($intersectionIntervals['numeric']) > 0 || $intersectionIntervals['branches']['exclude'] || \count($intersectionIntervals['branches']['names']) > 0; 127 | } 128 | 129 | /** 130 | * Attempts to optimize a MultiConstraint 131 | * 132 | * When merging MultiConstraints together they can get very large, this will 133 | * compact it by looking at the real intervals covered by all the constraints 134 | * and then creates a new constraint containing only the smallest amount of rules 135 | * to match the same intervals. 136 | * 137 | * @return ConstraintInterface 138 | */ 139 | public static function compactConstraint(ConstraintInterface $constraint) 140 | { 141 | if (!$constraint instanceof MultiConstraint) { 142 | return $constraint; 143 | } 144 | 145 | $intervals = self::generateIntervals($constraint); 146 | $constraints = array(); 147 | $hasNumericMatchAll = false; 148 | 149 | if (\count($intervals['numeric']) === 1 && (string) $intervals['numeric'][0]->getStart() === (string) Interval::fromZero() && (string) $intervals['numeric'][0]->getEnd() === (string) Interval::untilPositiveInfinity()) { 150 | $constraints[] = $intervals['numeric'][0]->getStart(); 151 | $hasNumericMatchAll = true; 152 | } else { 153 | $unEqualConstraints = array(); 154 | for ($i = 0, $count = \count($intervals['numeric']); $i < $count; $i++) { 155 | $interval = $intervals['numeric'][$i]; 156 | 157 | // if current interval ends with < N and next interval begins with > N we can swap this out for != N 158 | // but this needs to happen as a conjunctive expression together with the start of the current interval 159 | // and end of next interval, so [>=M, N, [>=M, !=N, getEnd()->getOperator() === '<' && $i+1 < $count) { 162 | $nextInterval = $intervals['numeric'][$i+1]; 163 | if ($interval->getEnd()->getVersion() === $nextInterval->getStart()->getVersion() && $nextInterval->getStart()->getOperator() === '>') { 164 | // only add a start if we didn't already do so, can be skipped if we're looking at second 165 | // interval in [>=M, N, P, =M, !=N] already and we only want to add !=P right now 167 | if (\count($unEqualConstraints) === 0 && (string) $interval->getStart() !== (string) Interval::fromZero()) { 168 | $unEqualConstraints[] = $interval->getStart(); 169 | } 170 | $unEqualConstraints[] = new Constraint('!=', $interval->getEnd()->getVersion()); 171 | continue; 172 | } 173 | } 174 | 175 | if (\count($unEqualConstraints) > 0) { 176 | // this is where the end of the following interval of a != constraint is added as explained above 177 | if ((string) $interval->getEnd() !== (string) Interval::untilPositiveInfinity()) { 178 | $unEqualConstraints[] = $interval->getEnd(); 179 | } 180 | 181 | // count is 1 if entire constraint is just one != expression 182 | if (\count($unEqualConstraints) > 1) { 183 | $constraints[] = new MultiConstraint($unEqualConstraints, true); 184 | } else { 185 | $constraints[] = $unEqualConstraints[0]; 186 | } 187 | 188 | $unEqualConstraints = array(); 189 | continue; 190 | } 191 | 192 | // convert back >= x - <= x intervals to == x 193 | if ($interval->getStart()->getVersion() === $interval->getEnd()->getVersion() && $interval->getStart()->getOperator() === '>=' && $interval->getEnd()->getOperator() === '<=') { 194 | $constraints[] = new Constraint('==', $interval->getStart()->getVersion()); 195 | continue; 196 | } 197 | 198 | if ((string) $interval->getStart() === (string) Interval::fromZero()) { 199 | $constraints[] = $interval->getEnd(); 200 | } elseif ((string) $interval->getEnd() === (string) Interval::untilPositiveInfinity()) { 201 | $constraints[] = $interval->getStart(); 202 | } else { 203 | $constraints[] = new MultiConstraint(array($interval->getStart(), $interval->getEnd()), true); 204 | } 205 | } 206 | } 207 | 208 | $devConstraints = array(); 209 | 210 | if (0 === \count($intervals['branches']['names'])) { 211 | if ($intervals['branches']['exclude']) { 212 | if ($hasNumericMatchAll) { 213 | return new MatchAllConstraint; 214 | } 215 | // otherwise constraint should contain a != operator and already cover this 216 | } 217 | } else { 218 | foreach ($intervals['branches']['names'] as $branchName) { 219 | if ($intervals['branches']['exclude']) { 220 | $devConstraints[] = new Constraint('!=', $branchName); 221 | } else { 222 | $devConstraints[] = new Constraint('==', $branchName); 223 | } 224 | } 225 | 226 | // excluded branches, e.g. != dev-foo are conjunctive with the interval, so 227 | // > 2.0 != dev-foo must return a conjunctive constraint 228 | if ($intervals['branches']['exclude']) { 229 | if (\count($constraints) > 1) { 230 | return new MultiConstraint(array_merge( 231 | array(new MultiConstraint($constraints, false)), 232 | $devConstraints 233 | ), true); 234 | } 235 | 236 | if (\count($constraints) === 1 && (string)$constraints[0] === (string)Interval::fromZero()) { 237 | if (\count($devConstraints) > 1) { 238 | return new MultiConstraint($devConstraints, true); 239 | } 240 | return $devConstraints[0]; 241 | } 242 | 243 | return new MultiConstraint(array_merge($constraints, $devConstraints), true); 244 | } 245 | 246 | // otherwise devConstraints contains a list of == operators for branches which are disjunctive with the 247 | // rest of the constraint 248 | $constraints = array_merge($constraints, $devConstraints); 249 | } 250 | 251 | if (\count($constraints) > 1) { 252 | return new MultiConstraint($constraints, false); 253 | } 254 | 255 | if (\count($constraints) === 1) { 256 | return $constraints[0]; 257 | } 258 | 259 | return new MatchNoneConstraint; 260 | } 261 | 262 | /** 263 | * Creates an array of numeric intervals and branch constraints representing a given constraint 264 | * 265 | * if the returned numeric array is empty it means the constraint matches nothing in the numeric range (0 - +inf) 266 | * if the returned branches array is empty it means no dev-* versions are matched 267 | * if a constraint matches all possible dev-* versions, branches will contain Interval::anyDev() 268 | * 269 | * @return array 270 | * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}} 271 | */ 272 | public static function get(ConstraintInterface $constraint) 273 | { 274 | $key = (string) $constraint; 275 | 276 | if (!isset(self::$intervalsCache[$key])) { 277 | self::$intervalsCache[$key] = self::generateIntervals($constraint); 278 | } 279 | 280 | return self::$intervalsCache[$key]; 281 | } 282 | 283 | /** 284 | * @param bool $stopOnFirstValidInterval 285 | * 286 | * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}} 287 | */ 288 | private static function generateIntervals(ConstraintInterface $constraint, $stopOnFirstValidInterval = false) 289 | { 290 | if ($constraint instanceof MatchAllConstraint) { 291 | return array('numeric' => array(new Interval(Interval::fromZero(), Interval::untilPositiveInfinity())), 'branches' => Interval::anyDev()); 292 | } 293 | 294 | if ($constraint instanceof MatchNoneConstraint) { 295 | return array('numeric' => array(), 'branches' => array('names' => array(), 'exclude' => false)); 296 | } 297 | 298 | if ($constraint instanceof Constraint) { 299 | return self::generateSingleConstraintIntervals($constraint); 300 | } 301 | 302 | if (!$constraint instanceof MultiConstraint) { 303 | throw new \UnexpectedValueException('The constraint passed in should be an MatchAllConstraint, Constraint or MultiConstraint instance, got '.\get_class($constraint).'.'); 304 | } 305 | 306 | $constraints = $constraint->getConstraints(); 307 | 308 | $numericGroups = array(); 309 | $constraintBranches = array(); 310 | foreach ($constraints as $c) { 311 | $res = self::get($c); 312 | $numericGroups[] = $res['numeric']; 313 | $constraintBranches[] = $res['branches']; 314 | } 315 | 316 | if ($constraint->isDisjunctive()) { 317 | $branches = Interval::noDev(); 318 | foreach ($constraintBranches as $b) { 319 | if ($b['exclude']) { 320 | if ($branches['exclude']) { 321 | // disjunctive constraint, so only exclude what's excluded in all constraints 322 | // !=a,!=b || !=b,!=c => !=b 323 | $branches['names'] = array_intersect($branches['names'], $b['names']); 324 | } else { 325 | // disjunctive constraint so exclude all names which are not explicitly included in the alternative 326 | // (==b || ==c) || !=a,!=b => !=a 327 | $branches['exclude'] = true; 328 | $branches['names'] = array_diff($b['names'], $branches['names']); 329 | } 330 | } else { 331 | if ($branches['exclude']) { 332 | // disjunctive constraint so exclude all names which are not explicitly included in the alternative 333 | // !=a,!=b || (==b || ==c) => !=a 334 | $branches['names'] = array_diff($branches['names'], $b['names']); 335 | } else { 336 | // disjunctive constraint, so just add all the other branches 337 | // (==a || ==b) || ==c => ==a || ==b || ==c 338 | $branches['names'] = array_merge($branches['names'], $b['names']); 339 | } 340 | } 341 | } 342 | } else { 343 | $branches = Interval::anyDev(); 344 | foreach ($constraintBranches as $b) { 345 | if ($b['exclude']) { 346 | if ($branches['exclude']) { 347 | // conjunctive, so just add all branch names to be excluded 348 | // !=a && !=b => !=a,!=b 349 | $branches['names'] = array_merge($branches['names'], $b['names']); 350 | } else { 351 | // conjunctive, so only keep included names which are not excluded 352 | // (==a||==c) && !=a,!=b => ==c 353 | $branches['names'] = array_diff($branches['names'], $b['names']); 354 | } 355 | } else { 356 | if ($branches['exclude']) { 357 | // conjunctive, so only keep included names which are not excluded 358 | // !=a,!=b && (==a||==c) => ==c 359 | $branches['names'] = array_diff($b['names'], $branches['names']); 360 | $branches['exclude'] = false; 361 | } else { 362 | // conjunctive, so only keep names that are included in both 363 | // (==a||==b) && (==a||==c) => ==a 364 | $branches['names'] = array_intersect($branches['names'], $b['names']); 365 | } 366 | } 367 | } 368 | } 369 | 370 | $branches['names'] = array_unique($branches['names']); 371 | 372 | if (\count($numericGroups) === 1) { 373 | return array('numeric' => $numericGroups[0], 'branches' => $branches); 374 | } 375 | 376 | $borders = array(); 377 | foreach ($numericGroups as $group) { 378 | foreach ($group as $interval) { 379 | $borders[] = array('version' => $interval->getStart()->getVersion(), 'operator' => $interval->getStart()->getOperator(), 'side' => 'start'); 380 | $borders[] = array('version' => $interval->getEnd()->getVersion(), 'operator' => $interval->getEnd()->getOperator(), 'side' => 'end'); 381 | } 382 | } 383 | 384 | $opSortOrder = self::$opSortOrder; 385 | usort($borders, function ($a, $b) use ($opSortOrder) { 386 | $order = version_compare($a['version'], $b['version']); 387 | if ($order === 0) { 388 | return $opSortOrder[$a['operator']] - $opSortOrder[$b['operator']]; 389 | } 390 | 391 | return $order; 392 | }); 393 | 394 | $activeIntervals = 0; 395 | $intervals = array(); 396 | $index = 0; 397 | $activationThreshold = $constraint->isConjunctive() ? \count($numericGroups) : 1; 398 | $start = null; 399 | foreach ($borders as $border) { 400 | if ($border['side'] === 'start') { 401 | $activeIntervals++; 402 | } else { 403 | $activeIntervals--; 404 | } 405 | if (!$start && $activeIntervals >= $activationThreshold) { 406 | $start = new Constraint($border['operator'], $border['version']); 407 | } elseif ($start && $activeIntervals < $activationThreshold) { 408 | // filter out invalid intervals like > x - <= x, or >= x - < x 409 | if ( 410 | version_compare($start->getVersion(), $border['version'], '=') 411 | && ( 412 | ($start->getOperator() === '>' && $border['operator'] === '<=') 413 | || ($start->getOperator() === '>=' && $border['operator'] === '<') 414 | ) 415 | ) { 416 | unset($intervals[$index]); 417 | } else { 418 | $intervals[$index] = new Interval($start, new Constraint($border['operator'], $border['version'])); 419 | $index++; 420 | 421 | if ($stopOnFirstValidInterval) { 422 | break; 423 | } 424 | } 425 | 426 | $start = null; 427 | } 428 | } 429 | 430 | return array('numeric' => $intervals, 'branches' => $branches); 431 | } 432 | 433 | /** 434 | * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}} 435 | */ 436 | private static function generateSingleConstraintIntervals(Constraint $constraint) 437 | { 438 | $op = $constraint->getOperator(); 439 | 440 | // handle branch constraints first 441 | if (strpos($constraint->getVersion(), 'dev-') === 0) { 442 | $intervals = array(); 443 | $branches = array('names' => array(), 'exclude' => false); 444 | 445 | // != dev-foo means any numeric version may match, we treat >/< like != they are not really defined for branches 446 | if ($op === '!=') { 447 | $intervals[] = new Interval(Interval::fromZero(), Interval::untilPositiveInfinity()); 448 | $branches = array('names' => array($constraint->getVersion()), 'exclude' => true); 449 | } elseif ($op === '==') { 450 | $branches['names'][] = $constraint->getVersion(); 451 | } 452 | 453 | return array( 454 | 'numeric' => $intervals, 455 | 'branches' => $branches, 456 | ); 457 | } 458 | 459 | if ($op[0] === '>') { // > & >= 460 | return array('numeric' => array(new Interval($constraint, Interval::untilPositiveInfinity())), 'branches' => Interval::noDev()); 461 | } 462 | if ($op[0] === '<') { // < & <= 463 | return array('numeric' => array(new Interval(Interval::fromZero(), $constraint)), 'branches' => Interval::noDev()); 464 | } 465 | if ($op === '!=') { 466 | // convert !=x to intervals of 0 - x - +inf + dev* 467 | return array('numeric' => array( 468 | new Interval(Interval::fromZero(), new Constraint('<', $constraint->getVersion())), 469 | new Interval(new Constraint('>', $constraint->getVersion()), Interval::untilPositiveInfinity()), 470 | ), 'branches' => Interval::anyDev()); 471 | } 472 | 473 | // convert ==x to an interval of >=x - <=x 474 | return array('numeric' => array( 475 | new Interval(new Constraint('>=', $constraint->getVersion()), new Constraint('<=', $constraint->getVersion())), 476 | ), 'branches' => Interval::noDev()); 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/VersionParser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Semver; 13 | 14 | use Composer\Semver\Constraint\ConstraintInterface; 15 | use Composer\Semver\Constraint\MatchAllConstraint; 16 | use Composer\Semver\Constraint\MultiConstraint; 17 | use Composer\Semver\Constraint\Constraint; 18 | 19 | /** 20 | * Version parser. 21 | * 22 | * @author Jordi Boggiano 23 | */ 24 | class VersionParser 25 | { 26 | /** 27 | * Regex to match pre-release data (sort of). 28 | * 29 | * Due to backwards compatibility: 30 | * - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted. 31 | * - Only stabilities as recognized by Composer are allowed to precede a numerical identifier. 32 | * - Numerical-only pre-release identifiers are not supported, see tests. 33 | * 34 | * |--------------| 35 | * [major].[minor].[patch] -[pre-release] +[build-metadata] 36 | * 37 | * @var string 38 | */ 39 | private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?'; 40 | 41 | /** @var string */ 42 | private static $stabilitiesRegex = 'stable|RC|beta|alpha|dev'; 43 | 44 | /** 45 | * Returns the stability of a version. 46 | * 47 | * @param string $version 48 | * 49 | * @return string 50 | * @phpstan-return 'stable'|'RC'|'beta'|'alpha'|'dev' 51 | */ 52 | public static function parseStability($version) 53 | { 54 | $version = (string) preg_replace('{#.+$}', '', (string) $version); 55 | 56 | if (strpos($version, 'dev-') === 0 || '-dev' === substr($version, -4)) { 57 | return 'dev'; 58 | } 59 | 60 | preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match); 61 | 62 | if (!empty($match[3])) { 63 | return 'dev'; 64 | } 65 | 66 | if (!empty($match[1])) { 67 | if ('beta' === $match[1] || 'b' === $match[1]) { 68 | return 'beta'; 69 | } 70 | if ('alpha' === $match[1] || 'a' === $match[1]) { 71 | return 'alpha'; 72 | } 73 | if ('rc' === $match[1]) { 74 | return 'RC'; 75 | } 76 | } 77 | 78 | return 'stable'; 79 | } 80 | 81 | /** 82 | * @param string $stability 83 | * 84 | * @return string 85 | * @phpstan-return 'stable'|'RC'|'beta'|'alpha'|'dev' 86 | */ 87 | public static function normalizeStability($stability) 88 | { 89 | $stability = strtolower((string) $stability); 90 | 91 | if (!in_array($stability, array('stable', 'rc', 'beta', 'alpha', 'dev'), true)) { 92 | throw new \InvalidArgumentException('Invalid stability string "'.$stability.'", expected one of stable, RC, beta, alpha or dev'); 93 | } 94 | 95 | return $stability === 'rc' ? 'RC' : $stability; 96 | } 97 | 98 | /** 99 | * @param string $version 100 | * 101 | * @return bool 102 | */ 103 | public function isValid($version) 104 | { 105 | try { 106 | $this->normalize($version); 107 | } catch (\UnexpectedValueException $e) { 108 | return false; 109 | } 110 | 111 | return true; 112 | } 113 | 114 | /** 115 | * Normalizes a version string to be able to perform comparisons on it. 116 | * 117 | * @param string $version 118 | * @param ?string $fullVersion optional complete version string to give more context 119 | * 120 | * @throws \UnexpectedValueException 121 | * 122 | * @return string 123 | */ 124 | public function normalize($version, $fullVersion = null) 125 | { 126 | $version = trim((string) $version); 127 | $origVersion = $version; 128 | if (null === $fullVersion) { 129 | $fullVersion = $version; 130 | } 131 | 132 | // strip off aliasing 133 | if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) { 134 | $version = $match[1]; 135 | } 136 | 137 | // strip off stability flag 138 | if (preg_match('{@(?:' . self::$stabilitiesRegex . ')$}i', $version, $match)) { 139 | $version = substr($version, 0, strlen($version) - strlen($match[0])); 140 | } 141 | 142 | // normalize master/trunk/default branches to dev-name for BC with 1.x as these used to be valid constraints 143 | if (\in_array($version, array('master', 'trunk', 'default'), true)) { 144 | $version = 'dev-' . $version; 145 | } 146 | 147 | // if requirement is branch-like, use full name 148 | if (stripos($version, 'dev-') === 0) { 149 | return 'dev-' . substr($version, 4); 150 | } 151 | 152 | // strip off build metadata 153 | if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) { 154 | $version = $match[1]; 155 | } 156 | 157 | // match classical versioning 158 | if (preg_match('{^v?(\d{1,5}+)(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) { 159 | $version = $matches[1] 160 | . (!empty($matches[2]) ? $matches[2] : '.0') 161 | . (!empty($matches[3]) ? $matches[3] : '.0') 162 | . (!empty($matches[4]) ? $matches[4] : '.0'); 163 | $index = 5; 164 | // match date(time) based versioning 165 | } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3}){0,2})' . self::$modifierRegex . '$}i', $version, $matches)) { 166 | $version = (string) preg_replace('{\D}', '.', $matches[1]); 167 | $index = 2; 168 | } 169 | 170 | // add version modifiers if a version was matched 171 | if (isset($index)) { 172 | if (!empty($matches[$index])) { 173 | if ('stable' === $matches[$index]) { 174 | return $version; 175 | } 176 | $version .= '-' . $this->expandStability($matches[$index]) . (isset($matches[$index + 1]) && '' !== $matches[$index + 1] ? ltrim($matches[$index + 1], '.-') : ''); 177 | } 178 | 179 | if (!empty($matches[$index + 2])) { 180 | $version .= '-dev'; 181 | } 182 | 183 | return $version; 184 | } 185 | 186 | // match dev branches 187 | if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) { 188 | try { 189 | $normalized = $this->normalizeBranch($match[1]); 190 | // a branch ending with -dev is only valid if it is numeric 191 | // if it gets prefixed with dev- it means the branch name should 192 | // have had a dev- prefix already when passed to normalize 193 | if (strpos($normalized, 'dev-') === false) { 194 | return $normalized; 195 | } 196 | } catch (\Exception $e) { 197 | } 198 | } 199 | 200 | $extraMessage = ''; 201 | if (preg_match('{ +as +' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))?$}', $fullVersion)) { 202 | $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version'; 203 | } elseif (preg_match('{^' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))? +as +}', $fullVersion)) { 204 | $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-'; 205 | } 206 | 207 | throw new \UnexpectedValueException('Invalid version string "' . $origVersion . '"' . $extraMessage); 208 | } 209 | 210 | /** 211 | * Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison. 212 | * 213 | * @param string $branch Branch name (e.g. 2.1.x-dev) 214 | * 215 | * @return string|false Numeric prefix if present (e.g. 2.1.) or false 216 | */ 217 | public function parseNumericAliasPrefix($branch) 218 | { 219 | if (preg_match('{^(?P(\d++\\.)*\d++)(?:\.x)?-dev$}i', (string) $branch, $matches)) { 220 | return $matches['version'] . '.'; 221 | } 222 | 223 | return false; 224 | } 225 | 226 | /** 227 | * Normalizes a branch name to be able to perform comparisons on it. 228 | * 229 | * @param string $name 230 | * 231 | * @return string 232 | */ 233 | public function normalizeBranch($name) 234 | { 235 | $name = trim((string) $name); 236 | 237 | if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) { 238 | $version = ''; 239 | for ($i = 1; $i < 5; ++$i) { 240 | $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x'; 241 | } 242 | 243 | return str_replace('x', '9999999', $version) . '-dev'; 244 | } 245 | 246 | return 'dev-' . $name; 247 | } 248 | 249 | /** 250 | * Normalizes a default branch name (i.e. master on git) to 9999999-dev. 251 | * 252 | * @param string $name 253 | * 254 | * @return string 255 | * 256 | * @deprecated No need to use this anymore in theory, Composer 2 does not normalize any branch names to 9999999-dev anymore 257 | */ 258 | public function normalizeDefaultBranch($name) 259 | { 260 | if ($name === 'dev-master' || $name === 'dev-default' || $name === 'dev-trunk') { 261 | return '9999999-dev'; 262 | } 263 | 264 | return (string) $name; 265 | } 266 | 267 | /** 268 | * Parses a constraint string into MultiConstraint and/or Constraint objects. 269 | * 270 | * @param string $constraints 271 | * 272 | * @return ConstraintInterface 273 | */ 274 | public function parseConstraints($constraints) 275 | { 276 | $prettyConstraint = (string) $constraints; 277 | 278 | $orConstraints = preg_split('{\s*\|\|?\s*}', trim((string) $constraints)); 279 | if (false === $orConstraints) { 280 | throw new \RuntimeException('Failed to preg_split string: '.$constraints); 281 | } 282 | $orGroups = array(); 283 | 284 | foreach ($orConstraints as $orConstraint) { 285 | $andConstraints = preg_split('{(?< ,]) *(? 1) { 290 | $constraintObjects = array(); 291 | foreach ($andConstraints as $andConstraint) { 292 | foreach ($this->parseConstraint($andConstraint) as $parsedAndConstraint) { 293 | $constraintObjects[] = $parsedAndConstraint; 294 | } 295 | } 296 | } else { 297 | $constraintObjects = $this->parseConstraint($andConstraints[0]); 298 | } 299 | 300 | if (1 === \count($constraintObjects)) { 301 | $constraint = $constraintObjects[0]; 302 | } else { 303 | $constraint = new MultiConstraint($constraintObjects); 304 | } 305 | 306 | $orGroups[] = $constraint; 307 | } 308 | 309 | $parsedConstraint = MultiConstraint::create($orGroups, false); 310 | 311 | $parsedConstraint->setPrettyString($prettyConstraint); 312 | 313 | return $parsedConstraint; 314 | } 315 | 316 | /** 317 | * @param string $constraint 318 | * 319 | * @throws \UnexpectedValueException 320 | * 321 | * @return array 322 | * 323 | * @phpstan-return non-empty-array 324 | */ 325 | private function parseConstraint($constraint) 326 | { 327 | // strip off aliasing 328 | if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $constraint, $match)) { 329 | $constraint = $match[1]; 330 | } 331 | 332 | // strip @stability flags, and keep it for later use 333 | if (preg_match('{^([^,\s]*?)@(' . self::$stabilitiesRegex . ')$}i', $constraint, $match)) { 334 | $constraint = '' !== $match[1] ? $match[1] : '*'; 335 | if ($match[2] !== 'stable') { 336 | $stabilityModifier = $match[2]; 337 | } 338 | } 339 | 340 | // get rid of #refs as those are used by composer only 341 | if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraint, $match)) { 342 | $constraint = $match[1]; 343 | } 344 | 345 | if (preg_match('{^(v)?[xX*](\.[xX*])*$}i', $constraint, $match)) { 346 | if (!empty($match[1]) || !empty($match[2])) { 347 | return array(new Constraint('>=', '0.0.0.0-dev')); 348 | } 349 | 350 | return array(new MatchAllConstraint()); 351 | } 352 | 353 | $versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:' . self::$modifierRegex . '|\.([xX*][.-]?dev))(?:\+[^\s]+)?'; 354 | 355 | // Tilde Range 356 | // 357 | // Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous 358 | // version, to ensure that unstable instances of the current version are allowed. However, if a stability 359 | // suffix is added to the constraint, then a >= match on the current version is used instead. 360 | if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) { 361 | if (strpos($constraint, '~>') === 0) { 362 | throw new \UnexpectedValueException( 363 | 'Could not parse version constraint ' . $constraint . ': ' . 364 | 'Invalid operator "~>", you probably meant to use the "~" operator' 365 | ); 366 | } 367 | 368 | // Work out which position in the version we are operating at 369 | if (isset($matches[4]) && '' !== $matches[4] && null !== $matches[4]) { 370 | $position = 4; 371 | } elseif (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) { 372 | $position = 3; 373 | } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) { 374 | $position = 2; 375 | } else { 376 | $position = 1; 377 | } 378 | 379 | // when matching 2.x-dev or 3.0.x-dev we have to shift the second or third number, despite no second/third number matching above 380 | if (!empty($matches[8])) { 381 | $position++; 382 | } 383 | 384 | // Calculate the stability suffix 385 | $stabilitySuffix = ''; 386 | if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) { 387 | $stabilitySuffix .= '-dev'; 388 | } 389 | 390 | $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); 391 | $lowerBound = new Constraint('>=', $lowVersion); 392 | 393 | // For upper bound, we increment the position of one more significance, 394 | // but highPosition = 0 would be illegal 395 | $highPosition = max(1, $position - 1); 396 | $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev'; 397 | $upperBound = new Constraint('<', $highVersion); 398 | 399 | return array( 400 | $lowerBound, 401 | $upperBound, 402 | ); 403 | } 404 | 405 | // Caret Range 406 | // 407 | // Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple. 408 | // In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for 409 | // versions 0.X >=0.1.0, and no updates for versions 0.0.X 410 | if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) { 411 | // Work out which position in the version we are operating at 412 | if ('0' !== $matches[1] || '' === $matches[2] || null === $matches[2]) { 413 | $position = 1; 414 | } elseif ('0' !== $matches[2] || '' === $matches[3] || null === $matches[3]) { 415 | $position = 2; 416 | } else { 417 | $position = 3; 418 | } 419 | 420 | // Calculate the stability suffix 421 | $stabilitySuffix = ''; 422 | if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) { 423 | $stabilitySuffix .= '-dev'; 424 | } 425 | 426 | $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); 427 | $lowerBound = new Constraint('>=', $lowVersion); 428 | 429 | // For upper bound, we increment the position of one more significance, 430 | // but highPosition = 0 would be illegal 431 | $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; 432 | $upperBound = new Constraint('<', $highVersion); 433 | 434 | return array( 435 | $lowerBound, 436 | $upperBound, 437 | ); 438 | } 439 | 440 | // X Range 441 | // 442 | // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple. 443 | // A partial version range is treated as an X-Range, so the special character is in fact optional. 444 | if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) { 445 | if (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) { 446 | $position = 3; 447 | } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) { 448 | $position = 2; 449 | } else { 450 | $position = 1; 451 | } 452 | 453 | $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev'; 454 | $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; 455 | 456 | if ($lowVersion === '0.0.0.0-dev') { 457 | return array(new Constraint('<', $highVersion)); 458 | } 459 | 460 | return array( 461 | new Constraint('>=', $lowVersion), 462 | new Constraint('<', $highVersion), 463 | ); 464 | } 465 | 466 | // Hyphen Range 467 | // 468 | // Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range, 469 | // then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in 470 | // the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but 471 | // nothing that would be greater than the provided tuple parts. 472 | if (preg_match('{^(?P' . $versionRegex . ') +- +(?P' . $versionRegex . ')($)}i', $constraint, $matches)) { 473 | // Calculate the stability suffix 474 | $lowStabilitySuffix = ''; 475 | if (empty($matches[6]) && empty($matches[8]) && empty($matches[9])) { 476 | $lowStabilitySuffix = '-dev'; 477 | } 478 | 479 | $lowVersion = $this->normalize($matches['from']); 480 | $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix); 481 | 482 | $empty = function ($x) { 483 | return ($x === 0 || $x === '0') ? false : empty($x); 484 | }; 485 | 486 | if ((!$empty($matches[12]) && !$empty($matches[13])) || !empty($matches[15]) || !empty($matches[17]) || !empty($matches[18])) { 487 | $highVersion = $this->normalize($matches['to']); 488 | $upperBound = new Constraint('<=', $highVersion); 489 | } else { 490 | $highMatch = array('', $matches[11], $matches[12], $matches[13], $matches[14]); 491 | 492 | // validate to version 493 | $this->normalize($matches['to']); 494 | 495 | $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[12]) ? 1 : 2, 1) . '-dev'; 496 | $upperBound = new Constraint('<', $highVersion); 497 | } 498 | 499 | return array( 500 | $lowerBound, 501 | $upperBound, 502 | ); 503 | } 504 | 505 | // Basic Comparators 506 | if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) { 507 | try { 508 | try { 509 | $version = $this->normalize($matches[2]); 510 | } catch (\UnexpectedValueException $e) { 511 | // recover from an invalid constraint like foobar-dev which should be dev-foobar 512 | // except if the constraint uses a known operator, in which case it must be a parse error 513 | if (substr($matches[2], -4) === '-dev' && preg_match('{^[0-9a-zA-Z-./]+$}', $matches[2])) { 514 | $version = $this->normalize('dev-'.substr($matches[2], 0, -4)); 515 | } else { 516 | throw $e; 517 | } 518 | } 519 | 520 | $op = $matches[1] ?: '='; 521 | 522 | if ($op !== '==' && $op !== '=' && !empty($stabilityModifier) && self::parseStability($version) === 'stable') { 523 | $version .= '-' . $stabilityModifier; 524 | } elseif ('<' === $op || '>=' === $op) { 525 | if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) { 526 | if (strpos($matches[2], 'dev-') !== 0) { 527 | $version .= '-dev'; 528 | } 529 | } 530 | } 531 | 532 | return array(new Constraint($matches[1] ?: '=', $version)); 533 | } catch (\Exception $e) { 534 | } 535 | } 536 | 537 | $message = 'Could not parse version constraint ' . $constraint; 538 | if (isset($e)) { 539 | $message .= ': ' . $e->getMessage(); 540 | } 541 | 542 | throw new \UnexpectedValueException($message); 543 | } 544 | 545 | /** 546 | * Increment, decrement, or simply pad a version number. 547 | * 548 | * Support function for {@link parseConstraint()} 549 | * 550 | * @param array $matches Array with version parts in array indexes 1,2,3,4 551 | * @param int $position 1,2,3,4 - which segment of the version to increment/decrement 552 | * @param int $increment 553 | * @param string $pad The string to pad version parts after $position 554 | * 555 | * @return string|null The new version 556 | * 557 | * @phpstan-param string[] $matches 558 | */ 559 | private function manipulateVersionString(array $matches, $position, $increment = 0, $pad = '0') 560 | { 561 | for ($i = 4; $i > 0; --$i) { 562 | if ($i > $position) { 563 | $matches[$i] = $pad; 564 | } elseif ($i === $position && $increment) { 565 | $matches[$i] += $increment; 566 | // If $matches[$i] was 0, carry the decrement 567 | if ($matches[$i] < 0) { 568 | $matches[$i] = $pad; 569 | --$position; 570 | 571 | // Return null on a carry overflow 572 | if ($i === 1) { 573 | return null; 574 | } 575 | } 576 | } 577 | } 578 | 579 | return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4]; 580 | } 581 | 582 | /** 583 | * Expand shorthand stability string to long version. 584 | * 585 | * @param string $stability 586 | * 587 | * @return string 588 | */ 589 | private function expandStability($stability) 590 | { 591 | $stability = strtolower($stability); 592 | 593 | switch ($stability) { 594 | case 'a': 595 | return 'alpha'; 596 | case 'b': 597 | return 'beta'; 598 | case 'p': 599 | case 'pl': 600 | return 'patch'; 601 | case 'rc': 602 | return 'RC'; 603 | default: 604 | return $stability; 605 | } 606 | } 607 | } 608 | --------------------------------------------------------------------------------