From 4a4b0bc240e4199137ce15db175d1b0115a33307 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 29 Nov 2025 14:59:33 +0100 Subject: [PATCH] Implement BinaryModHandler --- .../ExprHandler/BinaryDivHandler.php | 13 +++- .../ExprHandler/BinaryModHandler.php | 64 +++++++++++++++++++ .../InitializerExprTypeResolver.php | 5 ++ .../PHPStan/Analyser/Generator/data/gnsr.php | 20 ++++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/Analyser/Generator/ExprHandler/BinaryModHandler.php diff --git a/src/Analyser/Generator/ExprHandler/BinaryDivHandler.php b/src/Analyser/Generator/ExprHandler/BinaryDivHandler.php index 07b7a76796..17ec34c213 100644 --- a/src/Analyser/Generator/ExprHandler/BinaryDivHandler.php +++ b/src/Analyser/Generator/ExprHandler/BinaryDivHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\Generator\ExprHandler; +use DivisionByZeroError; use Generator; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; @@ -10,9 +11,12 @@ use PHPStan\Analyser\Generator\ExprAnalysisResult; use PHPStan\Analyser\Generator\ExprHandler; use PHPStan\Analyser\Generator\GeneratorScope; +use PHPStan\Analyser\Generator\InternalThrowPoint; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\ObjectType; use function array_merge; /** @@ -36,13 +40,20 @@ public function analyseExpr(Stmt $stmt, Expr $expr, GeneratorScope $scope, Expre $leftResult = yield new ExprAnalysisRequest($stmt, $expr->left, $scope, $context, $alternativeNodeCallback); $rightResult = yield new ExprAnalysisRequest($stmt, $expr->right, $leftResult->scope, $context, $alternativeNodeCallback); + $throwPoints = array_merge($leftResult->throwPoints, $rightResult->throwPoints); + if ( + !$rightResult->type->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + ) { + $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); + } + return new ExprAnalysisResult( $this->initializerExprTypeResolver->getDivTypeFromTypes($expr->left, $expr->right, $leftResult->type, $rightResult->type), $this->initializerExprTypeResolver->getDivTypeFromTypes($expr->left, $expr->right, $leftResult->nativeType, $rightResult->nativeType), $rightResult->scope, hasYield: $leftResult->hasYield || $rightResult->hasYield, isAlwaysTerminating: $leftResult->isAlwaysTerminating || $rightResult->isAlwaysTerminating, - throwPoints: array_merge($leftResult->throwPoints, $rightResult->throwPoints), + throwPoints: $throwPoints, impurePoints: array_merge($leftResult->impurePoints, $rightResult->impurePoints), specifiedTruthyTypes: new SpecifiedTypes(), specifiedFalseyTypes: new SpecifiedTypes(), diff --git a/src/Analyser/Generator/ExprHandler/BinaryModHandler.php b/src/Analyser/Generator/ExprHandler/BinaryModHandler.php new file mode 100644 index 0000000000..ef5cdf983f --- /dev/null +++ b/src/Analyser/Generator/ExprHandler/BinaryModHandler.php @@ -0,0 +1,64 @@ + + */ +#[AutowiredService] +final class BinaryModHandler implements ExprHandler +{ + + public function __construct(private InitializerExprTypeResolver $initializerExprTypeResolver) + { + } + + public function supports(Expr $expr): bool + { + return $expr instanceof Expr\BinaryOp\Mod; + } + + public function analyseExpr(Stmt $stmt, Expr $expr, GeneratorScope $scope, ExpressionContext $context, ?callable $alternativeNodeCallback): Generator + { + $leftResult = yield new ExprAnalysisRequest($stmt, $expr->left, $scope, $context, $alternativeNodeCallback); + $rightResult = yield new ExprAnalysisRequest($stmt, $expr->right, $leftResult->scope, $context, $alternativeNodeCallback); + + $throwPoints = array_merge($leftResult->throwPoints, $rightResult->throwPoints); + if ( + !$rightResult->type->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + ) { + $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); + } + + return new ExprAnalysisResult( + $this->initializerExprTypeResolver->getModTypeFromTypes($expr->left, $expr->right, $leftResult->type, $rightResult->type), + $this->initializerExprTypeResolver->getModTypeFromTypes($expr->left, $expr->right, $leftResult->nativeType, $rightResult->nativeType), + $rightResult->scope, + hasYield: $leftResult->hasYield || $rightResult->hasYield, + isAlwaysTerminating: $leftResult->isAlwaysTerminating || $rightResult->isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: array_merge($leftResult->impurePoints, $rightResult->impurePoints), + specifiedTruthyTypes: new SpecifiedTypes(), + specifiedFalseyTypes: new SpecifiedTypes(), + specifiedNullTypes: new SpecifiedTypes(), + ); + } + +} diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 27ceac0a4a..4f0fa58e1d 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1238,6 +1238,11 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + return $this->getModTypeFromTypes($left, $right, $leftType, $rightType); + } + + public function getModTypeFromTypes(Expr $left, Expr $right, Type $leftType, Type $rightType): Type + { if ($leftType instanceof NeverType || $rightType instanceof NeverType) { return $this->getNeverType($leftType, $rightType); } diff --git a/tests/PHPStan/Analyser/Generator/data/gnsr.php b/tests/PHPStan/Analyser/Generator/data/gnsr.php index b915536172..cf417287bd 100644 --- a/tests/PHPStan/Analyser/Generator/data/gnsr.php +++ b/tests/PHPStan/Analyser/Generator/data/gnsr.php @@ -5,6 +5,7 @@ namespace GeneratorNodeScopeResolverTest; use Closure; +use DivisionByZeroError; use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; @@ -50,6 +51,25 @@ public function doDiv($a, $b, int $c, int $d): void assertNativeType('1', 1 / 1); assertType('(float|int)', $c / $d); assertNativeType('(float|int)', $c / $d); + + assertType('*ERROR*', $c / 0); // DivisionByZeroError + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doMod($a, $b, int $c, int $d): void + { + assertType('int', $a % $b); + assertNativeType('int', $a % $b); + assertType('0', 1 % 1); + assertNativeType('0', 1 % 1); + assertType('int', $c % $d); + assertNativeType('int', $c % $d); + + assertType('*ERROR*', $c % 0); // DivisionByZeroError } /**