From 46927a839bff076335b7d3177d944d6b7aa547d5 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:56:46 +0000 Subject: [PATCH] Fix phpstan/phpstan#12964: Support covariant templates in property hooks and asymmetric visibility - Treat properties with only a get hook (no set hook) as covariant position - Treat properties with private(set) or protected(set) as covariant position - New regression test in tests/PHPStan/Rules/Generics/data/bug-12964.php --- src/Rules/Generics/PropertyVarianceRule.php | 22 ++- .../Generics/PropertyVarianceRuleTest.php | 59 ++++++++ .../PHPStan/Rules/Generics/data/bug-12964.php | 137 ++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Generics/data/bug-12964.php diff --git a/src/Rules/Generics/PropertyVarianceRule.php b/src/Rules/Generics/PropertyVarianceRule.php index 03121823d39..f2a71a74ad9 100644 --- a/src/Rules/Generics/PropertyVarianceRule.php +++ b/src/Rules/Generics/PropertyVarianceRule.php @@ -45,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $variance = $node->isReadOnly() || $node->isReadOnlyByPhpDoc() + $variance = $node->isReadOnly() || $node->isReadOnlyByPhpDoc() || $this->isEffectivelyReadOnly($node) ? TemplateTypeVariance::createCovariant() : TemplateTypeVariance::createInvariant(); @@ -56,4 +56,24 @@ public function processNode(Node $node, Scope $scope): array ); } + private function isEffectivelyReadOnly(ClassPropertyNode $node): bool + { + if ($node->isPrivateSet() || $node->isProtectedSet()) { + return true; + } + + $hooks = $node->getHooks(); + if ($hooks === []) { + return false; + } + + foreach ($hooks as $hook) { + if ($hook->name->name === 'set') { + return false; + } + } + + return true; + } + } diff --git a/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php index b5aeefa8e96..ac5b313a137 100644 --- a/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php @@ -137,4 +137,63 @@ public function testBug13049(): void $this->analyse([__DIR__ . '/data/bug-13049.php'], []); } + #[RequiresPhp('>= 8.4')] + public function testBug12964(): void + { + $this->analyse([__DIR__ . '/data/bug-12964.php'], [ + [ + 'Template type X is declared as covariant, but occurs in contravariant position in property Bug12964\C::$b.', + 51, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property Bug12964\C::$d.', + 57, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property Bug12964\D::$a.', + 65, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property Bug12964\D::$c.', + 71, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property Bug12964\D::$d.', + 74, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in property Bug12964\E::$b.', + 85, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property Bug12964\E::$d.', + 91, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in property Bug12964\F::$b.', + 103, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property Bug12964\F::$d.', + 109, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property Bug12964\G::$a.', + 118, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property Bug12964\G::$c.', + 124, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property Bug12964\G::$d.', + 127, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property Bug12964\H::$a.', + 136, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generics/data/bug-12964.php b/tests/PHPStan/Rules/Generics/data/bug-12964.php new file mode 100644 index 00000000000..74c507b75a5 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-12964.php @@ -0,0 +1,137 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug12964; + +/** @template-contravariant T */ +interface In { +} + +/** @template-covariant T */ +interface Out { +} + +/** @template T */ +interface Invariant { +} + +/** + * @template-covariant T + */ +interface A +{ + /** + * @var T + */ + public mixed $b { get; } +} + +/** + * @template-covariant T + */ +final class B +{ + /** + * @param T $data + */ + public function __construct( + public private(set) mixed $data, + ) {} +} + +/** + * @template-covariant X + */ +class C { + /** @var X */ + public private(set) mixed $a; + + /** @var In */ + public private(set) mixed $b; + + /** @var Out */ + public private(set) mixed $c; + + /** @var Invariant */ + public private(set) mixed $d; +} + +/** + * @template-contravariant X + */ +class D { + /** @var X */ + public private(set) mixed $a; + + /** @var In */ + public private(set) mixed $b; + + /** @var Out */ + public private(set) mixed $c; + + /** @var Invariant */ + public private(set) mixed $d; +} + +/** + * @template-covariant X + */ +class E { + /** @var X */ + public protected(set) mixed $a; + + /** @var In */ + public protected(set) mixed $b; + + /** @var Out */ + public protected(set) mixed $c; + + /** @var Invariant */ + public protected(set) mixed $d; +} + +/** + * @template-covariant X + */ +interface F +{ + /** @var X */ + public mixed $a { get; } + + /** @var In */ + public mixed $b { get; } + + /** @var Out */ + public mixed $c { get; } + + /** @var Invariant */ + public mixed $d { get; } +} + +/** + * @template-contravariant X + */ +interface G +{ + /** @var X */ + public mixed $a { get; } + + /** @var In */ + public mixed $b { get; } + + /** @var Out */ + public mixed $c { get; } + + /** @var Invariant */ + public mixed $d { get; } +} + +/** + * @template-covariant X + */ +interface H +{ + /** @var X */ + public mixed $a { get; set; } +}