diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 3c002791e68..03584a658c2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -51,9 +51,11 @@ use Psalm\Storage\Assertion\IsCountable; use Psalm\Storage\Assertion\IsEqualIsset; use Psalm\Storage\Assertion\IsGreaterThan; +use Psalm\Storage\Assertion\IsGreaterThanOrEqualTo; use Psalm\Storage\Assertion\IsIdentical; use Psalm\Storage\Assertion\IsIsset; use Psalm\Storage\Assertion\IsLessThan; +use Psalm\Storage\Assertion\IsLessThanOrEqualTo; use Psalm\Storage\Assertion\IsLooselyEqual; use Psalm\Storage\Assertion\IsNotIdentical; use Psalm\Storage\Assertion\IsNotLooselyEqual; @@ -1644,8 +1646,7 @@ protected static function hasCountEqualityCheck( protected static function hasSuperiorNumberCheck( FileSource $source, PhpParser\Node\Expr\BinaryOp $conditional, - ?int &$literal_value_comparison, - bool &$isset_assert + ?int &$literal_value_comparison ) { $right_assignment = false; $value_right = null; @@ -1666,10 +1667,7 @@ protected static function hasSuperiorNumberCheck( $value_right = $conditional->right->expr->value; } if ($right_assignment === true && $value_right !== null) { - $isset_assert = $value_right === 0 && $conditional instanceof Greater; - - $literal_value_comparison = $value_right + - ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 1 : 0); + $literal_value_comparison = $value_right; return self::ASSIGNMENT_TO_RIGHT; } @@ -1693,10 +1691,7 @@ protected static function hasSuperiorNumberCheck( $value_left = $conditional->left->expr->value; } if ($left_assignment === true && $value_left !== null) { - $isset_assert = $value_left === 0 && $conditional instanceof Greater; - - $literal_value_comparison = $value_left + - ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? -1 : 0); + $literal_value_comparison = $value_left; return self::ASSIGNMENT_TO_LEFT; } @@ -1711,8 +1706,7 @@ protected static function hasSuperiorNumberCheck( protected static function hasInferiorNumberCheck( FileSource $source, PhpParser\Node\Expr\BinaryOp $conditional, - ?int &$literal_value_comparison, - bool &$isset_assert + ?int &$literal_value_comparison ) { $right_assignment = false; $value_right = null; @@ -1733,10 +1727,8 @@ protected static function hasInferiorNumberCheck( $value_right = $conditional->right->expr->value; } if ($right_assignment === true && $value_right !== null) { - $isset_assert = $value_right === 0 && $conditional instanceof Smaller; + $literal_value_comparison = $value_right; - $literal_value_comparison = $value_right + - ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? -1 : 0); return self::ASSIGNMENT_TO_RIGHT; } @@ -1759,10 +1751,7 @@ protected static function hasInferiorNumberCheck( $value_left = $conditional->left->expr->value; } if ($left_assignment === true && $value_left !== null) { - $isset_assert = $value_left === 0 && $conditional instanceof Smaller; - - $literal_value_comparison = $value_left + - ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 1 : 0); + $literal_value_comparison = $value_left; return self::ASSIGNMENT_TO_LEFT; } @@ -3788,13 +3777,11 @@ private static function getGreaterAssertions( $count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional, $min_count); $max_count = null; $count_inequality_position = self::hasLessThanCountEqualityCheck($conditional, $max_count); - $isset_assert = false; $superior_value_comparison = null; $superior_value_position = self::hasSuperiorNumberCheck( $source, $conditional, - $superior_value_comparison, - $isset_assert + $superior_value_comparison ); if ($count_equality_position) { @@ -3851,7 +3838,7 @@ private static function getGreaterAssertions( return $if_types ? [$if_types] : []; } - if ($superior_value_position) { + if ($superior_value_position && $superior_value_comparison !== null) { if ($superior_value_position === self::ASSIGNMENT_TO_RIGHT) { $var_name = ExpressionIdentifier::getArrayVarId( $conditional->left, @@ -3868,13 +3855,17 @@ private static function getGreaterAssertions( if ($var_name !== null) { if ($superior_value_position === self::ASSIGNMENT_TO_RIGHT) { - $if_types[$var_name] = [[new IsGreaterThan($superior_value_comparison)]]; + if ($conditional instanceof GreaterOrEqual) { + $if_types[$var_name] = [[new IsGreaterThanOrEqualTo($superior_value_comparison)]]; + } else { + $if_types[$var_name] = [[new IsGreaterThan($superior_value_comparison)]]; + } } else { - $if_types[$var_name] = [[new IsLessThan($superior_value_comparison)]]; - } - - if ($isset_assert) { - $if_types[$var_name][] = [new IsEqualIsset()]; + if ($conditional instanceof GreaterOrEqual) { + $if_types[$var_name] = [[new IsLessThanOrEqualTo($superior_value_comparison)]]; + } else { + $if_types[$var_name] = [[new IsLessThan($superior_value_comparison)]]; + } } } @@ -3898,13 +3889,11 @@ private static function getSmallerAssertions( $count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional, $min_count); $max_count = null; $count_inequality_position = self::hasLessThanCountEqualityCheck($conditional, $max_count); - $isset_assert = false; $inferior_value_comparison = null; $inferior_value_position = self::hasInferiorNumberCheck( $source, $conditional, - $inferior_value_comparison, - $isset_assert + $inferior_value_comparison ); if ($count_equality_position) { @@ -3973,15 +3962,19 @@ private static function getSmallerAssertions( } - if ($var_name !== null) { + if ($var_name !== null && $inferior_value_comparison !== null) { if ($inferior_value_position === self::ASSIGNMENT_TO_RIGHT) { - $if_types[$var_name] = [[new IsLessThan($inferior_value_comparison)]]; + if ($conditional instanceof SmallerOrEqual) { + $if_types[$var_name] = [[new IsLessThanOrEqualTo($inferior_value_comparison)]]; + } else { + $if_types[$var_name] = [[new IsLessThan($inferior_value_comparison)]]; + } } else { - $if_types[$var_name] = [[new IsGreaterThan($inferior_value_comparison)]]; - } - - if ($isset_assert) { - $if_types[$var_name][] = [new IsEqualIsset()]; + if ($conditional instanceof SmallerOrEqual) { + $if_types[$var_name] = [[new IsGreaterThanOrEqualTo($inferior_value_comparison)]]; + } else { + $if_types[$var_name] = [[new IsGreaterThan($inferior_value_comparison)]]; + } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index 73b2cddaa90..27e262be596 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -547,7 +547,6 @@ public static function handleByRefArrayAdjustment( ] ); } else { - /** @psalm-suppress InvalidPropertyAssignmentValue */ $array_atomic_type->count--; } } else { @@ -565,7 +564,6 @@ public static function handleByRefArrayAdjustment( ] ); } else { - /** @psalm-suppress InvalidPropertyAssignmentValue */ $array_atomic_type->count--; } } else { diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index 647396f9900..2a112e94e8d 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -328,6 +328,44 @@ private static function handleLiteralNegatedEquality( $did_remove_type = true; } } + + $existing_range_types = $existing_var_type->getRangeInts(); + + if ($existing_range_types) { + foreach ($existing_range_types as $int_key => $literal_type) { + if ($literal_type->contains($assertion_type->value)) { + $did_remove_type = true; + $existing_var_type->removeType($int_key); + if ($literal_type->min_bound === null + || $literal_type->min_bound <= $assertion_type->value - 1 + ) { + $existing_var_type->addType(new Type\Atomic\TIntRange( + $literal_type->min_bound, + $assertion_type->value - 1 + )); + } + if ($literal_type->max_bound === null + || $literal_type->max_bound >= $assertion_type->value + 1 + ) { + $existing_var_type->addType(new Type\Atomic\TIntRange( + $assertion_type->value + 1, + $literal_type->max_bound + )); + } + } + } + } + + if (isset($existing_var_type->getAtomicTypes()['int']) + && get_class($existing_var_type->getAtomicTypes()['int']) === Type\Atomic\TInt::class + ) { + $did_remove_type = true; + //this may be used to generate a range containing any int except the one that was asserted against + //but this is failing some tests + /*$existing_var_type->removeType('int'); + $existing_var_type->addType(new Type\Atomic\TIntRange(null, $assertion_type->value - 1)); + $existing_var_type->addType(new Type\Atomic\TIntRange($assertion_type->value + 1, null));*/ + } } else { $scalar_var_type = clone $assertion_type; } diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 7ce33e72eaf..f35470d63cb 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -153,7 +153,7 @@ public static function reconcile( } if ($assertion instanceof IsGreaterThan) { - return self::reconcileSuperiorTo( + return self::reconcileIsGreaterThan( $assertion, $existing_var_type, $inside_loop, @@ -166,7 +166,7 @@ public static function reconcile( } if ($assertion instanceof IsLessThan) { - return self::reconcileInferiorTo( + return self::reconcileIsLessThan( $assertion, $existing_var_type, $inside_loop, @@ -1612,7 +1612,7 @@ private static function reconcileHasArrayKey( /** * @param string[] $suppressed_issues */ - private static function reconcileSuperiorTo( + private static function reconcileIsGreaterThan( IsGreaterThan $assertion, Union $existing_var_type, bool $inside_loop, @@ -1622,19 +1622,21 @@ private static function reconcileSuperiorTo( ?CodeLocation $code_location, array $suppressed_issues ): Union { - $assertion_value = $assertion->value; + //we add 1 from the assertion value because we're on a strict operator + $assertion_value = $assertion->value + 1; $did_remove_type = false; + if ($existing_var_type->hasType('null') && $assertion->doesFilterNull()) { + $did_remove_type = true; + $existing_var_type->removeType('null'); + } + foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { if ($inside_loop) { continue; } - if ($assertion_value === null) { - continue; - } - if ($atomic_type instanceof TIntRange) { if ($atomic_type->contains($assertion_value)) { // if the range contains the assertion, the range must be adapted @@ -1715,7 +1717,7 @@ private static function reconcileSuperiorTo( /** * @param string[] $suppressed_issues */ - private static function reconcileInferiorTo( + private static function reconcileIsLessThan( IsLessThan $assertion, Union $existing_var_type, bool $inside_loop, @@ -1725,19 +1727,21 @@ private static function reconcileInferiorTo( ?CodeLocation $code_location, array $suppressed_issues ): Union { - $assertion_value = $assertion->value; + //we remove 1 from the assertion value because we're on a strict operator + $assertion_value = $assertion->value - 1; $did_remove_type = false; + if ($existing_var_type->hasType('null') && $assertion->doesFilterNull()) { + $did_remove_type = true; + $existing_var_type->removeType('null'); + } + foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { if ($inside_loop) { continue; } - if ($assertion_value === null) { - continue; - } - if ($atomic_type instanceof TIntRange) { if ($atomic_type->contains($assertion_value)) { // if the range contains the assertion, the range must be adapted diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index fc8d6b62c98..148cec13af5 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -1653,22 +1653,23 @@ private static function reconcileResource( */ private static function reconcileIsLessThanOrEqualTo( IsLessThanOrEqualTo $assertion, - Union $existing_var_type, - bool $inside_loop, - string $old_var_type_string, - ?string $var_id, - bool $negated, - ?CodeLocation $code_location, - array $suppressed_issues + Union $existing_var_type, + bool $inside_loop, + string $old_var_type_string, + ?string $var_id, + bool $negated, + ?CodeLocation $code_location, + array $suppressed_issues ): Union { - if ($assertion->value === null) { - return $existing_var_type; - } - - $assertion_value = $assertion->value - 1; + $assertion_value = $assertion->value; $did_remove_type = false; + if ($existing_var_type->hasType('null') && $assertion->doesFilterNull()) { + $did_remove_type = true; + $existing_var_type->removeType('null'); + } + foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { if ($inside_loop) { continue; @@ -1756,22 +1757,23 @@ private static function reconcileIsLessThanOrEqualTo( */ private static function reconcileIsGreaterThanOrEqualTo( IsGreaterThanOrEqualTo $assertion, - Union $existing_var_type, - bool $inside_loop, - string $old_var_type_string, - ?string $var_id, - bool $negated, - ?CodeLocation $code_location, - array $suppressed_issues + Union $existing_var_type, + bool $inside_loop, + string $old_var_type_string, + ?string $var_id, + bool $negated, + ?CodeLocation $code_location, + array $suppressed_issues ): Union { - if ($assertion->value === null) { - return $existing_var_type; - } - - $assertion_value = $assertion->value + 1; + $assertion_value = $assertion->value; $did_remove_type = false; + if ($existing_var_type->hasType('null') && $assertion->doesFilterNull()) { + $did_remove_type = true; + $existing_var_type->removeType('null'); + } + foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { if ($inside_loop) { continue; diff --git a/src/Psalm/Storage/Assertion/IsGreaterThan.php b/src/Psalm/Storage/Assertion/IsGreaterThan.php index 13d4c088f6c..96ba74aa396 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThan.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThan.php @@ -6,9 +6,9 @@ class IsGreaterThan extends Assertion { - public ?int $value; + public int $value; - public function __construct(?int $value) + public function __construct(int $value) { $this->value = $value; } @@ -29,4 +29,9 @@ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLessThanOrEqualTo && $this->value === $assertion->value; } + + public function doesFilterNull(): bool + { + return true; + } } diff --git a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php index b0248d1f9a9..5cf032ae063 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php @@ -6,9 +6,9 @@ class IsGreaterThanOrEqualTo extends Assertion { - public ?int $value; + public int $value; - public function __construct(?int $value) + public function __construct(int $value) { $this->value = $value; } @@ -34,4 +34,9 @@ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLessThan && $this->value === $assertion->value; } + + public function doesFilterNull(): bool + { + return $this->value !== 0; + } } diff --git a/src/Psalm/Storage/Assertion/IsLessThan.php b/src/Psalm/Storage/Assertion/IsLessThan.php index 24b3f628823..ab797dac228 100644 --- a/src/Psalm/Storage/Assertion/IsLessThan.php +++ b/src/Psalm/Storage/Assertion/IsLessThan.php @@ -6,9 +6,9 @@ class IsLessThan extends Assertion { - public ?int $value; + public int $value; - public function __construct(?int $value) + public function __construct(int $value) { $this->value = $value; } @@ -29,4 +29,9 @@ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsGreaterThanOrEqualTo && $this->value === $assertion->value; } + + public function doesFilterNull(): bool + { + return $this->value === 0; + } } diff --git a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php index c56a40e90ac..82dc9d65732 100644 --- a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php @@ -6,9 +6,9 @@ class IsLessThanOrEqualTo extends Assertion { - public ?int $value; + public int $value; - public function __construct(?int $value) + public function __construct(int $value) { $this->value = $value; } @@ -34,4 +34,9 @@ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsGreaterThan && $this->value === $assertion->value; } + + public function doesFilterNull(): bool + { + return false; + } } diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 2f2e37f197d..3e94d3bd853 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -847,6 +847,21 @@ function example(object $foo): string return ($foo instanceof FooInterface ? $foo->toString() : null) ?? "Not a stringable foo"; }', ], + 'handleLiteralInequalityWithInts' => [ + 'code' => ' $i + * @return int<1, max> + */ + function toPositiveInt(int $i): int + { + if ($i !== 0) { + return $i; + } + return 1; + }', + ], ]; } diff --git a/tests/IntRangeTest.php b/tests/IntRangeTest.php index 9cb2e8fa263..491d81d98e0 100644 --- a/tests/IntRangeTest.php +++ b/tests/IntRangeTest.php @@ -716,6 +716,90 @@ class DocComment 'assertions' => [ ], ], + 'assertionsAndNegationsOnRanges' => [ + 'code' => ' + throw new Exception(); + } + + $res2 = $a; //should be int<1, max> + + if ($b > 1) { + $res3 = $b; //should be int<2, max> + throw new Exception(); + } + + $res4 = $b; //should be int + + if ($c <= 1) { + $res5 = $c; //should be int + throw new Exception(); + } + + $res6 = $c; //should be int<2, max> + + if ($d >= 1) { + $res7 = $d; //should be int<1, max> + throw new Exception(); + } + + $res8 = $d; //should be int + + + + if (1 < $e) { + $res9 = $e; //should be int<2, max> + throw new Exception(); + } + + $res10 = $e; //should be int + + if (1 > $f) { + $res11 = $f; //should be int + throw new Exception(); + } + + $res12 = $f; //should be int<1, max> + + if (1 <= $g) { + $res13 = $g; //should be int<1, max> + throw new Exception(); + } + + $res14 = $g; //should be int + + if (1 >= $h) { + $res15 = $h; //should be int + throw new Exception(); + } + + $res16 = $h; //should be int<2, max>', + 'assertions' => [ + //'$res1' => 'int', + '$res2' => 'int<1, max>', + //'$res3' => 'int<2, max>', + '$res4' => 'int', + //'$res5' => 'int', + '$res6' => 'int<2, max>', + //'$res7' => 'int<1, max>', + '$res8' => 'int', + + //'$res9' => 'int<2, max>', + '$res10' => 'int', + //'$res11' => 'int', + '$res12' => 'int<1, max>', + //'$res13' => 'int<1, max>', + '$res14' => 'int', + //'$res15' => 'int', + '$res16' => 'int<2, max>', + + ], + ], ]; }