diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8616d227c86..9ce95e3aeaf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1122,7 +1122,7 @@ parameters: - rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. identifier: phpstanApi.instanceofType - count: 3 + count: 4 path: src/Type/Generic/GenericObjectType.php - @@ -1134,7 +1134,7 @@ parameters: - rawMessage: 'Doing instanceof PHPStan\Type\ObjectType is error-prone and deprecated. Use Type::isObject() or Type::getObjectClassNames() instead.' identifier: phpstanApi.instanceofType - count: 2 + count: 3 path: src/Type/Generic/GenericObjectType.php - diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 7ee37d6d413..abfe1d1d3a7 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -21,6 +21,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\RecursionGuard; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -123,7 +124,17 @@ public function getVariances(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + $result = $type->isAcceptedBy($this, $strictTypes); + if (!$result->yes() && $type instanceof UnionType) { + $mergedType = $this->mergeUnionMembers($type); + if ($mergedType !== null) { + $mergedResult = $this->isSuperTypeOfInternal($mergedType, true)->toAcceptsResult(); + if ($mergedResult->yes()) { + return $mergedResult; + } + } + } + return $result; } return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); @@ -209,6 +220,43 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSupe return $result; } + private function mergeUnionMembers(UnionType $unionType): ?self + { + $className = $this->getClassName(); + $paramCount = count($this->types); + $typeParameterArrays = []; + + foreach ($unionType->getTypes() as $memberType) { + if (!$memberType instanceof ObjectType) { + return null; + } + $ancestor = $memberType->getAncestorWithClassName($className); + if (!$ancestor instanceof self) { + return null; + } + if (count($ancestor->getTypes()) !== $paramCount) { + return null; + } + foreach ($ancestor->getTypes() as $typeParam) { + if (count($typeParam->getReferencedTemplateTypes(TemplateTypeVariance::createInvariant())) > 0) { + return null; + } + } + $typeParameterArrays[] = $ancestor->getTypes(); + } + + $mergedTypes = []; + for ($i = 0; $i < $paramCount; $i++) { + $typesAtPosition = []; + foreach ($typeParameterArrays as $types) { + $typesAtPosition[] = $types[$i]; + } + $mergedTypes[$i] = TypeCombinator::union(...$typesAtPosition); + } + + return new self($className, $mergedTypes); + } + public function getClassReflection(): ?ClassReflection { if ($this->classReflection !== null) { diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a77bd07ab44..278b789d461 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2850,4 +2850,9 @@ public function testBug13643(): void $this->analyse([__DIR__ . '/data/bug-13643.php'], []); } + public function testBug3136(): void + { + $this->analyse([__DIR__ . '/data/bug-3136.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-3136.php b/tests/PHPStan/Rules/Functions/data/bug-3136.php new file mode 100644 index 00000000000..870c2ad6251 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3136.php @@ -0,0 +1,89 @@ +value = $value; } +} + +/** @template T of TypeAorB */ +class SubContainer extends Container { + /** @param T $value */ + public function __construct(TypeAorB $value) { parent::__construct($value); } +} + +/** + * @template TKey + * @template TValue of TypeAorB + */ +class Pair { + /** @var TKey */ + public $key; + /** @var TValue */ + public $value; + /** + * @param TKey $key + * @param TValue $value + */ + public function __construct($key, TypeAorB $value) { + $this->key = $key; + $this->value = $value; + } +} + +/** + * @template T of TypeAorB + * @param Container $container + */ +function run(Container $container): void{ + var_dump($container->value); +} + +/** + * @template TKey + * @template TValue of TypeAorB + * @param Pair $pair + */ +function runPair(Pair $pair): void{ + var_dump($pair->key, $pair->value); +} + +$a = new Container(new TypeA); +$b = new Container(new TypeB); + +run($a); +run($b); + +// union of two generic objects +foreach ([$a, $b] as $item){ + run($item); +} + +// union of three generic objects +$c = new Container(new TypeC); +foreach ([$a, $b, $c] as $item){ + run($item); +} + +// subclass union +$subA = new SubContainer(new TypeA); +$subB = new SubContainer(new TypeB); +foreach ([$subA, $subB] as $item){ + run($item); +} + +// multiple template parameters +$p1 = new Pair(1, new TypeA); +$p2 = new Pair(2, new TypeB); +foreach ([$p1, $p2] as $item){ + runPair($item); +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 9a2a8e1a9f9..856257fb73b 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4017,4 +4017,12 @@ public function testBug13272(): void $this->analyse([__DIR__ . '/data/bug-13272.php'], []); } + public function testBug3136(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3136.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-3136.php b/tests/PHPStan/Rules/Methods/data/bug-3136.php new file mode 100644 index 00000000000..2587708ca8f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3136.php @@ -0,0 +1,43 @@ +value = $value; } +} + +class Runner { + /** + * @template T of TypeAorB + * @param Container $container + */ + public function run(Container $container): void { + var_dump($container->value); + } + + /** + * @template T of TypeAorB + * @param Container $container + */ + public static function runStatic(Container $container): void { + var_dump($container->value); + } +} + +$a = new Container(new TypeA); +$b = new Container(new TypeB); + +$runner = new Runner(); + +foreach ([$a, $b] as $item){ + $runner->run($item); + Runner::runStatic($item); +}