Skip to content

Commit 0df35db

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
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 phpstan/phpstan#12280
1 parent 1bbe9dc commit 0df35db

4 files changed

Lines changed: 90 additions & 0 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
8989
use PHPStan\Reflection\PropertyReflection;
9090
use PHPStan\Reflection\ReflectionProvider;
91+
use PHPStan\Reflection\Type\UnionTypePropertyReflection;
9192
use PHPStan\Rules\Properties\PropertyReflectionFinder;
9293
use PHPStan\ShouldNotHappenException;
9394
use PHPStan\TrinaryLogic;
@@ -4897,12 +4898,32 @@ public function canAccessProperty(PropertyReflection $propertyReflection): bool
48974898
/** @api */
48984899
public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool
48994900
{
4901+
if ($propertyReflection instanceof UnionTypePropertyReflection) {
4902+
foreach ($propertyReflection->getProperties() as $innerProperty) {
4903+
if (!$this->canReadProperty($innerProperty)) {
4904+
return false;
4905+
}
4906+
}
4907+
4908+
return true;
4909+
}
4910+
49004911
return $this->canAccessClassMember($propertyReflection);
49014912
}
49024913

49034914
/** @api */
49044915
public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool
49054916
{
4917+
if ($propertyReflection instanceof UnionTypePropertyReflection) {
4918+
foreach ($propertyReflection->getProperties() as $innerProperty) {
4919+
if (!$this->canWriteProperty($innerProperty)) {
4920+
return false;
4921+
}
4922+
}
4923+
4924+
return true;
4925+
}
4926+
49064927
if (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) {
49074928
return $this->canAccessClassMember($propertyReflection);
49084929
}

src/Reflection/Type/UnionTypePropertyReflection.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ public function __construct(private array $properties)
2323
{
2424
}
2525

26+
/**
27+
* @return ExtendedPropertyReflection[]
28+
*/
29+
public function getProperties(): array
30+
{
31+
return $this->properties;
32+
}
33+
2634
public function getName(): string
2735
{
2836
return $this->properties[0]->getName();

tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,4 +1246,12 @@ public function testBug13537(): void
12461246
$this->analyse([__DIR__ . '/data/bug-13537.php'], $errors);
12471247
}
12481248

1249+
public function testBug12280(): void
1250+
{
1251+
$this->checkThisOnly = false;
1252+
$this->checkUnionTypes = true;
1253+
$this->checkDynamicProperties = false;
1254+
$this->analyse([__DIR__ . '/data/bug-12280.php'], []);
1255+
}
1256+
12491257
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12280;
6+
7+
final readonly class A
8+
{
9+
public function __construct(
10+
public \DateTime $date,
11+
) {}
12+
}
13+
14+
class B
15+
{
16+
public function __construct(
17+
private \DateTime $date,
18+
) {}
19+
20+
/**
21+
* @param list<A> $a
22+
* @param list<B> $b
23+
* @return list<\DateTime>
24+
*/
25+
public static function test1(array $a, array $b): array
26+
{
27+
$getDate = static function(A|self $value): \DateTime {
28+
return $value->date;
29+
};
30+
31+
return [
32+
...array_map($getDate(...), $a),
33+
...array_map($getDate(...), $b),
34+
];
35+
}
36+
37+
/**
38+
* @param list<A> $a
39+
* @param list<B> $b
40+
* @return list<\DateTime>
41+
*/
42+
public static function test2(array $a, array $b): array
43+
{
44+
$getDate = static function(self|A $value): \DateTime {
45+
return $value->date;
46+
};
47+
48+
return [
49+
...array_map($getDate(...), $a),
50+
...array_map($getDate(...), $b),
51+
];
52+
}
53+
}

0 commit comments

Comments
 (0)