diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a26a86a066..4ef3c14e30 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -88,6 +88,7 @@ use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\Type\UnionTypePropertyReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -4897,12 +4898,32 @@ public function canAccessProperty(PropertyReflection $propertyReflection): bool /** @api */ public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool { + if ($propertyReflection instanceof UnionTypePropertyReflection) { + foreach ($propertyReflection->getProperties() as $innerProperty) { + if (!$this->canReadProperty($innerProperty)) { + return false; + } + } + + return true; + } + return $this->canAccessClassMember($propertyReflection); } /** @api */ public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool { + if ($propertyReflection instanceof UnionTypePropertyReflection) { + foreach ($propertyReflection->getProperties() as $innerProperty) { + if (!$this->canWriteProperty($innerProperty)) { + return false; + } + } + + return true; + } + if (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) { return $this->canAccessClassMember($propertyReflection); } diff --git a/src/Reflection/Type/UnionTypePropertyReflection.php b/src/Reflection/Type/UnionTypePropertyReflection.php index 33eb2a6824..76dbc28905 100644 --- a/src/Reflection/Type/UnionTypePropertyReflection.php +++ b/src/Reflection/Type/UnionTypePropertyReflection.php @@ -23,6 +23,14 @@ public function __construct(private array $properties) { } + /** + * @return ExtendedPropertyReflection[] + */ + public function getProperties(): array + { + return $this->properties; + } + public function getName(): string { return $this->properties[0]->getName(); diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index e5e2edc4d8..ba60307a93 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -1246,4 +1246,12 @@ public function testBug13537(): void $this->analyse([__DIR__ . '/data/bug-13537.php'], $errors); } + public function testBug12280(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-12280.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-12280.php b/tests/PHPStan/Rules/Properties/data/bug-12280.php new file mode 100644 index 0000000000..0441ae7379 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12280.php @@ -0,0 +1,53 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug12280; + +final readonly class A +{ + public function __construct( + public \DateTime $date, + ) {} +} + +class B +{ + public function __construct( + private \DateTime $date, + ) {} + + /** + * @param list $a + * @param list $b + * @return list<\DateTime> + */ + public static function test1(array $a, array $b): array + { + $getDate = static function(A|self $value): \DateTime { + return $value->date; + }; + + return [ + ...array_map($getDate(...), $a), + ...array_map($getDate(...), $b), + ]; + } + + /** + * @param list $a + * @param list $b + * @return list<\DateTime> + */ + public static function test2(array $a, array $b): array + { + $getDate = static function(self|A $value): \DateTime { + return $value->date; + }; + + return [ + ...array_map($getDate(...), $a), + ...array_map($getDate(...), $b), + ]; + } +}