diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 2ac5906c16..fe4356db12 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1446,37 +1446,43 @@ 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) - ) { + $types = TypeCombinator::union($leftType, $rightType); + $leftNumberType = $leftType->toNumber(); + $rightNumberType = $rightType->toNumber(); - if ($leftType instanceof ConstantIntegerType) { + if ( + !$types instanceof MixedType + && ( + $rightNumberType instanceof IntegerRangeType + || $rightNumberType instanceof ConstantIntegerType + || $rightNumberType instanceof UnionType + ) + ) { + if ($leftNumberType instanceof IntegerRangeType || $leftNumberType instanceof ConstantIntegerType) { return $this->integerRangeMath( - $leftType, + $leftNumberType, $expr, - $rightType, + $rightNumberType, ); - } elseif ($leftType instanceof UnionType) { - + } elseif ($leftNumberType instanceof UnionType) { $unionParts = []; - foreach ($leftType->getTypes() as $type) { - if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $unionParts[] = $this->integerRangeMath($type, $expr, $rightType); + 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(); } return $union->toNumber(); } - - return $this->integerRangeMath($leftType, $expr, $rightType); } $specifiedTypes = $this->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); @@ -1484,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() @@ -1493,8 +1498,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(); } @@ -1531,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 { @@ -1548,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(); } @@ -1563,12 +1566,15 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T return $union->toNumber(); } + $operand = $operand->toNumber(); 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) { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 271ecf21e5..6a296910a1 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1249,6 +1249,8 @@ 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/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-8803.php b/tests/PHPStan/Analyser/data/bug-8803.php new file mode 100644 index 0000000000..a1d9ad568b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8803.php @@ -0,0 +1,36 @@ +format('N') + $offset; + if ($value > 7) { + } + + $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); + } + } + + public function testWithMixed(mixed $a, mixed $b): void + { + assertType('(array|float|int)', $a + $b); + assertType('(float|int)', 3 + $b); + assertType('(float|int)', $a + 3); + } +} 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; + } +} 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) {