1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Merge pull request #7511 from orklah/literal-inequality

improve literal inequality with ranges and rework GreaterThan/LessThan assertions
This commit is contained in:
orklah 2022-01-28 22:27:09 +01:00 committed by GitHub
commit 048025b1d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 241 additions and 87 deletions

View File

@ -51,9 +51,11 @@ use Psalm\Storage\Assertion\IsClassNotEqual;
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 @@ class AssertionFinder
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 @@ class AssertionFinder
$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 @@ class AssertionFinder
$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 @@ class AssertionFinder
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 @@ class AssertionFinder
$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 @@ class AssertionFinder
$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 @@ class AssertionFinder
$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 @@ class AssertionFinder
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 @@ class AssertionFinder
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 @@ class AssertionFinder
$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 @@ class AssertionFinder
}
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)]];
}
}
}

View File

@ -547,7 +547,6 @@ class ArrayFunctionArgumentsAnalyzer
]
);
} else {
/** @psalm-suppress InvalidPropertyAssignmentValue */
$array_atomic_type->count--;
}
} else {
@ -565,7 +564,6 @@ class ArrayFunctionArgumentsAnalyzer
]
);
} else {
/** @psalm-suppress InvalidPropertyAssignmentValue */
$array_atomic_type->count--;
}
} else {

View File

@ -328,6 +328,44 @@ class NegatedAssertionReconciler extends Reconciler
$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;
}

View File

@ -153,7 +153,7 @@ class SimpleAssertionReconciler extends Reconciler
}
if ($assertion instanceof IsGreaterThan) {
return self::reconcileSuperiorTo(
return self::reconcileIsGreaterThan(
$assertion,
$existing_var_type,
$inside_loop,
@ -166,7 +166,7 @@ class SimpleAssertionReconciler extends Reconciler
}
if ($assertion instanceof IsLessThan) {
return self::reconcileInferiorTo(
return self::reconcileIsLessThan(
$assertion,
$existing_var_type,
$inside_loop,
@ -1612,7 +1612,7 @@ class SimpleAssertionReconciler extends Reconciler
/**
* @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 @@ class SimpleAssertionReconciler extends Reconciler
?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 @@ class SimpleAssertionReconciler extends Reconciler
/**
* @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 @@ class SimpleAssertionReconciler extends Reconciler
?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

View File

@ -1653,22 +1653,23 @@ class SimpleNegatedAssertionReconciler extends Reconciler
*/
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 @@ class SimpleNegatedAssertionReconciler extends Reconciler
*/
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;

View File

@ -6,9 +6,9 @@ use Psalm\Storage\Assertion;
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 @@ class IsGreaterThan extends Assertion
{
return $assertion instanceof IsLessThanOrEqualTo && $this->value === $assertion->value;
}
public function doesFilterNull(): bool
{
return true;
}
}

View File

@ -6,9 +6,9 @@ use Psalm\Storage\Assertion;
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 @@ class IsGreaterThanOrEqualTo extends Assertion
{
return $assertion instanceof IsLessThan && $this->value === $assertion->value;
}
public function doesFilterNull(): bool
{
return $this->value !== 0;
}
}

View File

@ -6,9 +6,9 @@ use Psalm\Storage\Assertion;
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 @@ class IsLessThan extends Assertion
{
return $assertion instanceof IsGreaterThanOrEqualTo && $this->value === $assertion->value;
}
public function doesFilterNull(): bool
{
return $this->value === 0;
}
}

View File

@ -6,9 +6,9 @@ use Psalm\Storage\Assertion;
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 @@ class IsLessThanOrEqualTo extends Assertion
{
return $assertion instanceof IsGreaterThan && $this->value === $assertion->value;
}
public function doesFilterNull(): bool
{
return false;
}
}

View File

@ -837,6 +837,21 @@ class BinaryOperationTest extends TestCase
return ($foo instanceof FooInterface ? $foo->toString() : null) ?? "Not a stringable foo";
}',
],
'handleLiteralInequalityWithInts' => [
'code' => '<?php
/**
* @param int<0, max> $i
* @return int<1, max>
*/
function toPositiveInt(int $i): int
{
if ($i !== 0) {
return $i;
}
return 1;
}',
],
];
}

View File

@ -716,6 +716,90 @@ class IntRangeTest extends TestCase
'assertions' => [
],
],
'assertionsAndNegationsOnRanges' => [
'code' => '<?php
/** @var int $int */
$int = 1;
$a = $b = $c = $d = $e = $f = $g = $h = $int;
if ($a < 1) {
$res1 = $a; //should be int<min, 0>
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<min, 1>
if ($c <= 1) {
$res5 = $c; //should be int<min, 1>
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<min, 0>
if (1 < $e) {
$res9 = $e; //should be int<2, max>
throw new Exception();
}
$res10 = $e; //should be int<min, 1>
if (1 > $f) {
$res11 = $f; //should be int<min, 0>
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<min, 0>
if (1 >= $h) {
$res15 = $h; //should be int<min, 1>
throw new Exception();
}
$res16 = $h; //should be int<2, max>',
'assertions' => [
//'$res1' => 'int<min, 0>',
'$res2' => 'int<1, max>',
//'$res3' => 'int<2, max>',
'$res4' => 'int<min, 1>',
//'$res5' => 'int<min, 1>',
'$res6' => 'int<2, max>',
//'$res7' => 'int<1, max>',
'$res8' => 'int<min, 0>',
//'$res9' => 'int<2, max>',
'$res10' => 'int<min, 1>',
//'$res11' => 'int<min, 0>',
'$res12' => 'int<1, max>',
//'$res13' => 'int<1, max>',
'$res14' => 'int<min, 0>',
//'$res15' => 'int<min, 1>',
'$res16' => 'int<2, max>',
],
],
];
}