diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index bcce362a4..c1e197565 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -123,7 +123,8 @@ class AssertionReconciler extends \Psalm\Type\Reconciler $negated, $code_location, $suppressed_issues, - $failed_reconciliation + $failed_reconciliation, + $inside_loop ); } diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index 211979f47..87fd34e67 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -48,7 +48,8 @@ class NegatedAssertionReconciler extends Reconciler bool $negated, ?CodeLocation $code_location, array $suppressed_issues, - int &$failed_reconciliation + int &$failed_reconciliation, + bool $inside_loop ): Type\Union { $is_equality = $is_strict_equality || $is_loose_equality; @@ -211,7 +212,8 @@ class NegatedAssertionReconciler extends Reconciler $suppressed_issues, $failed_reconciliation, $is_equality, - $is_strict_equality + $is_strict_equality, + $inside_loop ); if ($simple_negated_type) { diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 74e753012..ff9fbb685 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -1589,6 +1589,7 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler string $assertion, bool $inside_loop ) : Union { + $assertion_value = (int)$assertion; foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { if ($inside_loop) { continue; @@ -1597,16 +1598,16 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler if ($atomic_type instanceof Atomic\TIntRange) { $existing_var_type->removeType($atomic_type->getKey()); if ($atomic_type->min_bound === null) { - $atomic_type->min_bound = (int)$assertion; + $atomic_type->min_bound = $assertion_value; } else { $atomic_type->min_bound = Atomic\TIntRange::getNewHighestBound( - (int)$assertion, + $assertion_value, $atomic_type->min_bound ); } $existing_var_type->addType($atomic_type); } elseif ($atomic_type instanceof Atomic\TLiteralInt) { - $new_range = new Atomic\TIntRange((int)$assertion, null); + $new_range = new Atomic\TIntRange($assertion_value, null); if (!$new_range->contains($atomic_type->value)) { //emit an issue here in the future about incompatible type $existing_var_type->removeType($atomic_type->getKey()); @@ -1614,21 +1615,21 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler } /*elseif ($inside_loop) { //when inside a loop, allow the range to extends the type $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->value < (int)$assertion) { - $existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, (int)$assertion)); + if ($atomic_type->value < $assertion_value) { + $existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, $assertion_value)); } else { - $existing_var_type->addType(new Atomic\TIntRange((int)$assertion, $atomic_type->value)); + $existing_var_type->addType(new Atomic\TIntRange($assertion_value, $atomic_type->value)); } }*/ } elseif ($atomic_type instanceof Atomic\TPositiveInt) { - if ((int)$assertion <= 0) { + if ($assertion_value <= 0) { //emit an issue here in the future about incompatible type } $existing_var_type->removeType($atomic_type->getKey()); - $existing_var_type->addType(new Atomic\TIntRange((int)$assertion, null)); + $existing_var_type->addType(new Atomic\TIntRange($assertion_value, null)); } elseif ($atomic_type instanceof TInt) { $existing_var_type->removeType($atomic_type->getKey()); - $existing_var_type->addType(new Atomic\TIntRange((int)$assertion, null)); + $existing_var_type->addType(new Atomic\TIntRange($assertion_value, null)); } } @@ -1640,6 +1641,7 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler string $assertion, bool $inside_loop ) : Union { + $assertion_value = (int)$assertion; foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { if ($inside_loop) { continue; @@ -1648,13 +1650,13 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler if ($atomic_type instanceof Atomic\TIntRange) { $existing_var_type->removeType($atomic_type->getKey()); if ($atomic_type->max_bound === null) { - $atomic_type->max_bound = (int)$assertion; + $atomic_type->max_bound = $assertion_value; } else { - $atomic_type->max_bound = min($atomic_type->max_bound, (int)$assertion); + $atomic_type->max_bound = min($atomic_type->max_bound, $assertion_value); } $existing_var_type->addType($atomic_type); } elseif ($atomic_type instanceof Atomic\TLiteralInt) { - $new_range = new Atomic\TIntRange(null, (int)$assertion); + $new_range = new Atomic\TIntRange(null, $assertion_value); if (!$new_range->contains($atomic_type->value)) { //emit an issue here in the future about incompatible type $existing_var_type->removeType($atomic_type->getKey()); @@ -1662,21 +1664,21 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler }/* elseif ($inside_loop) { //when inside a loop, allow the range to extends the type $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->value < (int)$assertion) { - $existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, (int)$assertion)); + if ($atomic_type->value < $assertion_value) { + $existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, $assertion_value)); } else { - $existing_var_type->addType(new Atomic\TIntRange((int)$assertion, $atomic_type->value)); + $existing_var_type->addType(new Atomic\TIntRange($assertion_value, $atomic_type->value)); } }*/ } elseif ($atomic_type instanceof Atomic\TPositiveInt) { - if ((int)$assertion <= 0) { + if ($assertion_value <= 0) { //emit an issue here in the future about incompatible type } $existing_var_type->removeType($atomic_type->getKey()); - $existing_var_type->addType(new Atomic\TIntRange(1, (int)$assertion)); + $existing_var_type->addType(new Atomic\TIntRange(1, $assertion_value)); } elseif ($atomic_type instanceof TInt) { $existing_var_type->removeType($atomic_type->getKey()); - $existing_var_type->addType(new Atomic\TIntRange(null, (int)$assertion)); + $existing_var_type->addType(new Atomic\TIntRange(null, $assertion_value)); } } diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 159340fd4..80d5cc112 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -26,7 +26,9 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Reconciler; +use Psalm\Type\Union; use function get_class; +use function max; use function substr; class SimpleNegatedAssertionReconciler extends Reconciler @@ -46,7 +48,8 @@ class SimpleNegatedAssertionReconciler extends Reconciler array $suppressed_issues = [], int &$failed_reconciliation = 0, bool $is_equality = false, - bool $is_strict_equality = false + bool $is_strict_equality = false, + bool $inside_loop = false ) : ?Type\Union { if ($assertion === 'object' && !$existing_var_type->hasMixed()) { return self::reconcileObject( @@ -231,6 +234,22 @@ class SimpleNegatedAssertionReconciler extends Reconciler return $existing_var_type; } + if ($assertion[0] === '>') { + return self::reconcileSuperiorTo( + $existing_var_type, + substr($assertion, 1), + $inside_loop + ); + } + + if ($assertion[0] === '<') { + return self::reconcileInferiorTo( + $existing_var_type, + substr($assertion, 1), + $inside_loop + ); + } + return null; } @@ -1651,4 +1670,99 @@ class SimpleNegatedAssertionReconciler extends Reconciler } } } + + private static function reconcileSuperiorTo(Union $existing_var_type, string $assertion, bool $inside_loop): Union + { + $assertion_value = (int)$assertion - 1; + foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { + if ($inside_loop) { + continue; + } + + if ($atomic_type instanceof Atomic\TIntRange) { + $existing_var_type->removeType($atomic_type->getKey()); + if ($atomic_type->max_bound === null) { + $atomic_type->max_bound = $assertion_value; + } else { + $atomic_type->max_bound = Atomic\TIntRange::getNewLowestBound( + $assertion_value, + $atomic_type->max_bound + ); + } + $existing_var_type->addType($atomic_type); + } elseif ($atomic_type instanceof Atomic\TLiteralInt) { + $new_range = new Atomic\TIntRange(null, $assertion_value); + if (!$new_range->contains($atomic_type->value)) { + //emit an issue here in the future about incompatible type + $existing_var_type->removeType($atomic_type->getKey()); + $existing_var_type->addType($new_range); + } /*elseif ($inside_loop) { + //when inside a loop, allow the range to extends the type + $existing_var_type->removeType($atomic_type->getKey()); + if ($atomic_type->value < $assertion_value) { + $existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, $assertion_value)); + } else { + $existing_var_type->addType(new Atomic\TIntRange($assertion_value, $atomic_type->value)); + } + }*/ + } elseif ($atomic_type instanceof Atomic\TPositiveInt) { + if ($assertion_value > 0) { + //emit an issue here in the future about incompatible type + } + $existing_var_type->removeType($atomic_type->getKey()); + $existing_var_type->addType(new Atomic\TIntRange(null, $assertion_value)); + } elseif ($atomic_type instanceof TInt) { + $existing_var_type->removeType($atomic_type->getKey()); + $existing_var_type->addType(new Atomic\TIntRange(null, $assertion_value)); + } + } + + return $existing_var_type; + } + + private static function reconcileInferiorTo(Union $existing_var_type, string $assertion, bool $inside_loop): Union + { + $assertion_value = (int)$assertion + 1; + foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { + if ($inside_loop) { + continue; + } + + if ($atomic_type instanceof Atomic\TIntRange) { + $existing_var_type->removeType($atomic_type->getKey()); + if ($atomic_type->min_bound === null) { + $atomic_type->min_bound = $assertion_value; + } else { + $atomic_type->min_bound = max($atomic_type->min_bound, $assertion_value); + } + $existing_var_type->addType($atomic_type); + } elseif ($atomic_type instanceof Atomic\TLiteralInt) { + $new_range = new Atomic\TIntRange($assertion_value, null); + if (!$new_range->contains($atomic_type->value)) { + //emit an issue here in the future about incompatible type + $existing_var_type->removeType($atomic_type->getKey()); + $existing_var_type->addType($new_range); + }/* elseif ($inside_loop) { + //when inside a loop, allow the range to extends the type + $existing_var_type->removeType($atomic_type->getKey()); + if ($atomic_type->value < $assertion_value) { + $existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, $assertion_value)); + } else { + $existing_var_type->addType(new Atomic\TIntRange($assertion_value, $atomic_type->value)); + } + }*/ + } elseif ($atomic_type instanceof Atomic\TPositiveInt) { + if ($assertion_value > 0) { + //emit an issue here in the future about incompatible type + } + $existing_var_type->removeType($atomic_type->getKey()); + $existing_var_type->addType(new Atomic\TIntRange($assertion_value, 1)); + } elseif ($atomic_type instanceof TInt) { + $existing_var_type->removeType($atomic_type->getKey()); + $existing_var_type->addType(new Atomic\TIntRange($assertion_value, null)); + } + } + + return $existing_var_type; + } } diff --git a/tests/IntRangeTest.php b/tests/IntRangeTest.php index 641c16b3c..2f30d3dbc 100644 --- a/tests/IntRangeTest.php +++ b/tests/IntRangeTest.php @@ -58,6 +58,102 @@ class IntRangeTest extends TestCase '$c===' => 'int<-499, -61>', ] ], + 'negatedAssertions' => [ + ' + if($a > 10){ + die(); + } + + if($b > -10){ + die(); + } + + //< + if($c < 500){ + die(); + } + + if($d < -500){ + die(); + } + + //>= + if($e >= 10){ + die(); + } + + if($f >= -10){ + die(); + } + + //<= + if($g <= 500){ + die(); + } + + if($h <= -500){ + die(); + } + + //> + if(10 > $i){ + die(); + } + + if(-10 > $j){ + die(); + } + + //< + if(500 < $k){ + die(); + } + + if(-500 < $l){ + die(); + } + + //>= + if(10 >= $m){ + die(); + } + + if(-10 >= $n){ + die(); + } + + //<= + if(500 <= $o){ + die(); + } + + if(-500 <= $p){ + die(); + } + //inverse + ', + 'assertions' => [ + '$a===' => 'int', + '$b===' => 'int', + '$c===' => 'int<500, max>', + '$d===' => 'int<-500, max>', + '$e===' => 'int', + '$f===' => 'int', + '$g===' => 'int<501, max>', + '$h===' => 'int<-499, max>', + '$i===' => 'int<10, max>', + '$j===' => 'int<-10, max>', + '$k===' => 'int', + '$l===' => 'int', + '$m===' => 'int<11, max>', + '$n===' => 'int<-9, max>', + '$o===' => 'int', + '$p===' => 'int', + ] + ], 'intOperations' => [ '