Skip to content

Commit

Permalink
Proper getIntervals implementation, move isSubsetOf out of constraint…
Browse files Browse the repository at this point in the history
…s into Intervals class
  • Loading branch information
Seldaek committed May 8, 2020
1 parent 639fc07 commit b894127
Show file tree
Hide file tree
Showing 7 changed files with 584 additions and 297 deletions.
122 changes: 0 additions & 122 deletions src/Constraint/Constraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,128 +121,6 @@ public function matches(ConstraintInterface $provider)
return $provider->matches($this);
}

/**
* {@inheritDoc}
*/
public function isSubsetOf(ConstraintInterface $constraint)
{
if ($constraint instanceof EmptyConstraint) {
return true;
}

if ($constraint instanceof MultiConstraint) {
// != X is a subset of Y if Y does not intersect with X, and no dev versions are involved
if ($this->operator === self::OP_NE) {
return false === strpos((string) $constraint, ' dev-') && !$constraint->matches(new Constraint('==', $this->version));
}

if ($constraint->isConjunctive()) {
foreach ($constraint->getConstraints() as $c) {
if (!$this->isSubsetOf($c)) {
return false;
}
}

return true;
}

foreach ($constraint->getConstraints() as $c) {
if ($this->isSubsetOf($c)) {
return true;
}
}

return false;
}

// constraints are subsets of themselves
if ((string) $constraint === (string) $this) {
return true;
}

// exact non-match constraint can only be a subset of themselves at this point so if they were not we abort
if ($this->operator === self::OP_NE || $constraint->operator === self::OP_NE) {
return false;
}

// exact match constraint are a subset if they intersect
if ($this->operator === self::OP_EQ) {
return $this->matches($constraint);
}

// exact match constraint which are not equal to $this can not be a subset
if ($constraint->operator === self::OP_EQ) {
return false;
}

if ($this->operator === $constraint->operator) {
// compare e.g. < 3 && < 4.5 to see if this side is smaller than the other (and thus a subset)
if ($this->operator === self::OP_LE || $this->operator === self::OP_LT) {
return $this->versionCompare($this->version, $constraint->version, '<=');
}
// compare e.g. > 3 && > 4.5 to see if this side is bigger than the other (and thus a subset)
if ($this->operator === self::OP_GE || $this->operator === self::OP_GT) {
return $this->versionCompare($this->version, $constraint->version, '>=');
}

throw new \LogicException('Should not be reached');
}

// compare e.g. < 3 && <= 4.5 to see if this side is smaller or eq than the other (and thus a subset)
if ($this->operator === self::OP_LT && $constraint->operator === self::OP_LE) {
return $this->versionCompare($this->version, $constraint->version, '<=');
}
// e.g. < 3 && > 4.5, this cannot be a subset
if ($this->operator === self::OP_LT && $constraint->operator === self::OP_GT) {
return false;
}
// e.g. < 3 && >= 4.5, this can only be a subset of >=0
if ($this->operator === self::OP_LT && $constraint->operator === self::OP_GE) {
return $this->versionCompare('0.0.0.0', $constraint->version, '==');
}

// compare e.g. <= 3 && < 4.5 to see if this side is smaller than the other (and thus a subset)
if ($this->operator === self::OP_LE && $constraint->operator === self::OP_LT) {
return $this->versionCompare($this->version, $constraint->version, '<');
}
// e.g. <= 3 && > 4.5, this cannot be a subset
if ($this->operator === self::OP_LE && $constraint->operator === self::OP_GT) {
return false;
}
// e.g. <= 3 && >= 4.5, this can only be a subset of >=0
if ($this->operator === self::OP_LE && $constraint->operator === self::OP_GE) {
return $this->versionCompare('0.0.0.0', $constraint->version, '==');
}

// compare e.g. > 3 && >= 4.5 to see if this side is bigger or eq than the other (and thus a subset)
if ($this->operator === self::OP_GT && $constraint->operator === self::OP_GE) {
return $this->versionCompare($this->version, $constraint->version, '>=');
}
// e.g. > 3 && < 4.5, this cannot be a subset
if ($this->operator === self::OP_GT && $constraint->operator === self::OP_LT) {
return false;
}
// e.g. > 3 && <= 4.5, this can only be a subset of <=INF which we do not really support so assuming false
if ($this->operator === self::OP_GT && $constraint->operator === self::OP_LE) {
return false;
}

// compare e.g. >= 3 && > 4.5 to see if this side is bigger than the other (and thus a subset)
if ($this->operator === self::OP_GE && $constraint->operator === self::OP_GT) {
return $this->versionCompare($this->version, $constraint->version, '>');
}
// e.g. >= 3 && < 4.5, this cannot be a subset
if ($this->operator === self::OP_GE && $constraint->operator === self::OP_LT) {
return false;
}
// e.g. >= 3 && <= 4.5, this can only be a subset of <=INF which we do not really support so assuming false
if ($this->operator === self::OP_GE && $constraint->operator === self::OP_LE) {
return false;
}

throw new \LogicException('Should not be reached');
}

/**
* @param string|null $prettyString
*/
Expand Down
9 changes: 0 additions & 9 deletions src/Constraint/ConstraintInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,6 @@ interface ConstraintInterface
*/
public function matches(ConstraintInterface $provider);

/**
* Checks whether the given constraint is wholly contained within this constraint
*
* @param ConstraintInterface $constraint
*
* @return bool
*/
public function isSubsetOf(ConstraintInterface $constraint);

/**
* @return Bound
*/
Expand Down
12 changes: 0 additions & 12 deletions src/Constraint/EmptyConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,6 @@ public function matches(ConstraintInterface $provider)
return true;
}

/**
* Technically MultiConstraint can be equivalent to EmptyConstraints,
* but it is hard to detect so we only consider this to be a subset of
* itself
*
* {@inheritDoc}
*/
public function isSubsetOf(ConstraintInterface $constraint)
{
return $constraint instanceof EmptyConstraint;
}

public function compile($operator)
{
return 'true';
Expand Down
147 changes: 0 additions & 147 deletions src/Constraint/MultiConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,153 +132,6 @@ public function matches(ConstraintInterface $provider)
return true;
}

/**
* {@inheritDoc}
*/
public function isSubsetOf(ConstraintInterface $constraint)
{
if ($constraint instanceof EmptyConstraint) {
return true;
}

if ($constraint instanceof MultiConstraint) {
// $this is a subset of x || y if it is a subset any of x or y
if ($constraint->isDisjunctive()) {
foreach ($constraint->getConstraints() as $set) {
if ($this->isSubsetOf($set)) {
return true;
}
}
}

if ($this->isConjunctive()) {
// TODO could be more strict for conjunctive constraints
}

// special case for contiguous constraints (>/>= x && < y) compared with a similar one
if ($this->isContiguousFiniteRange() && $constraint->isContiguousFiniteRange()) {
$sets = $constraint->getConstraints();
foreach ($this->getConstraints() as $index => $c) {
if (!$c->isSubsetOf($sets[$index])) {
return false;
}
}

return true;
}

foreach ($this->getConstraints() as $index => $c) {
if (!$c->isSubsetOf($constraint)) {
return false;
}
}

return true;
}

if ($this->isConjunctive()) {
// TODO could be more strict for conjunctive constraints
}

// =/!= cannot lead to optimization via intervals
$op = $constraint->getOperator();
if (!in_array($op, array('==', '!='), true)) {
$intervals = $this->getIntervals();
if ($intervals && $intervals['intervals'] && !$intervals['devConstraints']) {
$matched = true;
foreach ($intervals['intervals'] as $interval) {
// if the interval start range is not a subset of >/>=x, the interval is no subset
if (isset($interval['start']) && in_array($op, array('>', '>='), true) && !$interval['start']->isSubsetOf($constraint)) {
$matched = false;
}
// if the interval start range is not a subset of </<=x, the interval is no subset
if (isset($interval['end']) && in_array($op, array('<', '<='), true) && !$interval['end']->isSubsetOf($constraint)) {
$matched = false;
}
// a disjunctive constraint with !=x means the matched range could be almost anything so we just abort
// if it is conjunctive though and the interval is a subset, exceptions do not matter as exceptions which are outside the intervals
// are not part of the set of valid versions anyway
if (isset($interval['exceptions']) && $this->isDisjunctive()) {
$matched = false;
break;
}
}
if ($matched) {
return true;
}
}
}

foreach ($this->getConstraints() as $c) {
if (!$c->isSubsetOf($constraint)) {
return false;
}
}

return true;
}

public function isContiguousFiniteRange()
{
if (\count($this->constraints) !== 2 || !$this->conjunctive) {
return false;
}

$constraint1 = (string) $this->constraints[0];
$constraint2 = (string) $this->constraints[1];

return (($constraint1[0] === '>' && $constraint1[1] === '=') || ($constraint1[0] === '>' && $constraint1[1] === ' '))
&& (($constraint2[0] === '<' && $constraint2[1] === '=') || ($constraint2[0] === '<' && $constraint2[1] === ' '));
}

public function getIntervals()
{
$constraints = $this->constraints;
usort($constraints, function ($a, $b) {
if ($a instanceof Constraint && $b instanceof Constraint) {
return version_compare($a->getVersion(), $b->getVersion());
}

return -1;
});

$intervals = array();
$dev = array();
$last = array();
foreach ($constraints as $constraint) {
if ($constraint instanceof MultiConstraint || $constraint instanceof EmptyConstraint) {
return null;
}

$op = $constraint->getOperator();
if (in_array($op, array('>', '>='), true)) {
if ($last) {
$intervals[] = $last;
}
$last = array('start' => $constraint);
} elseif (in_array($op, array('<', '<='), true)) {
$last['end'] = $constraint;
} elseif ($op === '!=') {
if (substr($constraint->getVersion(), 0, 4) === 'dev-') {
$dev[] = $constraint;
} else {
$last['exceptions'][] = $constraint;
}
} else {
$intervals[] = array('start' => $constraint, 'end' => $constraint);
}
}
if ($last) {
$intervals[] = $last;
}

// TODO add tests for this, need to make sure exceptions end up in the correct interval and that sorting above works
// TODO could potentially merge intervals together if constraint is conjunctive to reduce the ranges we need to check
// TODO investigate support for nested MultiConstraint

return array('intervals' => $intervals, 'devConstraints' => $dev);
}

/**
* @param string|null $prettyString
*/
Expand Down
Loading

0 comments on commit b894127

Please sign in to comment.