diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index b7ae9ba4cb..bd463144b5 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -13,6 +13,7 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; @@ -975,6 +976,14 @@ public static function intersect(Type ...$types): Type continue 2; } + if ($types[$i] instanceof GenericClassStringType && $types[$j] instanceof GenericClassStringType) { + $genericType = self::intersect($types[$i]->getGenericType(), $types[$j]->getGenericType()); + $types[$i] = new GenericClassStringType($genericType); + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + continue; } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 5f74fb9820..fcc2d22928 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1038,6 +1038,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/cli-globals.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8033.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-union-unshift.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7987.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-4875.php b/tests/PHPStan/Analyser/data/bug-4875.php index d58ea60d12..ee0e89a44d 100644 --- a/tests/PHPStan/Analyser/data/bug-4875.php +++ b/tests/PHPStan/Analyser/data/bug-4875.php @@ -30,8 +30,8 @@ function doFoo() $mock = $this->mockIt(Blah::class); assertType('Bug4875\Blah&Bug4875\Mock', $mock); - assertType('class-string&class-string&literal-string', $mock::class); - assertType('class-string&class-string', get_class($mock)); + assertType('class-string&literal-string', $mock::class); + assertType('class-string', get_class($mock)); } } diff --git a/tests/PHPStan/Analyser/data/bug-7987.php b/tests/PHPStan/Analyser/data/bug-7987.php new file mode 100644 index 0000000000..23bda9fb31 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7987.php @@ -0,0 +1,53 @@ + $objectClass + * @return TypeObject + */ + public function getObject(string $objectClass): AbstractA|AbstractB + { + if (is_subclass_of($objectClass, AbstractA::class)) { + assertType('class-string', $objectClass); + $object = $this->getObjectA($objectClass); + assertType('TypeObject of Bug7987\AbstractA (method Bug7987\Factory::getObject(), argument)', $object); + } elseif (is_subclass_of($objectClass, AbstractB::class)) { + assertType('class-string', $objectClass); + $object = $this->getObjectB($objectClass); + assertType('TypeObject of Bug7987\AbstractB (method Bug7987\Factory::getObject(), argument)', $object); + } else { + throw new \Exception("unable to instantiate $objectClass"); + } + assertType('TypeObject of Bug7987\AbstractA (method Bug7987\Factory::getObject(), argument)|TypeObject of Bug7987\AbstractB (method Bug7987\Factory::getObject(), argument)', $object); + return $object; + } + + /** + * @template TypeObject of AbstractA + * @param class-string $objectClass + * @return TypeObject + */ + private function getObjectA(string $objectClass): AbstractA + { + return new $objectClass(); + } + + /** + * @template TypeObject of AbstractB + * @param class-string $objectClass + * @return TypeObject + */ + private function getObjectB(string $objectClass): AbstractB + { + return new $objectClass(); + } +}