diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 5b672a2db5b..39b40f66a9f 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -15,12 +15,16 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; +use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeSpecifier; +use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BooleanAndNode; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function array_merge; /** @@ -34,6 +38,7 @@ final class BooleanAndHandler implements ExprHandler public function __construct( private NodeScopeResolver $nodeScopeResolver, + private TypeSpecifier $typeSpecifier, ) { } @@ -99,15 +104,42 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftTruthyScope), $scope, $storage, $context); + $leftTruthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createTruthy()); + return new ExpressionResult( $leftMergedWithRightScope, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), - truthyScopeCallback: static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr->right), + truthyScopeCallback: static fn (): MutatingScope => self::computeTruthyScope($rightResult->getScope(), $expr->right, $leftTruthySpecifiedTypes, $leftTruthyScope), falseyScopeCallback: static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), ); } + private static function computeTruthyScope( + MutatingScope $rightScope, + Expr $rightExpr, + SpecifiedTypes $leftTruthySpecifiedTypes, + MutatingScope $leftTruthyScope, + ): MutatingScope + { + $scope = $rightScope->filterByTruthyValue($rightExpr); + + foreach ($leftTruthySpecifiedTypes->getSureNotTypes() as [$exprNode, $sureNotType]) { + if (!$leftTruthyScope->getType($exprNode)->equals($rightScope->getType($exprNode))) { + continue; + } + + $exprType = $scope->getType($exprNode); + if (TypeCombinator::remove($exprType, $sureNotType) === $exprType) { + continue; + } + + $scope = $scope->removeTypeFromExpression($exprNode, $sureNotType); + } + + return $scope; + } + } diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index 9c828edb942..b43d25afe84 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -13,12 +13,16 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; +use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeSpecifier; +use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BooleanOrNode; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function array_merge; /** @@ -32,6 +36,7 @@ final class BooleanOrHandler implements ExprHandler public function __construct( private NodeScopeResolver $nodeScopeResolver, + private TypeSpecifier $typeSpecifier, ) { } @@ -83,6 +88,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftFalseyScope), $scope, $storage, $context); + $leftFalseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createFalsey()); + return new ExpressionResult( $leftMergedWithRightScope, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), @@ -90,8 +97,33 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), truthyScopeCallback: static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr->right), + falseyScopeCallback: static fn (): MutatingScope => self::computeFalseyScope($rightResult->getScope(), $expr->right, $leftFalseySpecifiedTypes, $leftFalseyScope), ); } + private static function computeFalseyScope( + MutatingScope $rightScope, + Expr $rightExpr, + SpecifiedTypes $leftFalseySpecifiedTypes, + MutatingScope $leftFalseyScope, + ): MutatingScope + { + $scope = $rightScope->filterByFalseyValue($rightExpr); + + foreach ($leftFalseySpecifiedTypes->getSureNotTypes() as [$exprNode, $sureNotType]) { + if (!$leftFalseyScope->getType($exprNode)->equals($rightScope->getType($exprNode))) { + continue; + } + + $exprType = $scope->getType($exprNode); + if (TypeCombinator::remove($exprType, $sureNotType) === $exprType) { + continue; + } + + $scope = $scope->removeTypeFromExpression($exprNode, $sureNotType); + } + + return $scope; + } + } diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index df18959ae24..fe35b522e81 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -64,8 +64,20 @@ public static function addNull(Type $type): Type public static function remove(Type $fromType, Type $typeToRemove): Type { if ($typeToRemove instanceof UnionType) { - foreach ($typeToRemove->getTypes() as $unionTypeToRemove) { - $fromType = self::remove($fromType, $unionTypeToRemove); + $typesToRemove = $typeToRemove->getTypes(); + $changed = true; + while ($changed && count($typesToRemove) > 0) { + $changed = false; + foreach ($typesToRemove as $key => $unionTypeToRemove) { + $newFromType = self::remove($fromType, $unionTypeToRemove); + if ($newFromType === $fromType) { + continue; + } + + $fromType = $newFromType; + unset($typesToRemove[$key]); + $changed = true; + } } return $fromType; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-9114.php b/tests/PHPStan/Analyser/nsrt/bug-9114.php new file mode 100644 index 00000000000..d1015fcb2b8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9114.php @@ -0,0 +1,47 @@ +