diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4f3e7f9c433..6ca5cb75fef 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2844,7 +2844,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope if ($rightType instanceof ConstantStringType && $this->reflectionProvider->hasClass($rightType->getValue())) { return $this->create( $unwrappedLeftExpr->getArgs()[0]->value, - new ObjectType($rightType->getValue(), classReflection: $this->reflectionProvider->getClass($rightType->getValue())->asFinal()), + $this->determineExactClassType($scope, $unwrappedLeftExpr->getArgs()[0]->value, $rightType->getValue()), $context, $scope, )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr); @@ -2969,7 +2969,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope if ($this->reflectionProvider->hasClass($rightType->getValue())) { return $this->create( $unwrappedLeftExpr->class, - new ObjectType($rightType->getValue(), classReflection: $this->reflectionProvider->getClass($rightType->getValue())->asFinal()), + $this->determineExactClassType($scope, $unwrappedLeftExpr->class, $rightType->getValue()), $context, $scope, )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr); @@ -3000,7 +3000,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope if ($this->reflectionProvider->hasClass($leftType->getValue())) { return $this->create( $unwrappedRightExpr->class, - new ObjectType($leftType->getValue(), classReflection: $this->reflectionProvider->getClass($leftType->getValue())->asFinal()), + $this->determineExactClassType($scope, $unwrappedRightExpr->class, $leftType->getValue()), $context, $scope, )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr)); @@ -3123,4 +3123,20 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope return (new SpecifiedTypes([], []))->setRootExpr($expr); } + private function determineExactClassType(Scope $scope, Expr $exprNode, string $className): Type + { + $exprType = $scope->getType($exprNode); + $classReflection = $this->reflectionProvider->getClass($className)->asFinal(); + $asFinalType = new ObjectType($className, classReflection: $classReflection); + + $plainType = new ObjectType($className); + $narrowed = TypeCombinator::intersect($exprType, $plainType); + + if ($plainType->isSuperTypeOf($narrowed)->yes() && !$narrowed->isSuperTypeOf($plainType)->yes()) { + return $narrowed; + } + + return $asFinalType; + } + } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14413.php b/tests/PHPStan/Analyser/nsrt/bug-14413.php new file mode 100644 index 00000000000..3c13ed6d9d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14413.php @@ -0,0 +1,76 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug14413; + +use function PHPStan\Testing\assertType; + +/** @template T */ +abstract class Animal { + /** @return T */ + abstract public function value(): mixed; +} + +/** + * @template T + * @extends Animal + */ +class Cat extends Animal { + /** @param T $val */ + public function __construct(private mixed $val) {} + /** @return T */ + public function value(): mixed { return $this->val; } +} + +/** + * @template T + * @extends Animal + */ +class Dog extends Animal { + /** @return never */ + public function value(): never { throw new \RuntimeException(); } +} + +/** @param Cat|Dog $a */ +function unionMatchPreservesGeneric(Animal $a): void { + match ($a::class) { + Cat::class => assertType('string', $a->value()), + Dog::class => assertType('never', $a->value()), + }; +} + +/** @param Cat|Dog $a */ +function ifElseClassPreservesGeneric(Animal $a): void { + if ($a::class === Cat::class) { + assertType('Bug14413\Cat', $a); + assertType('int', $a->value()); + } else { + assertType('int', $a->value()); + } +} + +/** @param Cat|Dog $a */ +function mirrorCasePreservesGeneric(Animal $a): void { + if (Cat::class === $a::class) { + assertType('float', $a->value()); + } +} + +/** @param Cat>|Dog> $a */ +function matchWithMethodCall(Animal $a): void { + $result = match ($a::class) { + Cat::class => $a->value(), + Dog::class => [], + }; + assertType('array', $result); +} + +/** @param Cat|Dog $a */ +function nonMatchingClass(Animal $a): void { + if ($a::class === \stdClass::class) { + assertType('*NEVER*', $a); + } else { + assertType('Bug14413\Cat|Bug14413\Dog', $a); + } +}