diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 9f7a86eb4a..5701a80016 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -734,7 +734,16 @@ public function specifyTypesInCondition( $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); $rightScope = $scope->filterByTruthyValue($expr->left); $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); - $types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); + if ($context->true()) { + $types = $leftTypes->unionWith($rightTypes); + } else { + $leftNormalized = $leftTypes->normalize($scope); + $rightNormalized = $rightTypes->normalize($rightScope); + $types = $leftNormalized->intersectWith($rightNormalized); + $leftFalseyScope = $scope->filterByFalseyValue($expr->left); + $rightFalseyScope = $rightScope->filterByFalseyValue($expr->right); + $types = $this->augmentDisjunctionTypes($scope, $leftNormalized, $rightNormalized, $leftFalseyScope, $rightFalseyScope, $types); + } if ($context->false()) { $leftTypesForHolders = $leftTypes; $rightTypesForHolders = $rightTypes; @@ -788,8 +797,13 @@ public function specifyTypesInCondition( ) { $types = $leftTypes->normalize($scope); } else { - $types = $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); + $leftNormalized = $leftTypes->normalize($scope); + $rightNormalized = $rightTypes->normalize($rightScope); + $types = $leftNormalized->intersectWith($rightNormalized); $types = $this->augmentBooleanOrTruthyWithConditionalHolders($scope, $rightScope, $expr, $types); + $leftTruthyScopeForAugment = $scope->filterByTruthyValue($expr->left); + $rightTruthyScopeForAugment = $rightScope->filterByTruthyValue($expr->right); + $types = $this->augmentDisjunctionTypes($scope, $leftNormalized, $rightNormalized, $leftTruthyScopeForAugment, $rightTruthyScopeForAugment, $types); } } else { $types = $leftTypes->unionWith($rightTypes); @@ -2076,6 +2090,65 @@ private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $sco return $types; } + private function augmentDisjunctionTypes( + MutatingScope $scope, + SpecifiedTypes $leftNormalized, + SpecifiedTypes $rightNormalized, + MutatingScope $leftFilteredScope, + MutatingScope $rightFilteredScope, + SpecifiedTypes $types, + ): SpecifiedTypes + { + $candidateExprs = []; + foreach ($leftNormalized->getSureTypes() as $exprString => [$exprNode, $type]) { + $candidateExprs[$exprString] = $exprNode; + } + foreach ($rightNormalized->getSureTypes() as $exprString => [$exprNode, $type]) { + $candidateExprs[$exprString] = $exprNode; + } + + $existingSureTypes = $types->getSureTypes(); + + foreach ($candidateExprs as $exprString => $targetExpr) { + if (isset($existingSureTypes[$exprString])) { + continue; + } + + if (!$scope->hasExpressionType($targetExpr)->yes()) { + continue; + } + if (!$leftFilteredScope->hasExpressionType($targetExpr)->yes()) { + continue; + } + if (!$rightFilteredScope->hasExpressionType($targetExpr)->yes()) { + continue; + } + + $originalType = $scope->getType($targetExpr); + $leftType = $leftFilteredScope->getType($targetExpr); + $rightType = $rightFilteredScope->getType($targetExpr); + + if ($leftType->equals($originalType) || !$originalType->isSuperTypeOf($leftType)->yes()) { + continue; + } + + if ($rightType->equals($originalType) || !$originalType->isSuperTypeOf($rightType)->yes()) { + continue; + } + + $unionType = TypeCombinator::union($leftType, $rightType); + if ($unionType->equals($originalType)) { + continue; + } + + $types = $types->unionWith( + $this->create($targetExpr, $unionType, TypeSpecifierContext::createTrue(), $scope), + ); + } + + return $types; + } + /** * @return array */ diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php index 8016cee235..c33f80b927 100644 --- a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php @@ -10,5 +10,5 @@ if ((new \PHPStan\Tests\AssertionClass())->assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { } -assertType('string|null', $foo); +assertType('string', $foo); assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php index 8016cee235..c33f80b927 100644 --- a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php @@ -10,5 +10,5 @@ if ((new \PHPStan\Tests\AssertionClass())->assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { } -assertType('string|null', $foo); +assertType('string', $foo); assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14566.php b/tests/PHPStan/Analyser/nsrt/bug-14566.php new file mode 100644 index 0000000000..19ca598afe --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14566.php @@ -0,0 +1,98 @@ +treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); }