From db623a63348d85e4645b09acfd7c240df3678ccd Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 31 Jan 2023 00:33:35 +0100 Subject: [PATCH 1/7] Sdd support for numeric string computation --- .../InitializerExprTypeResolver.php | 21 ++++++++++--------- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/bug-8803.php | 20 ++++++++++++++++++ 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-8803.php diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 2ac5906c16..9b6a38ebd6 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1446,23 +1446,26 @@ private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { - if (($leftType instanceof IntegerRangeType || $leftType instanceof ConstantIntegerType || $leftType instanceof UnionType) && - ($rightType instanceof IntegerRangeType || $rightType instanceof ConstantIntegerType || $rightType instanceof UnionType) + $leftNumberType = $leftType->toNumber(); + $rightNumberType = $rightType->toNumber(); + + if (($leftNumberType instanceof IntegerRangeType || $leftNumberType instanceof ConstantIntegerType || $leftType instanceof UnionType) && + ($rightNumberType instanceof IntegerRangeType || $rightNumberType instanceof ConstantIntegerType || $rightType instanceof UnionType) ) { - if ($leftType instanceof ConstantIntegerType) { + if ($leftNumberType instanceof ConstantIntegerType) { return $this->integerRangeMath( - $leftType, + $leftNumberType, $expr, - $rightType, + $rightNumberType, ); } elseif ($leftType instanceof UnionType) { - $unionParts = []; foreach ($leftType->getTypes() as $type) { - if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $unionParts[] = $this->integerRangeMath($type, $expr, $rightType); + $numberType = $type->toNumber(); + if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) { + $unionParts[] = $this->integerRangeMath($numberType, $expr, $rightNumberType); } else { $unionParts[] = $type; } @@ -1493,8 +1496,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri return new ErrorType(); } - $leftNumberType = $leftType->toNumber(); - $rightNumberType = $rightType->toNumber(); if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { return new ErrorType(); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 271ecf21e5..81f3298ff0 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1249,6 +1249,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8956.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8917.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/ds-copy.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8803.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-type-alias.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8609.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-8609-function.php'); diff --git a/tests/PHPStan/Analyser/data/bug-8803.php b/tests/PHPStan/Analyser/data/bug-8803.php new file mode 100644 index 0000000000..a833e1a169 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8803.php @@ -0,0 +1,20 @@ +format('N') + $offset; + + assertType("'1'|'2'|'3'|'4'|'5'|'6'|'7'", $from->format('N')); + assertType('int<1, 14>', $offset); + assertType('int<2, 21>', $value); + } + } +} From 2d316412690ccdde41758d359a061a711ba78d91 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 31 Jan 2023 00:39:47 +0100 Subject: [PATCH 2/7] Add tests --- tests/PHPStan/Analyser/data/bug-8803.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/PHPStan/Analyser/data/bug-8803.php b/tests/PHPStan/Analyser/data/bug-8803.php index a833e1a169..f55933e9f6 100644 --- a/tests/PHPStan/Analyser/data/bug-8803.php +++ b/tests/PHPStan/Analyser/data/bug-8803.php @@ -17,4 +17,11 @@ public function sayHello(): void assertType('int<2, 21>', $value); } } + + public function testWithMixed(mixed $a, mixed $b): void + { + assertType('(array|float|int)', $a + $b); + assertType('(float|int)', 3 + $b); + assertType('(float|int)', $a + 3); + } } From 3d140c0c7a064aa026291b4e4c63c1e8f002091f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 31 Jan 2023 01:14:48 +0100 Subject: [PATCH 3/7] Fix --- src/Reflection/InitializerExprTypeResolver.php | 15 +++++++++++---- tests/PHPStan/Analyser/data/bug-8803.php | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 9b6a38ebd6..cba896c360 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -65,6 +65,8 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; + use function array_keys; use function array_merge; use function assert; @@ -1479,7 +1481,7 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri return $union->toNumber(); } - return $this->integerRangeMath($leftType, $expr, $rightType); + return $this->integerRangeMath($leftNumberType, $expr, $rightNumberType); } $specifiedTypes = $this->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); @@ -1532,7 +1534,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri /** * @param ConstantIntegerType|IntegerRangeType $range * @param BinaryOp\Div|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Plus $node - * @param IntegerRangeType|ConstantIntegerType|UnionType $operand */ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): Type { @@ -1549,8 +1550,9 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T $unionParts = []; foreach ($operand->getTypes() as $type) { - if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $unionParts[] = $this->integerRangeMath($range, $node, $type); + $numberType = $type->toNumber(); + if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) { + $unionParts[] = $this->integerRangeMath($range, $node, $numberType); } else { $unionParts[] = $type->toNumber(); } @@ -1564,6 +1566,11 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T return $union->toNumber(); } + $operand = $operand->toNumber(); + if (!$operand instanceof IntegerRangeType && !$operand instanceof ConstantIntegerType) { + return $operand; + } + if ($operand instanceof IntegerRangeType) { $operandMin = $operand->getMin(); $operandMax = $operand->getMax(); diff --git a/tests/PHPStan/Analyser/data/bug-8803.php b/tests/PHPStan/Analyser/data/bug-8803.php index f55933e9f6..40bf7a88e9 100644 --- a/tests/PHPStan/Analyser/data/bug-8803.php +++ b/tests/PHPStan/Analyser/data/bug-8803.php @@ -11,10 +11,12 @@ public function sayHello(): void $from = new \DateTimeImmutable('2023-01-30'); for ($offset = 1; $offset <= 14; $offset++) { $value = $from->format('N') + $offset; + $value2 = $offset + $from->format('N'); assertType("'1'|'2'|'3'|'4'|'5'|'6'|'7'", $from->format('N')); assertType('int<1, 14>', $offset); assertType('int<2, 21>', $value); + assertType('int<2, 21>', $value2); } } From a4894b8627c31e93bcf2f4f287f8e2d191b69203 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 31 Jan 2023 01:26:17 +0100 Subject: [PATCH 4/7] Simplify --- .../InitializerExprTypeResolver.php | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index cba896c360..d90d8b187b 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -65,8 +65,6 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; - use function array_keys; use function array_merge; use function assert; @@ -1451,11 +1449,8 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri $leftNumberType = $leftType->toNumber(); $rightNumberType = $rightType->toNumber(); - if (($leftNumberType instanceof IntegerRangeType || $leftNumberType instanceof ConstantIntegerType || $leftType instanceof UnionType) && - ($rightNumberType instanceof IntegerRangeType || $rightNumberType instanceof ConstantIntegerType || $rightType instanceof UnionType) - ) { - - if ($leftNumberType instanceof ConstantIntegerType) { + if ($rightNumberType instanceof IntegerRangeType || $rightNumberType instanceof ConstantIntegerType || $rightType instanceof UnionType) { + if ($leftNumberType instanceof IntegerRangeType || $leftNumberType instanceof ConstantIntegerType) { return $this->integerRangeMath( $leftNumberType, $expr, @@ -1480,8 +1475,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri return $union->toNumber(); } - - return $this->integerRangeMath($leftNumberType, $expr, $rightNumberType); } $specifiedTypes = $this->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); @@ -1567,16 +1560,14 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T } $operand = $operand->toNumber(); - if (!$operand instanceof IntegerRangeType && !$operand instanceof ConstantIntegerType) { - return $operand; - } - if ($operand instanceof IntegerRangeType) { $operandMin = $operand->getMin(); $operandMax = $operand->getMax(); - } else { + } elseif ($operand instanceof ConstantIntegerType) { $operandMin = $operand->getValue(); $operandMax = $operand->getValue(); + } else { + return $operand; } if ($node instanceof BinaryOp\Plus) { From 8cc1a93dd885a91d0aa48d176f20f1c40dbbcc4e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 31 Jan 2023 21:01:16 +0100 Subject: [PATCH 5/7] Add numeric-string test --- tests/PHPStan/Analyser/data/bug-8803.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/data/bug-8803.php b/tests/PHPStan/Analyser/data/bug-8803.php index 40bf7a88e9..fc8b1447b5 100644 --- a/tests/PHPStan/Analyser/data/bug-8803.php +++ b/tests/PHPStan/Analyser/data/bug-8803.php @@ -12,11 +12,15 @@ public function sayHello(): void for ($offset = 1; $offset <= 14; $offset++) { $value = $from->format('N') + $offset; $value2 = $offset + $from->format('N'); + $value3 = '1e3' + $offset; + $value4 = $offset + '1e3'; assertType("'1'|'2'|'3'|'4'|'5'|'6'|'7'", $from->format('N')); assertType('int<1, 14>', $offset); assertType('int<2, 21>', $value); assertType('int<2, 21>', $value2); + assertType('float', $value3); + assertType('float', $value4); } } From 1af7377b3c475b8cdf4e4208f1233cc70fa9d7b5 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 2 Feb 2023 21:13:58 +0100 Subject: [PATCH 6/7] AdAdd support for boolean --- .../InitializerExprTypeResolver.php | 19 ++++++++----- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/bug-8827.php | 27 +++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-8827.php diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index d90d8b187b..fe4356db12 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1446,30 +1446,38 @@ private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { + $types = TypeCombinator::union($leftType, $rightType); $leftNumberType = $leftType->toNumber(); $rightNumberType = $rightType->toNumber(); - if ($rightNumberType instanceof IntegerRangeType || $rightNumberType instanceof ConstantIntegerType || $rightType instanceof UnionType) { + if ( + !$types instanceof MixedType + && ( + $rightNumberType instanceof IntegerRangeType + || $rightNumberType instanceof ConstantIntegerType + || $rightNumberType instanceof UnionType + ) + ) { if ($leftNumberType instanceof IntegerRangeType || $leftNumberType instanceof ConstantIntegerType) { return $this->integerRangeMath( $leftNumberType, $expr, $rightNumberType, ); - } elseif ($leftType instanceof UnionType) { + } elseif ($leftNumberType instanceof UnionType) { $unionParts = []; - foreach ($leftType->getTypes() as $type) { + foreach ($leftNumberType->getTypes() as $type) { $numberType = $type->toNumber(); if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) { $unionParts[] = $this->integerRangeMath($numberType, $expr, $rightNumberType); } else { - $unionParts[] = $type; + $unionParts[] = $numberType; } } $union = TypeCombinator::union(...$unionParts); - if ($leftType instanceof BenevolentUnionType) { + if ($leftNumberType instanceof BenevolentUnionType) { return TypeUtils::toBenevolentUnion($union)->toNumber(); } @@ -1482,7 +1490,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri return $specifiedTypes; } - $types = TypeCombinator::union($leftType, $rightType); if ( $leftType->isArray()->yes() || $rightType->isArray()->yes() diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 81f3298ff0..6a296910a1 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1250,6 +1250,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8917.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/ds-copy.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8803.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8827.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-type-alias.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8609.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-8609-function.php'); diff --git a/tests/PHPStan/Analyser/data/bug-8827.php b/tests/PHPStan/Analyser/data/bug-8827.php new file mode 100644 index 0000000000..fae38f26b2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8827.php @@ -0,0 +1,27 @@ +', $efferent); // Expected: int<0, $nbElements> | Actual: 0|1 + assertType('int<0, max>', $afferent); // Expected: int<0, $nbElements> | Actual: 0|1 + + $instability = ($efferent + $afferent > 0) ? $efferent / ($afferent + $efferent) : 0; + } +} From f9b262ca8b8047e2e41b182752ba8f60d3285ab7 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 10 Apr 2023 20:23:28 +0200 Subject: [PATCH 7/7] Add tests --- tests/PHPStan/Analyser/data/bug-8803.php | 3 +++ .../NumberComparisonOperatorsConstantConditionRuleTest.php | 6 ++++++ .../Rules/Operators/InvalidBinaryOperationRuleTest.php | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/tests/PHPStan/Analyser/data/bug-8803.php b/tests/PHPStan/Analyser/data/bug-8803.php index fc8b1447b5..a1d9ad568b 100644 --- a/tests/PHPStan/Analyser/data/bug-8803.php +++ b/tests/PHPStan/Analyser/data/bug-8803.php @@ -11,6 +11,9 @@ public function sayHello(): void $from = new \DateTimeImmutable('2023-01-30'); for ($offset = 1; $offset <= 14; $offset++) { $value = $from->format('N') + $offset; + if ($value > 7) { + } + $value2 = $offset + $from->format('N'); $value3 = '1e3' + $offset; $value4 = $offset + '1e3'; diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 3c11736c25..cc2c600ece 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -207,6 +207,12 @@ public function testBug7075(): void $this->analyse([__DIR__ . '/data/bug-7075.php'], []); } + public function testBug8803(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8803.php'], []); + } + public function testBug8938(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index b5d75392e5..7729265a85 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -259,6 +259,11 @@ public function testBug3515(): void $this->analyse([__DIR__ . '/data/bug-3515.php'], []); } + public function testBug8827(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8827.php'], []); + } + public function testRuleWithNullsafeVariant(): void { if (PHP_VERSION_ID < 80000) {