From 36513388a628fac652f5d9cc5db8c6cdf21a175e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 13 Jan 2022 12:27:35 +0100 Subject: [PATCH] Scope generalization for integer range types Closes https://github.com/phpstan/phpstan/issues/3153 --- src/Analyser/MutatingScope.php | 80 +++++++++++++++++++ .../Analyser/LegacyNodeScopeResolverTest.php | 2 +- tests/PHPStan/Analyser/ScopeTest.php | 67 ++++++++++++++++ .../PHPStan/Analyser/data/for-loop-i-type.php | 6 +- ...isonOperatorsConstantConditionRuleTest.php | 10 +++ .../Rules/Comparison/data/bug-3153.php | 49 ++++++++++++ .../data/integer-range-generalization.php | 28 +++++++ 7 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-3153.php create mode 100644 tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index fcee38dc57..e6f8fdbe6f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -133,6 +133,8 @@ use function strtolower; use function substr; use function usort; +use const PHP_INT_MAX; +use const PHP_INT_MIN; class MutatingScope implements Scope { @@ -4679,6 +4681,7 @@ private static function generalizeType(Type $a, Type $b): Type $constantStrings = ['a' => [], 'b' => []]; $constantArrays = ['a' => [], 'b' => []]; $generalArrays = ['a' => [], 'b' => []]; + $integerRanges = ['a' => [], 'b' => []]; $otherTypes = []; foreach ([ @@ -4710,6 +4713,10 @@ private static function generalizeType(Type $a, Type $b): Type $generalArrays[$key][] = $type; continue; } + if ($type instanceof IntegerRangeType) { + $integerRanges[$key][] = $type; + continue; + } $otherTypes[] = $type; } @@ -4857,6 +4864,79 @@ private static function generalizeType(Type $a, Type $b): Type $resultTypes[] = TypeCombinator::union(...$constantIntegers['b']); } + if (count($integerRanges['a']) > 0) { + if (count($integerRanges['b']) === 0) { + $resultTypes[] = TypeCombinator::union(...$integerRanges['a']); + } else { + $integerRangesA = TypeCombinator::union(...$integerRanges['a']); + $integerRangesB = TypeCombinator::union(...$integerRanges['b']); + + if ($integerRangesA->equals($integerRangesB)) { + $resultTypes[] = $integerRangesA; + } else { + $min = null; + $max = null; + foreach ($integerRanges['a'] as $range) { + if ($range->getMin() === null) { + $rangeMin = PHP_INT_MIN; + } else { + $rangeMin = $range->getMin(); + } + if ($range->getMax() === null) { + $rangeMax = PHP_INT_MAX; + } else { + $rangeMax = $range->getMax(); + } + + if ($min === null || $rangeMin < $min) { + $min = $rangeMin; + } + if ($max !== null && $rangeMax <= $max) { + continue; + } + + $max = $rangeMax; + } + + $gotGreater = false; + $gotSmaller = false; + foreach ($integerRanges['b'] as $range) { + if ($range->getMin() === null) { + $rangeMin = PHP_INT_MIN; + } else { + $rangeMin = $range->getMin(); + } + if ($range->getMax() === null) { + $rangeMax = PHP_INT_MAX; + } else { + $rangeMax = $range->getMax(); + } + + if ($rangeMax > $max) { + $gotGreater = true; + } + if ($rangeMin >= $min) { + continue; + } + + $gotSmaller = true; + } + + if ($gotGreater && $gotSmaller) { + $resultTypes[] = new IntegerType(); + } elseif ($gotGreater) { + $resultTypes[] = IntegerRangeType::fromInterval($min, null); + } elseif ($gotSmaller) { + $resultTypes[] = IntegerRangeType::fromInterval(null, $max); + } else { + $resultTypes[] = TypeCombinator::union($integerRangesA, $integerRangesB); + } + } + } + } elseif (count($integerRanges['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$integerRanges['b']); + } + return TypeCombinator::union(...$resultTypes, ...$otherTypes); } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 3c8b5e8d06..f49e371de0 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -7097,7 +7097,7 @@ public function dataForLoopVariables(): array "'end'", ], [ - 'int<0, 10>', + 'int<0, max>', '$i', "'afterLoop'", ], diff --git a/tests/PHPStan/Analyser/ScopeTest.php b/tests/PHPStan/Analyser/ScopeTest.php index 61b3b6ab38..38268749a5 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -9,6 +9,7 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; @@ -155,6 +156,72 @@ public function dataGeneralize(): array ]), 'array>', ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]), + new UnionType([ + new ConstantIntegerType(-1), + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]), + 'int', + ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(2), + ]), + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ]), + '0|1|2', + ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ]), + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(2), + ]), + '0|1|2', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(1, 17), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(-1, 15), + 'int', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(1, null), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(null, 15), + 'int', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(0, null), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(null, 16), + 'int', + ], ]; } diff --git a/tests/PHPStan/Analyser/data/for-loop-i-type.php b/tests/PHPStan/Analyser/data/for-loop-i-type.php index 603b010f86..36b25f0dca 100644 --- a/tests/PHPStan/Analyser/data/for-loop-i-type.php +++ b/tests/PHPStan/Analyser/data/for-loop-i-type.php @@ -14,14 +14,14 @@ public function doBar() { assertType('int<1, 49>', $i); } - assertType('50', $i); + assertType('int<50, max>', $i); assertType(\stdClass::class, $foo); for($i = 50; $i > 0; $i--) { assertType('int<1, 50>', $i); } - assertType('0', $i); + assertType('int', $i); } public function doCount(array $a) { @@ -59,7 +59,7 @@ public function doLOrem() { break; } - assertType('int<1, 50>', $i); + assertType('int<1, max>', $i); } } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 6c24d4fc77..c1987ae689 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -73,4 +73,14 @@ public function testBug3867(): void $this->analyse([__DIR__ . '/data/bug-3867.php'], []); } + public function testIntegerRangeGeneralization(): void + { + $this->analyse([__DIR__ . '/data/integer-range-generalization.php'], []); + } + + public function testBug3153(): void + { + $this->analyse([__DIR__ . '/data/bug-3153.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3153.php b/tests/PHPStan/Rules/Comparison/data/bug-3153.php new file mode 100644 index 0000000000..64ae7ef1b8 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3153.php @@ -0,0 +1,49 @@ + 10 ){ + break; + } + } + } + } + + public function doBar() + { + $rows = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]; + $added_rows = 0; + $limit = random_int(1, 20); + + foreach($rows as $row){ + + if( $added_rows >= $limit ){ + break; + } + $added_rows++; + } + + if( $added_rows < 3 ){ + foreach($rows as $row){ + + $added_rows++; + + if( $added_rows > 10 ){ + break; + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php b/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php new file mode 100644 index 0000000000..c79e6e2106 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php @@ -0,0 +1,28 @@ +