diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 5ae4060811b..8451807c049 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -719,7 +719,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; @@ -773,8 +782,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); @@ -2061,6 +2075,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 8016cee235b..c33f80b9278 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 8016cee235b..c33f80b9278 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-13061.php b/tests/PHPStan/Analyser/nsrt/bug-13061.php new file mode 100644 index 00000000000..0f494d1481c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13061.php @@ -0,0 +1,68 @@ +} + * @phpstan-type TScenarioHash array{type?: 'scenario', title?: string|null} + * @phpstan-type TOutlineHash array{type: 'outline', title?: string|null, examples?: array} + * @phpstan-type TExampleTableHash array> + */ +abstract class GherkinArrayLoader +{ + /** + * @phpstan-param TFeatureHash $hash + */ + protected function loadFeatureHash(array $hash, int $line = 0): FeatureNode + { + $hash = array_merge( + [ + 'title' => null, + 'scenarios' => [], + ], + $hash + ); + + $scenarios = []; + foreach ((array) $hash['scenarios'] as $scenarioIterator => $scenarioHash) { + if (isset($scenarioHash['type']) && $scenarioHash['type'] === 'outline') { + assertType("array{type: 'outline', title?: string|null, examples?: array>>}", $scenarioHash); + $scenarios[] = $this->loadOutlineHash($scenarioHash, $scenarioIterator); + } else { + assertType("array{type?: 'scenario', title?: string|null}", $scenarioHash); + $scenarios[] = $this->loadScenarioHash($scenarioHash, $scenarioIterator); + } + } + + return new FeatureNode($hash['title'], $scenarios); + } + + /** + * @phpstan-param TScenarioHash $hash + */ + abstract protected function loadScenarioHash(array $hash, int $line = 0): ScenarioNode; + + /** + * @phpstan-param TOutlineHash $hash + */ + abstract protected function loadOutlineHash(array $hash, int $line = 0): OutlineNode; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14566.php b/tests/PHPStan/Analyser/nsrt/bug-14566.php new file mode 100644 index 00000000000..19ca598afed --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14566.php @@ -0,0 +1,98 @@ +from; + $newTill = $this->till; + + if ($newFrom !== null || $newTill !== null) { + if ($newFrom !== null && $newTill === null) { + $newFrom = $newFrom->setTime(0, 0); + $newTill = new \DateTimeImmutable('2300-12-31 23:59:59'); + } + + if ($newTill !== null && $newFrom === null) { + $newTill = $newTill->setTime(23, 59, 59, 999999); + $newFrom = new \DateTimeImmutable('1970-01-01 00:00:00'); + } + + assertType('DateTimeImmutable', $newFrom); + assertType('DateTimeImmutable', $newTill); + $this->checkDates($newFrom, $newTill); + } + } + + private function checkDates( + \DateTimeImmutable $from, + \DateTimeImmutable $till, + ): void + { + } +} + +class HelloWorldStringInt +{ + public function __construct( + private string|int $from, + private string|int $till, + ) + { + $newFrom = $this->from; + $newTill = $this->till; + + if (is_string($newFrom) || is_string($newTill)) { + if (is_string($newFrom) && is_string($newTill) === false) { + $newTill = 'test'; + } + + if (is_string($newTill) && is_string($newFrom) === false) { + $newFrom = 'test2'; + } + + assertType('string', $newFrom); + assertType('string', $newTill); + $this->checkDates($newFrom, $newTill); + } + } + + private function checkDates( + string $from, + string $till, + ): void + { + } +} diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index 1e7a8a7d90b..336dbd2b14c 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -511,7 +511,10 @@ public function testBug3632(): void [ 'Instanceof between Bug3632\NiceClass and Bug3632\NiceClass will always evaluate to true.', 36, - $tipText, + ], + [ + 'Instanceof between null and Bug3632\NiceClass will always evaluate to false.', + 36, ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index f86e67ef693..98f3f3e95af 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -170,7 +170,6 @@ public function testBug11903(): void [ 'Negated boolean expression is always true.', 21, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); }