Dep: update

주로 PHAN
This commit is contained in:
2021-08-06 22:39:09 +09:00
parent 5310c2e7f6
commit b306601c72
1098 changed files with 92137 additions and 33228 deletions
+3 -1
View File
@@ -101,11 +101,13 @@ class Comparator
* @param string $version2
*
* @return bool
*
* @phpstan-param Constraint::STR_OP_* $operator
*/
public static function compare($version1, $operator, $version2)
{
$constraint = new Constraint($operator, $version2);
return $constraint->matches(new Constraint('==', $version1));
return $constraint->matchSpecific(new Constraint('==', $version1), true);
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
/*
* This file is part of composer/semver.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Semver;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface;
/**
* Helper class to evaluate constraint by compiling and reusing the code to evaluate
*/
class CompilingMatcher
{
/**
* @var array
* @phpstan-var array<string, callable>
*/
private static $compiledCheckerCache = array();
/** @var bool */
private static $enabled;
/**
* @phpstan-var array<Constraint::OP_*, Constraint::STR_OP_*>
*/
private static $transOpInt = array(
Constraint::OP_EQ => Constraint::STR_OP_EQ,
Constraint::OP_LT => Constraint::STR_OP_LT,
Constraint::OP_LE => Constraint::STR_OP_LE,
Constraint::OP_GT => Constraint::STR_OP_GT,
Constraint::OP_GE => Constraint::STR_OP_GE,
Constraint::OP_NE => Constraint::STR_OP_NE,
);
/**
* Evaluates the expression: $constraint match $operator $version
*
* @param ConstraintInterface $constraint
* @param int $operator
* @phpstan-param Constraint::OP_* $operator
* @param string $version
*
* @return mixed
*/
public static function match(ConstraintInterface $constraint, $operator, $version)
{
if (self::$enabled === null) {
self::$enabled = !\in_array('eval', explode(',', (string) ini_get('disable_functions')), true);
}
if (!self::$enabled) {
return $constraint->matches(new Constraint(self::$transOpInt[$operator], $version));
}
$cacheKey = $operator.$constraint;
if (!isset(self::$compiledCheckerCache[$cacheKey])) {
$code = $constraint->compile($operator);
self::$compiledCheckerCache[$cacheKey] = $function = eval('return function($v, $b){return '.$code.';};');
} else {
$function = self::$compiledCheckerCache[$cacheKey];
}
return $function($version, strpos($version, 'dev-') === 0);
}
}
@@ -1,63 +0,0 @@
<?php
/*
* This file is part of composer/semver.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Semver\Constraint;
trigger_error('The ' . __NAMESPACE__ . '\AbstractConstraint abstract class is deprecated, there is no replacement for it, it will be removed in the next major version.', E_USER_DEPRECATED);
/**
* Base constraint class.
*/
abstract class AbstractConstraint implements ConstraintInterface
{
/** @var string */
protected $prettyString;
/**
* @param ConstraintInterface $provider
*
* @return bool
*/
public function matches(ConstraintInterface $provider)
{
if ($provider instanceof $this) {
// see note at bottom of this class declaration
return $this->matchSpecific($provider);
}
// turn matching around to find a match
return $provider->matches($this);
}
/**
* @param string $prettyString
*/
public function setPrettyString($prettyString)
{
$this->prettyString = $prettyString;
}
/**
* @return string
*/
public function getPrettyString()
{
if ($this->prettyString) {
return $this->prettyString;
}
return $this->__toString();
}
// implementations must implement a method of this format:
// not declared abstract here because type hinting violates parameter coherence (TODO right word?)
// public function matchSpecific(<SpecificConstraintType> $provider);
}
+122
View File
@@ -0,0 +1,122 @@
<?php
/*
* This file is part of composer/semver.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Semver\Constraint;
class Bound
{
/**
* @var string
*/
private $version;
/**
* @var bool
*/
private $isInclusive;
/**
* @param string $version
* @param bool $isInclusive
*/
public function __construct($version, $isInclusive)
{
$this->version = $version;
$this->isInclusive = $isInclusive;
}
/**
* @return string
*/
public function getVersion()
{
return $this->version;
}
/**
* @return bool
*/
public function isInclusive()
{
return $this->isInclusive;
}
/**
* @return bool
*/
public function isZero()
{
return $this->getVersion() === '0.0.0.0-dev' && $this->isInclusive();
}
/**
* @return bool
*/
public function isPositiveInfinity()
{
return $this->getVersion() === PHP_INT_MAX.'.0.0.0' && !$this->isInclusive();
}
/**
* Compares a bound to another with a given operator.
*
* @param Bound $other
* @param string $operator
*
* @return bool
*/
public function compareTo(Bound $other, $operator)
{
if (!\in_array($operator, array('<', '>'), true)) {
throw new \InvalidArgumentException('Does not support any other operator other than > or <.');
}
// If they are the same it doesn't matter
if ($this == $other) {
return false;
}
$compareResult = version_compare($this->getVersion(), $other->getVersion());
// Not the same version means we don't need to check if the bounds are inclusive or not
if (0 !== $compareResult) {
return (('>' === $operator) ? 1 : -1) === $compareResult;
}
// Question we're answering here is "am I higher than $other?"
return '>' === $operator ? $other->isInclusive() : !$other->isInclusive();
}
public function __toString()
{
return sprintf(
'%s [%s]',
$this->getVersion(),
$this->isInclusive() ? 'inclusive' : 'exclusive'
);
}
/**
* @return self
*/
public static function zero()
{
return new Bound('0.0.0.0-dev', true);
}
/**
* @return self
*/
public static function positiveInfinity()
{
return new Bound(PHP_INT_MAX.'.0.0.0', false);
}
}
+277 -57
View File
@@ -24,10 +24,21 @@ class Constraint implements ConstraintInterface
const OP_GE = 4;
const OP_NE = 5;
/* operator string values */
const STR_OP_EQ = '==';
const STR_OP_EQ_ALT = '=';
const STR_OP_LT = '<';
const STR_OP_LE = '<=';
const STR_OP_GT = '>';
const STR_OP_GE = '>=';
const STR_OP_NE = '!=';
const STR_OP_NE_ALT = '<>';
/**
* Operator to integer translation table.
*
* @var array
* @phpstan-var array<self::STR_OP_*, self::OP_*>
*/
private static $transOpStr = array(
'=' => self::OP_EQ,
@@ -44,6 +55,7 @@ class Constraint implements ConstraintInterface
* Integer to operator translation table.
*
* @var array
* @phpstan-var array<self::OP_*, self::STR_OP_*>
*/
private static $transOpInt = array(
self::OP_EQ => '==',
@@ -54,59 +66,23 @@ class Constraint implements ConstraintInterface
self::OP_NE => '!=',
);
/** @var string */
/**
* @var int
* @phpstan-var self::OP_*
*/
protected $operator;
/** @var string */
protected $version;
/** @var string */
/** @var string|null */
protected $prettyString;
/**
* @param ConstraintInterface $provider
*
* @return bool
*/
public function matches(ConstraintInterface $provider)
{
if ($provider instanceof $this) {
return $this->matchSpecific($provider);
}
/** @var Bound */
protected $lowerBound;
// turn matching around to find a match
return $provider->matches($this);
}
/**
* @param string $prettyString
*/
public function setPrettyString($prettyString)
{
$this->prettyString = $prettyString;
}
/**
* @return string
*/
public function getPrettyString()
{
if ($this->prettyString) {
return $this->prettyString;
}
return $this->__toString();
}
/**
* Get all supported comparison operators.
*
* @return array
*/
public static function getSupportedOperators()
{
return array_keys(self::$transOpStr);
}
/** @var Bound */
protected $upperBound;
/**
* Sets operator and version to compare with.
@@ -115,6 +91,8 @@ class Constraint implements ConstraintInterface
* @param string $version
*
* @throws \InvalidArgumentException if invalid operator is given.
*
* @phpstan-param self::STR_OP_* $operator
*/
public function __construct($operator, $version)
{
@@ -130,15 +108,94 @@ class Constraint implements ConstraintInterface
$this->version = $version;
}
/**
* @return string
*/
public function getVersion()
{
return $this->version;
}
/**
* @return string
*
* @phpstan-return self::STR_OP_*
*/
public function getOperator()
{
return self::$transOpInt[$this->operator];
}
/**
* @param ConstraintInterface $provider
*
* @return bool
*/
public function matches(ConstraintInterface $provider)
{
if ($provider instanceof self) {
return $this->matchSpecific($provider);
}
// turn matching around to find a match
return $provider->matches($this);
}
/**
* {@inheritDoc}
*/
public function setPrettyString($prettyString)
{
$this->prettyString = $prettyString;
}
/**
* {@inheritDoc}
*/
public function getPrettyString()
{
if ($this->prettyString) {
return $this->prettyString;
}
return $this->__toString();
}
/**
* Get all supported comparison operators.
*
* @return array
*
* @phpstan-return list<self::STR_OP_*>
*/
public static function getSupportedOperators()
{
return array_keys(self::$transOpStr);
}
/**
* @param string $operator
* @return int
*
* @phpstan-param self::STR_OP_* $operator
* @phpstan-return self::OP_*
*/
public static function getOperatorConstant($operator)
{
return self::$transOpStr[$operator];
}
/**
* @param string $a
* @param string $b
* @param string $operator
* @param bool $compareBranches
* @param bool $compareBranches
*
* @throws \InvalidArgumentException if invalid operator is given.
*
* @return bool
*
* @phpstan-param self::STR_OP_* $operator
*/
public function versionCompare($a, $b, $operator, $compareBranches = false)
{
@@ -150,8 +207,12 @@ class Constraint implements ConstraintInterface
));
}
$aIsBranch = 'dev-' === substr($a, 0, 4);
$bIsBranch = 'dev-' === substr($b, 0, 4);
$aIsBranch = strpos($a, 'dev-') === 0;
$bIsBranch = strpos($b, 'dev-') === 0;
if ($operator === '!=' && ($aIsBranch || $bIsBranch)) {
return $a !== $b;
}
if ($aIsBranch && $bIsBranch) {
return $operator === '==' && $a === $b;
@@ -162,12 +223,91 @@ class Constraint implements ConstraintInterface
return false;
}
return version_compare($a, $b, $operator);
return \version_compare($a, $b, $operator);
}
/**
* {@inheritDoc}
*/
public function compile($otherOperator)
{
if (strpos($this->version, 'dev-') === 0) {
if (self::OP_EQ === $this->operator) {
if (self::OP_EQ === $otherOperator) {
return sprintf('$b && $v === %s', \var_export($this->version, true));
}
if (self::OP_NE === $otherOperator) {
return sprintf('!$b || $v !== %s', \var_export($this->version, true));
}
return 'false';
}
if (self::OP_NE === $this->operator) {
if (self::OP_EQ === $otherOperator) {
return sprintf('!$b || $v !== %s', \var_export($this->version, true));
}
if (self::OP_NE === $otherOperator) {
return 'true';
}
return '!$b';
}
return 'false';
}
if (self::OP_EQ === $this->operator) {
if (self::OP_EQ === $otherOperator) {
return sprintf('\version_compare($v, %s, \'==\')', \var_export($this->version, true));
}
if (self::OP_NE === $otherOperator) {
return sprintf('$b || \version_compare($v, %s, \'!=\')', \var_export($this->version, true));
}
return sprintf('!$b && \version_compare(%s, $v, \'%s\')', \var_export($this->version, true), self::$transOpInt[$otherOperator]);
}
if (self::OP_NE === $this->operator) {
if (self::OP_EQ === $otherOperator) {
return sprintf('$b || (!$b && \version_compare($v, %s, \'!=\'))', \var_export($this->version, true));
}
if (self::OP_NE === $otherOperator) {
return 'true';
}
return '!$b';
}
if (self::OP_LT === $this->operator || self::OP_LE === $this->operator) {
if (self::OP_LT === $otherOperator || self::OP_LE === $otherOperator) {
return '!$b';
}
} else { // $this->operator must be self::OP_GT || self::OP_GE here
if (self::OP_GT === $otherOperator || self::OP_GE === $otherOperator) {
return '!$b';
}
}
if (self::OP_NE === $otherOperator) {
return 'true';
}
$codeComparison = sprintf('\version_compare($v, %s, \'%s\')', \var_export($this->version, true), self::$transOpInt[$this->operator]);
if ($this->operator === self::OP_LE) {
if ($otherOperator === self::OP_GT) {
return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison;
}
} elseif ($this->operator === self::OP_GE) {
if ($otherOperator === self::OP_LT) {
return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison;
}
}
return sprintf('!$b && %s', $codeComparison);
}
/**
* @param Constraint $provider
* @param bool $compareBranches
* @param bool $compareBranches
*
* @return bool
*/
@@ -184,22 +324,37 @@ class Constraint implements ConstraintInterface
// '!=' operator is match when other operator is not '==' operator or version is not match
// these kinds of comparisons always have a solution
if ($isNonEqualOp || $isProviderNonEqualOp) {
return (!$isEqualOp && !$isProviderEqualOp)
|| $this->versionCompare($provider->version, $this->version, '!=', $compareBranches);
if ($isNonEqualOp && !$isProviderNonEqualOp && !$isProviderEqualOp && strpos($provider->version, 'dev-') === 0) {
return false;
}
if ($isProviderNonEqualOp && !$isNonEqualOp && !$isEqualOp && strpos($this->version, 'dev-') === 0) {
return false;
}
if (!$isEqualOp && !$isProviderEqualOp) {
return true;
}
return $this->versionCompare($provider->version, $this->version, '!=', $compareBranches);
}
// an example for the condition is <= 2.0 & < 1.0
// these kinds of comparisons always have a solution
if ($this->operator !== self::OP_EQ && $noEqualOp === $providerNoEqualOp) {
return true;
return !(strpos($this->version, 'dev-') === 0 || strpos($provider->version, 'dev-') === 0);
}
if ($this->versionCompare($provider->version, $this->version, self::$transOpInt[$this->operator], $compareBranches)) {
$version1 = $isEqualOp ? $this->version : $provider->version;
$version2 = $isEqualOp ? $provider->version : $this->version;
$operator = $isEqualOp ? $provider->operator : $this->operator;
if ($this->versionCompare($version1, $version2, self::$transOpInt[$operator], $compareBranches)) {
// special case, e.g. require >= 1.0 and provide < 1.0
// 1.0 >= 1.0 but 1.0 is outside of the provided interval
return !($provider->version === $this->version
&& self::$transOpInt[$provider->operator] === $providerNoEqualOp
&& self::$transOpInt[$this->operator] !== $noEqualOp);
return !(self::$transOpInt[$provider->operator] === $providerNoEqualOp
&& self::$transOpInt[$this->operator] !== $noEqualOp
&& \version_compare($provider->version, $this->version, '=='));
}
return false;
@@ -212,4 +367,69 @@ class Constraint implements ConstraintInterface
{
return self::$transOpInt[$this->operator] . ' ' . $this->version;
}
/**
* {@inheritDoc}
*/
public function getLowerBound()
{
$this->extractBounds();
return $this->lowerBound;
}
/**
* {@inheritDoc}
*/
public function getUpperBound()
{
$this->extractBounds();
return $this->upperBound;
}
/**
* @return void
*/
private function extractBounds()
{
if (null !== $this->lowerBound) {
return;
}
// Branches
if (strpos($this->version, 'dev-') === 0) {
$this->lowerBound = Bound::zero();
$this->upperBound = Bound::positiveInfinity();
return;
}
switch ($this->operator) {
case self::OP_EQ:
$this->lowerBound = new Bound($this->version, true);
$this->upperBound = new Bound($this->version, true);
break;
case self::OP_LT:
$this->lowerBound = Bound::zero();
$this->upperBound = new Bound($this->version, false);
break;
case self::OP_LE:
$this->lowerBound = Bound::zero();
$this->upperBound = new Bound($this->version, true);
break;
case self::OP_GT:
$this->lowerBound = new Bound($this->version, false);
$this->upperBound = Bound::positiveInfinity();
break;
case self::OP_GE:
$this->lowerBound = new Bound($this->version, true);
$this->upperBound = Bound::positiveInfinity();
break;
case self::OP_NE:
$this->lowerBound = Bound::zero();
$this->upperBound = Bound::positiveInfinity();
break;
}
}
}
@@ -11,20 +11,63 @@
namespace Composer\Semver\Constraint;
/**
* DO NOT IMPLEMENT this interface. It is only meant for usage as a type hint
* in libraries relying on composer/semver but creating your own constraint class
* that implements this interface is not a supported use case and will cause the
* composer/semver components to return unexpected results.
*/
interface ConstraintInterface
{
/**
* Checks whether the given constraint intersects in any way with this constraint
*
* @param ConstraintInterface $provider
*
* @return bool
*/
public function matches(ConstraintInterface $provider);
/**
* Provides a compiled version of the constraint for the given operator
* The compiled version must be a PHP expression.
* Executor of compile version must provide 2 variables:
* - $v = the string version to compare with
* - $b = whether or not the version is a non-comparable branch (starts with "dev-")
*
* @see Constraint::OP_* for the list of available operators.
* @example return '!$b && version_compare($v, '1.0', '>')';
*
* @param int $otherOperator one Constraint::OP_*
*
* @return string
*
* @phpstan-param Constraint::OP_* $otherOperator
*/
public function compile($otherOperator);
/**
* @return Bound
*/
public function getUpperBound();
/**
* @return Bound
*/
public function getLowerBound();
/**
* @return string
*/
public function getPrettyString();
/**
* @param string|null $prettyString
*
* @return void
*/
public function setPrettyString($prettyString);
/**
* @return string
*/
@@ -13,10 +13,12 @@ namespace Composer\Semver\Constraint;
/**
* Defines the absence of a constraint.
*
* This constraint matches everything.
*/
class EmptyConstraint implements ConstraintInterface
class MatchAllConstraint implements ConstraintInterface
{
/** @var string */
/** @var string|null */
protected $prettyString;
/**
@@ -30,7 +32,15 @@ class EmptyConstraint implements ConstraintInterface
}
/**
* @param $prettyString
* {@inheritDoc}
*/
public function compile($otherOperator)
{
return 'true';
}
/**
* {@inheritDoc}
*/
public function setPrettyString($prettyString)
{
@@ -38,7 +48,7 @@ class EmptyConstraint implements ConstraintInterface
}
/**
* @return string
* {@inheritDoc}
*/
public function getPrettyString()
{
@@ -50,10 +60,26 @@ class EmptyConstraint implements ConstraintInterface
}
/**
* @return string
* {@inheritDoc}
*/
public function __toString()
{
return '[]';
return '*';
}
/**
* {@inheritDoc}
*/
public function getUpperBound()
{
return Bound::positiveInfinity();
}
/**
* {@inheritDoc}
*/
public function getLowerBound()
{
return Bound::zero();
}
}
@@ -0,0 +1,83 @@
<?php
/*
* This file is part of composer/semver.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Semver\Constraint;
/**
* Blackhole of constraints, nothing escapes it
*/
class MatchNoneConstraint implements ConstraintInterface
{
/** @var string|null */
protected $prettyString;
/**
* @param ConstraintInterface $provider
*
* @return bool
*/
public function matches(ConstraintInterface $provider)
{
return false;
}
/**
* {@inheritDoc}
*/
public function compile($otherOperator)
{
return 'false';
}
/**
* {@inheritDoc}
*/
public function setPrettyString($prettyString)
{
$this->prettyString = $prettyString;
}
/**
* {@inheritDoc}
*/
public function getPrettyString()
{
if ($this->prettyString) {
return $this->prettyString;
}
return (string) $this;
}
/**
* {@inheritDoc}
*/
public function __toString()
{
return '[]';
}
/**
* {@inheritDoc}
*/
public function getUpperBound()
{
return new Bound('0.0.0.0-dev', false);
}
/**
* {@inheritDoc}
*/
public function getLowerBound()
{
return new Bound('0.0.0.0-dev', false);
}
}
+214 -9
View File
@@ -16,21 +16,43 @@ namespace Composer\Semver\Constraint;
*/
class MultiConstraint implements ConstraintInterface
{
/** @var ConstraintInterface[] */
/**
* @var ConstraintInterface[]
* @phpstan-var non-empty-array<ConstraintInterface>
*/
protected $constraints;
/** @var string */
/** @var string|null */
protected $prettyString;
/** @var string|null */
protected $string;
/** @var bool */
protected $conjunctive;
/** @var Bound|null */
protected $lowerBound;
/** @var Bound|null */
protected $upperBound;
/**
* @param ConstraintInterface[] $constraints A set of constraints
* @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
* @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
*
* @throws \InvalidArgumentException If less than 2 constraints are passed
*/
public function __construct(array $constraints, $conjunctive = true)
{
if (\count($constraints) < 2) {
throw new \InvalidArgumentException(
'Must provide at least two constraints for a MultiConstraint. Use '.
'the regular Constraint class for one constraint only or MatchAllConstraint for none. You may use '.
'MultiConstraint::create() which optimizes and handles those cases automatically.'
);
}
$this->constraints = $constraints;
$this->conjunctive = $conjunctive;
}
@@ -59,6 +81,34 @@ class MultiConstraint implements ConstraintInterface
return !$this->conjunctive;
}
/**
* {@inheritDoc}
*/
public function compile($otherOperator)
{
$parts = array();
foreach ($this->constraints as $constraint) {
$code = $constraint->compile($otherOperator);
if ($code === 'true') {
if (!$this->conjunctive) {
return 'true';
}
} elseif ($code === 'false') {
if ($this->conjunctive) {
return 'false';
}
} else {
$parts[] = '('.$code.')';
}
}
if (!$parts) {
return $this->conjunctive ? 'true' : 'false';
}
return $this->conjunctive ? implode('&&', $parts) : implode('||', $parts);
}
/**
* @param ConstraintInterface $provider
*
@@ -68,7 +118,7 @@ class MultiConstraint implements ConstraintInterface
{
if (false === $this->conjunctive) {
foreach ($this->constraints as $constraint) {
if ($constraint->matches($provider)) {
if ($provider->matches($constraint)) {
return true;
}
}
@@ -76,8 +126,15 @@ class MultiConstraint implements ConstraintInterface
return false;
}
// when matching a conjunctive and a disjunctive multi constraint we have to iterate over the disjunctive one
// otherwise we'd return true if different parts of the disjunctive constraint match the conjunctive one
// which would lead to incorrect results, e.g. [>1 and <2] would match [<1 or >2] although they do not intersect
if ($provider instanceof MultiConstraint && $provider->isDisjunctive()) {
return $provider->matches($this);
}
foreach ($this->constraints as $constraint) {
if (!$constraint->matches($provider)) {
if (!$provider->matches($constraint)) {
return false;
}
}
@@ -86,7 +143,7 @@ class MultiConstraint implements ConstraintInterface
}
/**
* @param string $prettyString
* {@inheritDoc}
*/
public function setPrettyString($prettyString)
{
@@ -94,7 +151,7 @@ class MultiConstraint implements ConstraintInterface
}
/**
* @return string
* {@inheritDoc}
*/
public function getPrettyString()
{
@@ -106,15 +163,163 @@ class MultiConstraint implements ConstraintInterface
}
/**
* @return string
* {@inheritDoc}
*/
public function __toString()
{
if ($this->string !== null) {
return $this->string;
}
$constraints = array();
foreach ($this->constraints as $constraint) {
$constraints[] = (string) $constraint;
}
return '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']';
return $this->string = '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']';
}
/**
* {@inheritDoc}
*/
public function getLowerBound()
{
$this->extractBounds();
if (null === $this->lowerBound) {
throw new \LogicException('extractBounds should have populated the lowerBound property');
}
return $this->lowerBound;
}
/**
* {@inheritDoc}
*/
public function getUpperBound()
{
$this->extractBounds();
if (null === $this->upperBound) {
throw new \LogicException('extractBounds should have populated the upperBound property');
}
return $this->upperBound;
}
/**
* Tries to optimize the constraints as much as possible, meaning
* reducing/collapsing congruent constraints etc.
* Does not necessarily return a MultiConstraint instance if
* things can be reduced to a simple constraint
*
* @param ConstraintInterface[] $constraints A set of constraints
* @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
*
* @return ConstraintInterface
*/
public static function create(array $constraints, $conjunctive = true)
{
if (0 === \count($constraints)) {
return new MatchAllConstraint();
}
if (1 === \count($constraints)) {
return $constraints[0];
}
$optimized = self::optimizeConstraints($constraints, $conjunctive);
if ($optimized !== null) {
list($constraints, $conjunctive) = $optimized;
if (\count($constraints) === 1) {
return $constraints[0];
}
}
return new self($constraints, $conjunctive);
}
/**
* @param ConstraintInterface[] $constraints
* @param bool $conjunctive
* @return ?array
*
* @phpstan-return array{0: list<ConstraintInterface>, 1: bool}|null
*/
private static function optimizeConstraints(array $constraints, $conjunctive)
{
// parse the two OR groups and if they are contiguous we collapse
// them into one constraint
// [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4]
if (!$conjunctive) {
$left = $constraints[0];
$mergedConstraints = array();
$optimized = false;
for ($i = 1, $l = \count($constraints); $i < $l; $i++) {
$right = $constraints[$i];
if (
$left instanceof self
&& $left->conjunctive
&& $right instanceof self
&& $right->conjunctive
&& \count($left->constraints) === 2
&& \count($right->constraints) === 2
&& ($left0 = (string) $left->constraints[0])
&& $left0[0] === '>' && $left0[1] === '='
&& ($left1 = (string) $left->constraints[1])
&& $left1[0] === '<'
&& ($right0 = (string) $right->constraints[0])
&& $right0[0] === '>' && $right0[1] === '='
&& ($right1 = (string) $right->constraints[1])
&& $right1[0] === '<'
&& substr($left1, 2) === substr($right0, 3)
) {
$optimized = true;
$left = new MultiConstraint(
array(
$left->constraints[0],
$right->constraints[1],
),
true);
} else {
$mergedConstraints[] = $left;
$left = $right;
}
}
if ($optimized) {
$mergedConstraints[] = $left;
return array($mergedConstraints, false);
}
}
// TODO: Here's the place to put more optimizations
return null;
}
/**
* @return void
*/
private function extractBounds()
{
if (null !== $this->lowerBound) {
return;
}
foreach ($this->constraints as $constraint) {
if (null === $this->lowerBound || null === $this->upperBound) {
$this->lowerBound = $constraint->getLowerBound();
$this->upperBound = $constraint->getUpperBound();
continue;
}
if ($constraint->getLowerBound()->compareTo($this->lowerBound, $this->isConjunctive() ? '>' : '<')) {
$this->lowerBound = $constraint->getLowerBound();
}
if ($constraint->getUpperBound()->compareTo($this->upperBound, $this->isConjunctive() ? '<' : '>')) {
$this->upperBound = $constraint->getUpperBound();
}
}
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
/*
* This file is part of composer/semver.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Semver;
use Composer\Semver\Constraint\Constraint;
class Interval
{
/** @var Constraint */
private $start;
/** @var Constraint */
private $end;
public function __construct(Constraint $start, Constraint $end)
{
$this->start = $start;
$this->end = $end;
}
/**
* @return Constraint
*/
public function getStart()
{
return $this->start;
}
/**
* @return Constraint
*/
public function getEnd()
{
return $this->end;
}
/**
* @return Constraint
*/
public static function fromZero()
{
static $zero;
if (null === $zero) {
$zero = new Constraint('>=', '0.0.0.0-dev');
}
return $zero;
}
/**
* @return Constraint
*/
public static function untilPositiveInfinity()
{
static $positiveInfinity;
if (null === $positiveInfinity) {
$positiveInfinity = new Constraint('<', PHP_INT_MAX.'.0.0.0');
}
return $positiveInfinity;
}
/**
* @return self
*/
public static function any()
{
return new self(self::fromZero(), self::untilPositiveInfinity());
}
/**
* @return array{'names': string[], 'exclude': bool}
*/
public static function anyDev()
{
// any == exclude nothing
return array('names' => array(), 'exclude' => true);
}
/**
* @return array{'names': string[], 'exclude': bool}
*/
public static function noDev()
{
// nothing == no names included
return array('names' => array(), 'exclude' => false);
}
}
+478
View File
@@ -0,0 +1,478 @@
<?php
/*
* This file is part of composer/semver.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Semver;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Semver\Constraint\MatchNoneConstraint;
use Composer\Semver\Constraint\MultiConstraint;
/**
* Helper class generating intervals from constraints
*
* This contains utilities for:
*
* - compacting an existing constraint which can be used to combine several into one
* by creating a MultiConstraint out of the many constraints you have.
*
* - checking whether one subset is a subset of another.
*
* Note: You should call clear to free memoization memory usage when you are done using this class
*/
class Intervals
{
/**
* @phpstan-var array<string, array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}>
*/
private static $intervalsCache = array();
/**
* @phpstan-var array<string, int>
*/
private static $opSortOrder = array(
'>=' => -3,
'<' => -2,
'>' => 2,
'<=' => 3,
);
/**
* Clears the memoization cache once you are done
*
* @return void
*/
public static function clear()
{
self::$intervalsCache = array();
}
/**
* Checks whether $candidate is a subset of $constraint
*
* @return bool
*/
public static function isSubsetOf(ConstraintInterface $candidate, ConstraintInterface $constraint)
{
if ($constraint instanceof MatchAllConstraint) {
return true;
}
if ($candidate instanceof MatchNoneConstraint || $constraint instanceof MatchNoneConstraint) {
return false;
}
$intersectionIntervals = self::get(new MultiConstraint(array($candidate, $constraint), true));
$candidateIntervals = self::get($candidate);
if (\count($intersectionIntervals['numeric']) !== \count($candidateIntervals['numeric'])) {
return false;
}
foreach ($intersectionIntervals['numeric'] as $index => $interval) {
if (!isset($candidateIntervals['numeric'][$index])) {
return false;
}
if ((string) $candidateIntervals['numeric'][$index]->getStart() !== (string) $interval->getStart()) {
return false;
}
if ((string) $candidateIntervals['numeric'][$index]->getEnd() !== (string) $interval->getEnd()) {
return false;
}
}
if ($intersectionIntervals['branches']['exclude'] !== $candidateIntervals['branches']['exclude']) {
return false;
}
if (\count($intersectionIntervals['branches']['names']) !== \count($candidateIntervals['branches']['names'])) {
return false;
}
foreach ($intersectionIntervals['branches']['names'] as $index => $name) {
if ($name !== $candidateIntervals['branches']['names'][$index]) {
return false;
}
}
return true;
}
/**
* Checks whether $a and $b have any intersection, equivalent to $a->matches($b)
*
* @return bool
*/
public static function haveIntersections(ConstraintInterface $a, ConstraintInterface $b)
{
if ($a instanceof MatchAllConstraint || $b instanceof MatchAllConstraint) {
return true;
}
if ($a instanceof MatchNoneConstraint || $b instanceof MatchNoneConstraint) {
return false;
}
$intersectionIntervals = self::generateIntervals(new MultiConstraint(array($a, $b), true), true);
return \count($intersectionIntervals['numeric']) > 0 || $intersectionIntervals['branches']['exclude'] || \count($intersectionIntervals['branches']['names']) > 0;
}
/**
* Attempts to optimize a MultiConstraint
*
* When merging MultiConstraints together they can get very large, this will
* compact it by looking at the real intervals covered by all the constraints
* and then creates a new constraint containing only the smallest amount of rules
* to match the same intervals.
*
* @return ConstraintInterface
*/
public static function compactConstraint(ConstraintInterface $constraint)
{
if (!$constraint instanceof MultiConstraint) {
return $constraint;
}
$intervals = self::generateIntervals($constraint);
$constraints = array();
$hasNumericMatchAll = false;
if (\count($intervals['numeric']) === 1 && (string) $intervals['numeric'][0]->getStart() === (string) Interval::fromZero() && (string) $intervals['numeric'][0]->getEnd() === (string) Interval::untilPositiveInfinity()) {
$constraints[] = $intervals['numeric'][0]->getStart();
$hasNumericMatchAll = true;
} else {
$unEqualConstraints = array();
for ($i = 0, $count = \count($intervals['numeric']); $i < $count; $i++) {
$interval = $intervals['numeric'][$i];
// if current interval ends with < N and next interval begins with > N we can swap this out for != N
// but this needs to happen as a conjunctive expression together with the start of the current interval
// and end of next interval, so [>=M, <N] || [>N, <P] => [>=M, !=N, <P] but M/P can be skipped if
// they are zero/+inf
if ($interval->getEnd()->getOperator() === '<' && $i+1 < $count) {
$nextInterval = $intervals['numeric'][$i+1];
if ($interval->getEnd()->getVersion() === $nextInterval->getStart()->getVersion() && $nextInterval->getStart()->getOperator() === '>') {
// only add a start if we didn't already do so, can be skipped if we're looking at second
// interval in [>=M, <N] || [>N, <P] || [>P, <Q] where unEqualConstraints currently contains
// [>=M, !=N] already and we only want to add !=P right now
if (\count($unEqualConstraints) === 0 && (string) $interval->getStart() !== (string) Interval::fromZero()) {
$unEqualConstraints[] = $interval->getStart();
}
$unEqualConstraints[] = new Constraint('!=', $interval->getEnd()->getVersion());
continue;
}
}
if (\count($unEqualConstraints) > 0) {
// this is where the end of the following interval of a != constraint is added as explained above
if ((string) $interval->getEnd() !== (string) Interval::untilPositiveInfinity()) {
$unEqualConstraints[] = $interval->getEnd();
}
// count is 1 if entire constraint is just one != expression
if (\count($unEqualConstraints) > 1) {
$constraints[] = new MultiConstraint($unEqualConstraints, true);
} else {
$constraints[] = $unEqualConstraints[0];
}
$unEqualConstraints = array();
continue;
}
// convert back >= x - <= x intervals to == x
if ($interval->getStart()->getVersion() === $interval->getEnd()->getVersion() && $interval->getStart()->getOperator() === '>=' && $interval->getEnd()->getOperator() === '<=') {
$constraints[] = new Constraint('==', $interval->getStart()->getVersion());
continue;
}
if ((string) $interval->getStart() === (string) Interval::fromZero()) {
$constraints[] = $interval->getEnd();
} elseif ((string) $interval->getEnd() === (string) Interval::untilPositiveInfinity()) {
$constraints[] = $interval->getStart();
} else {
$constraints[] = new MultiConstraint(array($interval->getStart(), $interval->getEnd()), true);
}
}
}
$devConstraints = array();
if (0 === \count($intervals['branches']['names'])) {
if ($intervals['branches']['exclude']) {
if ($hasNumericMatchAll) {
return new MatchAllConstraint;
}
// otherwise constraint should contain a != operator and already cover this
}
} else {
foreach ($intervals['branches']['names'] as $branchName) {
if ($intervals['branches']['exclude']) {
$devConstraints[] = new Constraint('!=', $branchName);
} else {
$devConstraints[] = new Constraint('==', $branchName);
}
}
// excluded branches, e.g. != dev-foo are conjunctive with the interval, so
// > 2.0 != dev-foo must return a conjunctive constraint
if ($intervals['branches']['exclude']) {
if (\count($constraints) > 1) {
return new MultiConstraint(array_merge(
array(new MultiConstraint($constraints, false)),
$devConstraints
), true);
}
if (\count($constraints) === 1 && (string)$constraints[0] === (string)Interval::fromZero()) {
if (\count($devConstraints) > 1) {
return new MultiConstraint($devConstraints, true);
}
return $devConstraints[0];
}
return new MultiConstraint(array_merge($constraints, $devConstraints), true);
}
// otherwise devConstraints contains a list of == operators for branches which are disjunctive with the
// rest of the constraint
$constraints = array_merge($constraints, $devConstraints);
}
if (\count($constraints) > 1) {
return new MultiConstraint($constraints, false);
}
if (\count($constraints) === 1) {
return $constraints[0];
}
return new MatchNoneConstraint;
}
/**
* Creates an array of numeric intervals and branch constraints representing a given constraint
*
* if the returned numeric array is empty it means the constraint matches nothing in the numeric range (0 - +inf)
* if the returned branches array is empty it means no dev-* versions are matched
* if a constraint matches all possible dev-* versions, branches will contain Interval::anyDev()
*
* @return array
* @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}
*/
public static function get(ConstraintInterface $constraint)
{
$key = (string) $constraint;
if (!isset(self::$intervalsCache[$key])) {
self::$intervalsCache[$key] = self::generateIntervals($constraint);
}
return self::$intervalsCache[$key];
}
/**
* @param bool $stopOnFirstValidInterval
*
* @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}
*/
private static function generateIntervals(ConstraintInterface $constraint, $stopOnFirstValidInterval = false)
{
if ($constraint instanceof MatchAllConstraint) {
return array('numeric' => array(new Interval(Interval::fromZero(), Interval::untilPositiveInfinity())), 'branches' => Interval::anyDev());
}
if ($constraint instanceof MatchNoneConstraint) {
return array('numeric' => array(), 'branches' => array('names' => array(), 'exclude' => false));
}
if ($constraint instanceof Constraint) {
return self::generateSingleConstraintIntervals($constraint);
}
if (!$constraint instanceof MultiConstraint) {
throw new \UnexpectedValueException('The constraint passed in should be an MatchAllConstraint, Constraint or MultiConstraint instance, got '.\get_class($constraint).'.');
}
$constraints = $constraint->getConstraints();
$numericGroups = array();
$constraintBranches = array();
foreach ($constraints as $c) {
$res = self::get($c);
$numericGroups[] = $res['numeric'];
$constraintBranches[] = $res['branches'];
}
if ($constraint->isDisjunctive()) {
$branches = Interval::noDev();
foreach ($constraintBranches as $b) {
if ($b['exclude']) {
if ($branches['exclude']) {
// disjunctive constraint, so only exclude what's excluded in all constraints
// !=a,!=b || !=b,!=c => !=b
$branches['names'] = array_intersect($branches['names'], $b['names']);
} else {
// disjunctive constraint so exclude all names which are not explicitly included in the alternative
// (==b || ==c) || !=a,!=b => !=a
$branches['exclude'] = true;
$branches['names'] = array_diff($b['names'], $branches['names']);
}
} else {
if ($branches['exclude']) {
// disjunctive constraint so exclude all names which are not explicitly included in the alternative
// !=a,!=b || (==b || ==c) => !=a
$branches['names'] = array_diff($branches['names'], $b['names']);
} else {
// disjunctive constraint, so just add all the other branches
// (==a || ==b) || ==c => ==a || ==b || ==c
$branches['names'] = array_merge($branches['names'], $b['names']);
}
}
}
} else {
$branches = Interval::anyDev();
foreach ($constraintBranches as $b) {
if ($b['exclude']) {
if ($branches['exclude']) {
// conjunctive, so just add all branch names to be excluded
// !=a && !=b => !=a,!=b
$branches['names'] = array_merge($branches['names'], $b['names']);
} else {
// conjunctive, so only keep included names which are not excluded
// (==a||==c) && !=a,!=b => ==c
$branches['names'] = array_diff($branches['names'], $b['names']);
}
} else {
if ($branches['exclude']) {
// conjunctive, so only keep included names which are not excluded
// !=a,!=b && (==a||==c) => ==c
$branches['names'] = array_diff($b['names'], $branches['names']);
$branches['exclude'] = false;
} else {
// conjunctive, so only keep names that are included in both
// (==a||==b) && (==a||==c) => ==a
$branches['names'] = array_intersect($branches['names'], $b['names']);
}
}
}
}
$branches['names'] = array_unique($branches['names']);
if (\count($numericGroups) === 1) {
return array('numeric' => $numericGroups[0], 'branches' => $branches);
}
$borders = array();
foreach ($numericGroups as $group) {
foreach ($group as $interval) {
$borders[] = array('version' => $interval->getStart()->getVersion(), 'operator' => $interval->getStart()->getOperator(), 'side' => 'start');
$borders[] = array('version' => $interval->getEnd()->getVersion(), 'operator' => $interval->getEnd()->getOperator(), 'side' => 'end');
}
}
$opSortOrder = self::$opSortOrder;
usort($borders, function ($a, $b) use ($opSortOrder) {
$order = version_compare($a['version'], $b['version']);
if ($order === 0) {
return $opSortOrder[$a['operator']] - $opSortOrder[$b['operator']];
}
return $order;
});
$activeIntervals = 0;
$intervals = array();
$index = 0;
$activationThreshold = $constraint->isConjunctive() ? \count($numericGroups) : 1;
$start = null;
foreach ($borders as $border) {
if ($border['side'] === 'start') {
$activeIntervals++;
} else {
$activeIntervals--;
}
if (!$start && $activeIntervals >= $activationThreshold) {
$start = new Constraint($border['operator'], $border['version']);
} elseif ($start && $activeIntervals < $activationThreshold) {
// filter out invalid intervals like > x - <= x, or >= x - < x
if (
version_compare($start->getVersion(), $border['version'], '=')
&& (
($start->getOperator() === '>' && $border['operator'] === '<=')
|| ($start->getOperator() === '>=' && $border['operator'] === '<')
)
) {
unset($intervals[$index]);
} else {
$intervals[$index] = new Interval($start, new Constraint($border['operator'], $border['version']));
$index++;
if ($stopOnFirstValidInterval) {
break;
}
}
$start = null;
}
}
return array('numeric' => $intervals, 'branches' => $branches);
}
/**
* @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}}
*/
private static function generateSingleConstraintIntervals(Constraint $constraint)
{
$op = $constraint->getOperator();
// handle branch constraints first
if (strpos($constraint->getVersion(), 'dev-') === 0) {
$intervals = array();
$branches = array('names' => array(), 'exclude' => false);
// != dev-foo means any numeric version may match, we treat >/< like != they are not really defined for branches
if ($op === '!=') {
$intervals[] = new Interval(Interval::fromZero(), Interval::untilPositiveInfinity());
$branches = array('names' => array($constraint->getVersion()), 'exclude' => true);
} elseif ($op === '==') {
$branches['names'][] = $constraint->getVersion();
}
return array(
'numeric' => $intervals,
'branches' => $branches,
);
}
if ($op[0] === '>') { // > & >=
return array('numeric' => array(new Interval($constraint, Interval::untilPositiveInfinity())), 'branches' => Interval::noDev());
}
if ($op[0] === '<') { // < & <=
return array('numeric' => array(new Interval(Interval::fromZero(), $constraint)), 'branches' => Interval::noDev());
}
if ($op === '!=') {
// convert !=x to intervals of 0 - <x && >x - +inf + dev*
return array('numeric' => array(
new Interval(Interval::fromZero(), new Constraint('<', $constraint->getVersion())),
new Interval(new Constraint('>', $constraint->getVersion()), Interval::untilPositiveInfinity()),
), 'branches' => Interval::anyDev());
}
// convert ==x to an interval of >=x - <=x
return array('numeric' => array(
new Interval(new Constraint('>=', $constraint->getVersion()), new Constraint('<=', $constraint->getVersion())),
), 'branches' => Interval::noDev());
}
}
+13 -11
View File
@@ -45,10 +45,10 @@ class Semver
/**
* Return all versions that satisfy given constraints.
*
* @param array $versions
* @param string $constraints
* @param string[] $versions
* @param string $constraints
*
* @return array
* @return string[]
*/
public static function satisfiedBy(array $versions, $constraints)
{
@@ -62,9 +62,9 @@ class Semver
/**
* Sort given array of versions.
*
* @param array $versions
* @param string[] $versions
*
* @return array
* @return string[]
*/
public static function sort(array $versions)
{
@@ -74,9 +74,9 @@ class Semver
/**
* Sort given array of versions in reverse.
*
* @param array $versions
* @param string[] $versions
*
* @return array
* @return string[]
*/
public static function rsort(array $versions)
{
@@ -84,10 +84,10 @@ class Semver
}
/**
* @param array $versions
* @param int $direction
* @param string[] $versions
* @param int $direction
*
* @return array
* @return string[]
*/
private static function usort(array $versions, $direction)
{
@@ -101,7 +101,9 @@ class Semver
// Normalize outside of usort() scope for minor performance increase.
// Creates an array of arrays: [[normalized, key], ...]
foreach ($versions as $key => $version) {
$normalized[] = array($versionParser->normalize($version), $key);
$normalizedVersion = $versionParser->normalize($version);
$normalizedVersion = $versionParser->normalizeDefaultBranch($normalizedVersion);
$normalized[] = array($normalizedVersion, $key);
}
usort($normalized, function (array $left, array $right) use ($direction) {
+108 -70
View File
@@ -12,7 +12,7 @@
namespace Composer\Semver;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\EmptyConstraint;
use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\Semver\Constraint\Constraint;
@@ -38,8 +38,8 @@ class VersionParser
*/
private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?';
/** @var array */
private static $stabilities = array('stable', 'RC', 'beta', 'alpha', 'dev');
/** @var string */
private static $stabilitiesRegex = 'stable|RC|beta|alpha|dev';
/**
* Returns the stability of a version.
@@ -50,7 +50,7 @@ class VersionParser
*/
public static function parseStability($version)
{
$version = preg_replace('{#.+$}i', '', $version);
$version = (string) preg_replace('{#.+$}', '', $version);
if (strpos($version, 'dev-') === 0 || '-dev' === substr($version, -4)) {
return 'dev';
@@ -102,21 +102,24 @@ class VersionParser
public function normalize($version, $fullVersion = null)
{
$version = trim($version);
$origVersion = $version;
if (null === $fullVersion) {
$fullVersion = $version;
}
// strip off aliasing
if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) {
// verify that the alias is a version without constraint
$this->normalize($match[2]);
$version = $match[1];
}
// match master-like branches
if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) {
return '9999999-dev';
// strip off stability flag
if (preg_match('{@(?:' . self::$stabilitiesRegex . ')$}i', $version, $match)) {
$version = substr($version, 0, strlen($version) - strlen($match[0]));
}
// normalize master/trunk/default branches to dev-name for BC with 1.x as these used to be valid constraints
if (\in_array($version, array('master', 'trunk', 'default'), true)) {
$version = 'dev-' . $version;
}
// if requirement is branch-like, use full name
@@ -148,7 +151,7 @@ class VersionParser
if ('stable' === $matches[$index]) {
return $version;
}
$version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index + 1]) ? ltrim($matches[$index + 1], '.-') : '');
$version .= '-' . $this->expandStability($matches[$index]) . (isset($matches[$index + 1]) && '' !== $matches[$index + 1] ? ltrim($matches[$index + 1], '.-') : '');
}
if (!empty($matches[$index + 2])) {
@@ -161,19 +164,25 @@ class VersionParser
// match dev branches
if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) {
try {
return $this->normalizeBranch($match[1]);
$normalized = $this->normalizeBranch($match[1]);
// a branch ending with -dev is only valid if it is numeric
// if it gets prefixed with dev- it means the branch name should
// have had a dev- prefix already when passed to normalize
if (strpos($normalized, 'dev-') === false) {
return $normalized;
}
} catch (\Exception $e) {
}
}
$extraMessage = '';
if (preg_match('{ +as +' . preg_quote($version) . '$}', $fullVersion)) {
if (preg_match('{ +as +' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))?$}', $fullVersion)) {
$extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version';
} elseif (preg_match('{^' . preg_quote($version) . ' +as +}', $fullVersion)) {
} elseif (preg_match('{^' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))? +as +}', $fullVersion)) {
$extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-';
}
throw new \UnexpectedValueException('Invalid version string "' . $version . '"' . $extraMessage);
throw new \UnexpectedValueException('Invalid version string "' . $origVersion . '"' . $extraMessage);
}
/**
@@ -203,10 +212,6 @@ class VersionParser
{
$name = trim($name);
if (in_array($name, array('master', 'trunk', 'default'))) {
return $this->normalize($name);
}
if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) {
$version = '';
for ($i = 1; $i < 5; ++$i) {
@@ -219,6 +224,22 @@ class VersionParser
return 'dev-' . $name;
}
/**
* Normalizes a default branch name (i.e. master on git) to 9999999-dev.
*
* @param string $name
*
* @return string
*/
public function normalizeDefaultBranch($name)
{
if ($name === 'dev-master' || $name === 'dev-default' || $name === 'dev-trunk') {
return '9999999-dev';
}
return $name;
}
/**
* Parses a constraint string into MultiConstraint and/or Constraint objects.
*
@@ -230,20 +251,18 @@ class VersionParser
{
$prettyConstraint = $constraints;
if (preg_match('{^([^,\s]*?)@(' . implode('|', self::$stabilities) . ')$}i', $constraints, $match)) {
$constraints = empty($match[1]) ? '*' : $match[1];
}
if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraints, $match)) {
$constraints = $match[1];
}
$orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints));
if (false === $orConstraints) {
throw new \RuntimeException('Failed to preg_split string: '.$constraints);
}
$orGroups = array();
foreach ($orConstraints as $constraints) {
$andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $constraints);
if (count($andConstraints) > 1) {
if (false === $andConstraints) {
throw new \RuntimeException('Failed to preg_split string: '.$constraints);
}
if (\count($andConstraints) > 1) {
$constraintObjects = array();
foreach ($andConstraints as $constraint) {
foreach ($this->parseConstraint($constraint) as $parsedConstraint) {
@@ -254,7 +273,7 @@ class VersionParser
$constraintObjects = $this->parseConstraint($andConstraints[0]);
}
if (1 === count($constraintObjects)) {
if (1 === \count($constraintObjects)) {
$constraint = $constraintObjects[0];
} else {
$constraint = new MultiConstraint($constraintObjects);
@@ -263,28 +282,7 @@ class VersionParser
$orGroups[] = $constraint;
}
if (1 === count($orGroups)) {
$constraint = $orGroups[0];
} elseif (2 === count($orGroups)
// parse the two OR groups and if they are contiguous we collapse
// them into one constraint
&& $orGroups[0] instanceof MultiConstraint
&& $orGroups[1] instanceof MultiConstraint
&& 2 === count($orGroups[0]->getConstraints())
&& 2 === count($orGroups[1]->getConstraints())
&& ($a = (string) $orGroups[0])
&& strpos($a, '[>=') === 0 && (false !== ($posA = strpos($a, '<', 4)))
&& ($b = (string) $orGroups[1])
&& strpos($b, '[>=') === 0 && (false !== ($posB = strpos($b, '<', 4)))
&& substr($a, $posA + 2, -1) === substr($b, 4, $posB - 5)
) {
$constraint = new MultiConstraint(array(
new Constraint('>=', substr($a, 4, $posA - 5)),
new Constraint('<', substr($b, $posB + 2, -1)),
));
} else {
$constraint = new MultiConstraint($orGroups, false);
}
$constraint = MultiConstraint::create($orGroups, false);
$constraint->setPrettyString($prettyConstraint);
@@ -297,21 +295,38 @@ class VersionParser
* @throws \UnexpectedValueException
*
* @return array
*
* @phpstan-return non-empty-array<ConstraintInterface>
*/
private function parseConstraint($constraint)
{
if (preg_match('{^([^,\s]+?)@(' . implode('|', self::$stabilities) . ')$}i', $constraint, $match)) {
// strip off aliasing
if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $constraint, $match)) {
$constraint = $match[1];
}
// strip @stability flags, and keep it for later use
if (preg_match('{^([^,\s]*?)@(' . self::$stabilitiesRegex . ')$}i', $constraint, $match)) {
$constraint = '' !== $match[1] ? $match[1] : '*';
if ($match[2] !== 'stable') {
$stabilityModifier = $match[2];
}
}
if (preg_match('{^v?[xX*](\.[xX*])*$}i', $constraint)) {
return array(new EmptyConstraint());
// get rid of #refs as those are used by composer only
if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraint, $match)) {
$constraint = $match[1];
}
$versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?' . self::$modifierRegex . '(?:\+[^\s]+)?';
if (preg_match('{^(v)?[xX*](\.[xX*])*$}i', $constraint, $match)) {
if (!empty($match[1]) || !empty($match[2])) {
return array(new Constraint('>=', '0.0.0.0-dev'));
}
return array(new MatchAllConstraint());
}
$versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:' . self::$modifierRegex . '|\.([xX*][.-]?dev))(?:\+[^\s]+)?';
// Tilde Range
//
@@ -337,9 +352,14 @@ class VersionParser
$position = 1;
}
// 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
if (!empty($matches[8])) {
$position++;
}
// Calculate the stability suffix
$stabilitySuffix = '';
if (empty($matches[5]) && empty($matches[7])) {
if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) {
$stabilitySuffix .= '-dev';
}
@@ -375,7 +395,7 @@ class VersionParser
// Calculate the stability suffix
$stabilitySuffix = '';
if (empty($matches[5]) && empty($matches[7])) {
if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) {
$stabilitySuffix .= '-dev';
}
@@ -428,7 +448,7 @@ class VersionParser
if (preg_match('{^(?P<from>' . $versionRegex . ') +- +(?P<to>' . $versionRegex . ')($)}i', $constraint, $matches)) {
// Calculate the stability suffix
$lowStabilitySuffix = '';
if (empty($matches[6]) && empty($matches[8])) {
if (empty($matches[6]) && empty($matches[8]) && empty($matches[9])) {
$lowStabilitySuffix = '-dev';
}
@@ -439,12 +459,16 @@ class VersionParser
return ($x === 0 || $x === '0') ? false : empty($x);
};
if ((!$empty($matches[11]) && !$empty($matches[12])) || !empty($matches[14]) || !empty($matches[16])) {
if ((!$empty($matches[12]) && !$empty($matches[13])) || !empty($matches[15]) || !empty($matches[17]) || !empty($matches[18])) {
$highVersion = $this->normalize($matches['to']);
$upperBound = new Constraint('<=', $highVersion);
} else {
$highMatch = array('', $matches[10], $matches[11], $matches[12], $matches[13]);
$highVersion = $this->manipulateVersionString($highMatch, $empty($matches[11]) ? 1 : 2, 1) . '-dev';
$highMatch = array('', $matches[11], $matches[12], $matches[13], $matches[14]);
// validate to version
$this->normalize($matches['to']);
$highVersion = $this->manipulateVersionString($highMatch, $empty($matches[12]) ? 1 : 2, 1) . '-dev';
$upperBound = new Constraint('<', $highVersion);
}
@@ -457,11 +481,23 @@ class VersionParser
// Basic Comparators
if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
try {
$version = $this->normalize($matches[2]);
try {
$version = $this->normalize($matches[2]);
} catch (\UnexpectedValueException $e) {
// recover from an invalid constraint like foobar-dev which should be dev-foobar
// except if the constraint uses a known operator, in which case it must be a parse error
if (substr($matches[2], -4) === '-dev' && preg_match('{^[0-9a-zA-Z-./]+$}', $matches[2])) {
$version = $this->normalize('dev-'.substr($matches[2], 0, -4));
} else {
throw $e;
}
}
if (!empty($stabilityModifier) && self::parseStability($version) === 'stable') {
$op = $matches[1] ?: '=';
if ($op !== '==' && $op !== '=' && !empty($stabilityModifier) && self::parseStability($version) === 'stable') {
$version .= '-' . $stabilityModifier;
} elseif ('<' === $matches[1] || '>=' === $matches[1]) {
} elseif ('<' === $op || '>=' === $op) {
if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) {
if (strpos($matches[2], 'dev-') !== 0) {
$version .= '-dev';
@@ -487,14 +523,16 @@ class VersionParser
*
* Support function for {@link parseConstraint()}
*
* @param array $matches Array with version parts in array indexes 1,2,3,4
* @param int $position 1,2,3,4 - which segment of the version to increment/decrement
* @param int $increment
* @param string $pad The string to pad version parts after $position
* @param array $matches Array with version parts in array indexes 1,2,3,4
* @param int $position 1,2,3,4 - which segment of the version to increment/decrement
* @param int $increment
* @param string $pad The string to pad version parts after $position
*
* @return string The new version
* @return string|null The new version
*
* @phpstan-param string[] $matches
*/
private function manipulateVersionString($matches, $position, $increment = 0, $pad = '0')
private function manipulateVersionString(array $matches, $position, $increment = 0, $pad = '0')
{
for ($i = 4; $i > 0; --$i) {
if ($i > $position) {