From c72d0f36485b974ce054aef6de57a2b7d0e98621 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 9 Apr 2026 22:57:00 +0200 Subject: [PATCH] Do not report generic covariant issue without strict check --- src/Rules/PhpDoc/VarTagTypeRuleHelper.php | 14 ++++++++++++++ src/Type/Generic/GenericObjectType.php | 8 ++++++++ .../PhpDoc/WrongVariableNameInVarTagRuleTest.php | 9 ++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) 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..7b14fe36123 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; @@ -604,6 +604,13 @@ public function testGenericSubtype(): void ]); } + public function testGenericSubtypeWithoutStrictCheck(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = false; + $this->analyse([__DIR__ . '/data/generic-subtype.php'], []); + } + public function testNewIsAlwaysFinalClass(): void { $this->checkTypeAgainstPhpDocType = true;