diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 0e24587e4c..585f8fc7a2 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2940,11 +2940,18 @@ private function calculateFromScalars(Expr $node, ConstantScalarType $leftType, } if ($node instanceof Node\Expr\BinaryOp\Div || $node instanceof Node\Expr\AssignOp\Div) { + if ($rightNumberValue === 0 || $rightNumberValue === 0.0) { + return new ErrorType(); + } return $this->getTypeFromValue($leftNumberValue / $rightNumberValue); } if ($node instanceof Node\Expr\BinaryOp\Mod || $node instanceof Node\Expr\AssignOp\Mod) { - return $this->getTypeFromValue(((int) $leftNumberValue) % ((int) $rightNumberValue)); + $rightIntegerValue = (int) $rightNumberValue; + if ($rightIntegerValue === 0) { + return new ErrorType(); + } + return $this->getTypeFromValue(((int) $leftNumberValue) % $rightIntegerValue); } if ($node instanceof Expr\BinaryOp\ShiftLeft || $node instanceof Expr\AssignOp\ShiftLeft) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8e95a448ab..3d25b2bf98 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4,6 +4,7 @@ use ArrayAccess; use Closure; +use DivisionByZeroError; use PhpParser\Comment\Doc; use PhpParser\Node; use PhpParser\Node\Arg; @@ -116,6 +117,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; @@ -1745,6 +1747,12 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + if ( + ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && + !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + ) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); + } } elseif ($expr instanceof FuncCall) { $parametersAcceptor = null; $functionReflection = null; @@ -2310,6 +2318,12 @@ static function (?Type $offsetType, Type $valueType) use (&$arrayType): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep()); + if ( + ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && + !$scope->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + ) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); + } $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); diff --git a/src/Parallel/Scheduler.php b/src/Parallel/Scheduler.php index b6cc240ae8..1a3bd38ac4 100644 --- a/src/Parallel/Scheduler.php +++ b/src/Parallel/Scheduler.php @@ -13,6 +13,8 @@ class Scheduler /** * @param positive-int $jobSize + * @param positive-int $maximumNumberOfProcesses + * @param positive-int $minimumNumberOfJobsPerProcess */ public function __construct( private int $jobSize, diff --git a/tests/PHPStan/Parallel/SchedulerTest.php b/tests/PHPStan/Parallel/SchedulerTest.php index 0d63fc1380..fb1fd626cf 100644 --- a/tests/PHPStan/Parallel/SchedulerTest.php +++ b/tests/PHPStan/Parallel/SchedulerTest.php @@ -73,6 +73,8 @@ public function dataSchedule(): array /** * @dataProvider dataSchedule * @param positive-int $jobSize + * @param positive-int $maximumNumberOfProcesses + * @param positive-int $minimumNumberOfJobsPerProcess * @param 0|positive-int $numberOfFiles * @param array $expectedJobSizes */ diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 1e6a7af18c..2aa85cfddd 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -334,4 +334,94 @@ public function testUnionTypeError(): void ]); } + public function testBug6349(): void + { + $this->analyse([__DIR__ . '/data/bug-6349.php'], [ + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 29, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 33, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 44, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 48, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 106, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 110, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 121, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 125, + ], + [ + // throw point not implemented yet, because there is no way to narrow float value by !== 0.0 + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 139, + ], + [ + // throw point not implemented yet, because there is no way to narrow float value by !== 0.0 + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 143, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 172, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 176, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 187, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 191, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 249, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 253, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 264, + ], + [ + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 268, + ], + [ + // throw point not implemented yet, because there is no way to narrow float value by !== 0.0 + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 282, + ], + [ + // throw point not implemented yet, because there is no way to narrow float value by !== 0.0 + 'Dead catch - DivisionByZeroError is never thrown in the try block.', + 286, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6349.php b/tests/PHPStan/Rules/Exceptions/data/bug-6349.php new file mode 100644 index 0000000000..fa4ba7fb1f --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6349.php @@ -0,0 +1,289 @@ +|int<1, max> $value + */ + public function nonZeroIntegerRange1(int $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int $value + */ + public function nonZeroIntegerRange2(int $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int $value + */ + public function zeroIncludedIntegerRange(int $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param array $values + */ + public function sayHello(array $values): float + { + try { + return 99 / $values['a']; + } catch (\DivisionByZeroError $e) { + return 0.0; + } + try { + return 99 % $values['a']; + } catch (\DivisionByZeroError $e) { + return 0.0; + } + } + + /** + * @param '0' $value + */ + public function numericZeroString(string $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param '1' $value + */ + public function numericNonZeroString(string $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param float $value + */ + public function floatValue(float $value): void + { + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param float $value + */ + public function floatNonZeroValue(float $value): void + { + if ($value === 0.0) { + return; + } + try { + 99 / $value; + } catch (\DivisionByZeroError $e) { + } + try { + 99 % $value; + } catch (\DivisionByZeroError $e) { + } + } +} + +class TestAssignOp +{ + /** + * @param int $value + */ + public function integer($val, int $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int|int<1, max> $value + */ + public function nonZeroIntegerRange1($val, int $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int $value + */ + public function nonZeroIntegerRange2($val, int $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param int $value + */ + public function zeroIncludedIntegerRange($val, int $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param array $values + */ + public function sayHello($val, array $values): float + { + try { + return $val /= $values['a']; + } catch (\DivisionByZeroError $e) { + return 0.0; + } + try { + return $val %= $values['a']; + } catch (\DivisionByZeroError $e) { + return 0.0; + } + } + + /** + * @param '0' $value + */ + public function numericZeroString($val, string $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param '1' $value + */ + public function numericNonZeroString($val, string $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param float $value + */ + public function floatValue($val, float $value): void + { + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } + + /** + * @param float $value + */ + public function floatNonZeroValue($val, float $value): void + { + if ($value === 0.0) { + return; + } + try { + $val /= $value; + } catch (\DivisionByZeroError $e) { + } + try { + $val %= $value; + } catch (\DivisionByZeroError $e) { + } + } +}