diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php index d34ba70d4bc..4a6e8421d91 100644 --- a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -18,13 +18,16 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use function array_key_exists; +use function array_map; use function count; use function is_string; use function sprintf; @@ -182,6 +185,17 @@ private function isValidSuperType(Scope $scope, Type $type, Type $varTagType, in return $this->isSuperTypeOfVarType($scope, $type, $varTagType); } + $type = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof GenericObjectType) { + $type = $type->changeVariances(array_map( + static fn (TemplateTypeVariance $variance) => $variance->invariant() ? TemplateTypeVariance::createCovariant() : $variance, + $type->getVariances(), + )); + } + + return $traverse($type); + }); + if ($type->isConstantArray()->yes()) { if ($type->isIterableAtLeastOnce()->no()) { $type = new ArrayType(new MixedType(), new MixedType()); diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 63a10119e8a..7ee37d6d413 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -403,6 +403,14 @@ protected function recreate(string $className, array $types, ?Type $subtractedTy ); } + /** + * @param TemplateTypeVariance[] $variances + */ + public function changeVariances(array $variances): self + { + return $this->recreate($this->getClassName(), $this->getTypes(), $this->getSubtractedType(), $variances); + } + public function changeSubtractedType(?Type $subtractedType): Type { $result = parent::changeSubtractedType($subtractedType); diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index 2fc2aaf3203..008f0ed3121 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -586,7 +586,7 @@ public function testBug12457(): void ]); } - public function testGenericSubtype(): void + public function testGenericSubtypeWithStrictCheck(): void { $this->checkTypeAgainstPhpDocType = true; $this->strictWideningCheck = true; @@ -601,6 +601,22 @@ public function testGenericSubtype(): void 131, 'Template type E on class GenericSubtype\IRepository is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], + [ + 'PHPDoc tag @var with type GenericSubtype\Collection is not subtype of type GenericSubtype\Collection.', + 162, + ], + ]); + } + + public function testGenericSubtypeWithoutStrictCheck(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = false; + $this->analyse([__DIR__ . '/data/generic-subtype.php'], [ + [ + 'PHPDoc tag @var with type GenericSubtype\Collection is not subtype of type GenericSubtype\Collection.', + 162, + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php b/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php index adc29e95b15..1bc8a174b41 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php @@ -146,3 +146,19 @@ protected function getTargetRepository(): IRepository */ protected function test($repository): void {} } + +/** @template T */ +class Collection +{ +} + +abstract class AlwaysFail +{ + /** @return Collection */ + abstract public function getCollection(): Collection; + + public function test(): void { + /** @var Collection $collection */ + $collection = $this->getCollection(); + } +}