From 0df35dbf6f9bc25c0d5f4d9c522ad02a6c407128 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 13:58:51 +0000 Subject: [PATCH] Fix property access check on union types with mixed visibilities - UnionTypePropertyReflection's isPublic()/isPrivate() use AND semantics, returning false when union members have different visibilities - canAccessClassMember then fell through to the "protected" branch with an order-dependent getDeclaringClass(), causing false positives - Fix canReadProperty/canWriteProperty in MutatingScope to check each inner property individually when given a UnionTypePropertyReflection - Added getProperties() getter to UnionTypePropertyReflection - New regression test in tests/PHPStan/Rules/Properties/data/bug-12280.php Closes https://github.com/phpstan/phpstan/issues/12280 --- src/Analyser/MutatingScope.php | 21 ++++++++ .../Type/UnionTypePropertyReflection.php | 8 +++ .../Properties/AccessPropertiesRuleTest.php | 8 +++ .../Rules/Properties/data/bug-12280.php | 53 +++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-12280.php 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), + ]; + } +}