From dbee96fa962d05df117b840b04ee9ac4578b608f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:26:46 +0000 Subject: [PATCH] Fix phpstan/phpstan#6139: False positive for covariant generic parameter - When a covariant generic type (e.g. Element with @template-covariant T) is used as a method parameter type, covariant template type arguments are no longer falsely reported as variance violations - Added findCovariantTemplatesInCovariantGeneric() in VarianceCheck to detect and skip these cases using getObjectClassReflections() and getTemplateType() - New regression test in tests/PHPStan/Rules/Generics/data/bug-6139.php - Updated existing covariant variance test expectations to reflect the fix --- src/Rules/Generics/VarianceCheck.php | 47 +++++++++++++++++++ .../MethodSignatureVarianceRuleTest.php | 13 ++--- .../PHPStan/Rules/Generics/data/bug-6139.php | 24 ++++++++++ 3 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Rules/Generics/data/bug-6139.php diff --git a/src/Rules/Generics/VarianceCheck.php b/src/Rules/Generics/VarianceCheck.php index cd8d7192bfa..99be0f1ac22 100644 --- a/src/Rules/Generics/VarianceCheck.php +++ b/src/Rules/Generics/VarianceCheck.php @@ -9,6 +9,8 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; +use function count; +use function in_array; use function sprintf; #[AutowiredService] @@ -85,6 +87,8 @@ public function check(TemplateTypeVariance $positionVariance, Type $type, string { $errors = []; + $skipTemplates = $this->findCovariantTemplatesInCovariantGeneric($positionVariance, $type); + foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) { $referredType = $reference->getType(); if (($referredType->getScope()->getFunctionName() !== null && !$referredType->getVariance()->invariant()) @@ -92,6 +96,10 @@ public function check(TemplateTypeVariance $positionVariance, Type $type, string continue; } + if (in_array($referredType, $skipTemplates, true)) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( 'Template type %s is declared as %s, but occurs in %s position %s.', $referredType->getName(), @@ -104,6 +112,45 @@ public function check(TemplateTypeVariance $positionVariance, Type $type, string return $errors; } + /** + * When a covariant generic type (e.g. Element where Element has @template-covariant) + * is used as a parameter type, covariant template type arguments should not be flagged. + * The covariant generic only produces values of type T, so using it in a parameter + * position does not create a true contravariant use of T. + * + * @return list + */ + private function findCovariantTemplatesInCovariantGeneric(TemplateTypeVariance $positionVariance, Type $type): array + { + if (!$positionVariance->contravariant()) { + return []; + } + + $classReflections = $type->getObjectClassReflections(); + if (count($classReflections) !== 1) { + return []; + } + + $classReflection = $classReflections[0]; + $templateTypeMap = $classReflection->getTemplateTypeMap(); + $skipTemplates = []; + + foreach ($templateTypeMap->getTypes() as $templateName => $templateType) { + if (!$templateType instanceof TemplateType || !$templateType->getVariance()->covariant()) { + continue; + } + + $resolvedType = $type->getTemplateType($classReflection->getName(), $templateName); + if (!($resolvedType instanceof TemplateType) || !$resolvedType->getVariance()->covariant()) { + continue; + } + + $skipTemplates[] = $resolvedType; + } + + return $skipTemplates; + } + private function isTemplateTypeVarianceValid(TemplateTypeVariance $positionVariance, TemplateType $type): bool { return $positionVariance->validPosition($type->getVariance()); diff --git a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php index 81392060c25..7537f4c3c34 100644 --- a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php @@ -47,10 +47,6 @@ public function testRule(): void 'Template type X is declared as covariant, but occurs in invariant position in parameter e of method MethodSignatureVariance\Covariant\C::a().', 35, ], - [ - 'Template type X is declared as covariant, but occurs in contravariant position in parameter f of method MethodSignatureVariance\Covariant\C::a().', - 35, - ], [ 'Template type X is declared as covariant, but occurs in contravariant position in parameter h of method MethodSignatureVariance\Covariant\C::a().', 35, @@ -187,10 +183,6 @@ public function testRule(): void 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\StaticMethod\B::a().', 43, ], - [ - 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\StaticMethod\B::a().', - 43, - ], [ 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\StaticMethod\B::c().', 49, @@ -235,6 +227,11 @@ public function testPr2465(): void ]); } + public function testBug6139(): void + { + $this->analyse([__DIR__ . '/data/bug-6139.php'], []); + } + #[RequiresPhp('>= 8.0')] public function testBug10609(): void { diff --git a/tests/PHPStan/Rules/Generics/data/bug-6139.php b/tests/PHPStan/Rules/Generics/data/bug-6139.php new file mode 100644 index 00000000000..fdf70f4ebde --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-6139.php @@ -0,0 +1,24 @@ + $element + */ + public function addElement(Element $element): void; +}