diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 136e2023ba2..57cee2455cf 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3274,7 +3274,11 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self { $conditionalExpressions = $this->conditionalExpressions; - $conditionalExpressions[$exprString] = $conditionalExpressionHolders; + if (isset($conditionalExpressions[$exprString])) { + $conditionalExpressions[$exprString] = array_merge($conditionalExpressions[$exprString], $conditionalExpressionHolders); + } else { + $conditionalExpressions[$exprString] = $conditionalExpressionHolders; + } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), @@ -3295,6 +3299,14 @@ public function addConditionalExpressions(string $exprString, array $conditional ); } + /** + * @return array + */ + public function getConditionalExpressions(): array + { + return $this->conditionalExpressions; + } + public function exitFirstLevelStatements(): self { if (!$this->inFirstLevelStatement) { diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4f3e7f9c433..7be4501d1ad 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -756,7 +756,9 @@ public function specifyTypesInCondition( ) { $types = $leftTypes->normalize($scope); } else { - $types = $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); + $leftNormalized = $this->resolveConditionalExpressions($scope, $leftTypes->normalize($scope)); + $rightNormalized = $this->resolveConditionalExpressions($rightScope, $rightTypes->normalize($rightScope)); + $types = $leftNormalized->intersectWith($rightNormalized); } } else { $types = $leftTypes->unionWith($rightTypes); @@ -2098,6 +2100,59 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy return []; } + private function resolveConditionalExpressions(MutatingScope $scope, SpecifiedTypes $specifiedTypes): SpecifiedTypes + { + $sureTypes = $specifiedTypes->getSureTypes(); + $specifiedExpressions = []; + foreach ($sureTypes as $exprString => [$expr, $type]) { + $specifiedExpressions[$exprString] = ExpressionTypeHolder::createYes($expr, $type); + } + + if (count($specifiedExpressions) === 0) { + return $specifiedTypes; + } + + $additionalSureTypes = []; + foreach ($scope->getConditionalExpressions() as $conditionalExprString => $conditionalExpressions) { + foreach ($conditionalExpressions as $conditionalExpression) { + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { + continue 2; + } + } + + $holder = $conditionalExpression->getTypeHolder(); + if (isset($additionalSureTypes[$conditionalExprString])) { + $additionalSureTypes[$conditionalExprString] = [ + $holder->getExpr(), + TypeCombinator::intersect($additionalSureTypes[$conditionalExprString][1], $holder->getType()), + ]; + } else { + $additionalSureTypes[$conditionalExprString] = [$holder->getExpr(), $holder->getType()]; + } + } + } + + if (count($additionalSureTypes) === 0) { + return $specifiedTypes; + } + + foreach ($additionalSureTypes as $additionalExprString => [$additionalExpr, $additionalType]) { + if (isset($sureTypes[$additionalExprString])) { + $sureTypes[$additionalExprString] = [$additionalExpr, TypeCombinator::union($sureTypes[$additionalExprString][1], $additionalType)]; + } else { + $sureTypes[$additionalExprString] = [$additionalExpr, $additionalType]; + } + } + + $result = new SpecifiedTypes($sureTypes, $specifiedTypes->getSureNotTypes()); + if ($specifiedTypes->shouldOverwrite()) { + $result = $result->setAlwaysOverwriteTypes(); + } + + return $result->setRootExpr($specifiedTypes->getRootExpr()); + } + /** * @return array{Expr, ConstantScalarType, Type}|null */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-7716.php b/tests/PHPStan/Analyser/nsrt/bug-7716.php index fca0a7cfd8a..63280c3b466 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7716.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7716.php @@ -15,7 +15,7 @@ public function sayHello(array $array): int $hasBar = isset($array['bar']) && $array['bar'] > 1; if ($hasFoo) { - assertType('array{foo?: int, bar?: int}', $array); + assertType('array{foo: int, bar?: int}', $array); assertType('int<2, max>', $array['foo']); return $array['foo']; } @@ -44,7 +44,7 @@ public function sayHello2(array $array): int } if ($hasBar) { - assertType('array{foo?: int, bar?: int}', $array); + assertType('array{foo?: int, bar: int}', $array); assertType('int<2, max>', $array['bar']); return $array['bar']; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-9519.php b/tests/PHPStan/Analyser/nsrt/bug-9519.php new file mode 100644 index 00000000000..7a972d27232 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9519.php @@ -0,0 +1,87 @@ + $class_name + * @return T + */ +function getObject(string $class_name): object { + return new $class_name; +} + +function test4(): void { + $obj = getObject(ObjectClass::class); + $is_other = $obj instanceof OtherClass; + $is_interface = $obj instanceof SomeInterface; + + if ($is_interface) { + assertType('Bug9519\ObjectClass&Bug9519\SomeInterface', $obj); + } +} + +function test5(): void { + $obj = getObject(ObjectClass::class); + $is_interface = $obj instanceof SomeInterface; + $is_other = $obj instanceof OtherClass; + + if ($is_interface) { + assertType('Bug9519\ObjectClass&Bug9519\SomeInterface', $obj); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/multi-assign.php b/tests/PHPStan/Analyser/nsrt/multi-assign.php index ab4e4de1cf8..6a2b4a5f645 100644 --- a/tests/PHPStan/Analyser/nsrt/multi-assign.php +++ b/tests/PHPStan/Analyser/nsrt/multi-assign.php @@ -55,12 +55,12 @@ function (bool $b): void { function (bool $b): void { $foo = $bar = $baz = $b; if ($bar) { - assertType('bool', $b); + assertType('true', $b); assertType('bool', $foo); assertType('true', $bar); assertType('bool', $baz); } else { - assertType('bool', $b); + assertType('false', $b); assertType('bool', $foo); assertType('false', $bar); assertType('bool', $baz); @@ -70,12 +70,12 @@ function (bool $b): void { function (bool $b): void { $foo = $bar = $baz = $b; if ($baz) { - assertType('bool', $b); + assertType('true', $b); assertType('bool', $foo); assertType('bool', $bar); assertType('true', $baz); } else { - assertType('bool', $b); + assertType('false', $b); assertType('bool', $foo); assertType('bool', $bar); assertType('false', $baz);