Skip to content

Commit 3f82a55

Browse files
committed
Cannot unset property which might get hooked in subclass.
1 parent 19e1ff9 commit 3f82a55

File tree

3 files changed

+64
-3
lines changed

3 files changed

+64
-3
lines changed

src/Rules/Variables/UnsetRule.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Php\PhpVersion;
78
use PHPStan\Rules\IdentifierRuleError;
89
use PHPStan\Rules\Properties\PropertyReflectionFinder;
910
use PHPStan\Rules\Rule;
@@ -20,6 +21,7 @@ final class UnsetRule implements Rule
2021

2122
public function __construct(
2223
private PropertyReflectionFinder $propertyReflectionFinder,
24+
private PhpVersion $phpVersion,
2325
)
2426
{
2527
}
@@ -115,6 +117,23 @@ private function canBeUnset(Node $node, Scope $scope): ?IdentifierRuleError
115117
->line($node->getStartLine())
116118
->identifier('unset.hookedProperty')
117119
->build();
120+
} elseif ($this->phpVersion->supportsPropertyHooks()) {
121+
if (
122+
!$propertyReflection->isPrivate()
123+
&& !$propertyReflection->isFinal()->yes()
124+
&& !$propertyReflection->getDeclaringClass()->isFinal()
125+
) {
126+
return RuleErrorBuilder::message(
127+
sprintf(
128+
'Cannot unset %s::$%s property which might get hooked in subclass.',
129+
$propertyReflection->getDeclaringClass()->getDisplayName(),
130+
$foundPropertyReflection->getName(),
131+
),
132+
)
133+
->line($node->getStartLine())
134+
->identifier('unset.maybeHookedProperty')
135+
->build();
136+
}
118137
}
119138
}
120139

tests/PHPStan/Rules/Variables/UnsetRuleTest.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Rules\Variables;
44

5+
use PHPStan\Php\PhpVersion;
56
use PHPStan\Rules\Properties\PropertyReflectionFinder;
67
use PHPStan\Rules\Rule;
78
use PHPStan\Testing\RuleTestCase;
@@ -15,7 +16,10 @@ class UnsetRuleTest extends RuleTestCase
1516

1617
protected function getRule(): Rule
1718
{
18-
return new UnsetRule(self::getContainer()->getByType(PropertyReflectionFinder::class));
19+
return new UnsetRule(
20+
self::getContainer()->getByType(PropertyReflectionFinder::class),
21+
self::getContainer()->getByType(PhpVersion::class),
22+
);
1923
}
2024

2125
public function testUnsetRule(): void
@@ -56,7 +60,12 @@ public function testBug2752(): void
5660

5761
public function testBug4289(): void
5862
{
59-
$this->analyse([__DIR__ . '/data/bug-4289.php'], []);
63+
$this->analyse([__DIR__ . '/data/bug-4289.php'], [
64+
[
65+
'Cannot unset Bug4289\BaseClass::$fields property which might get hooked in subclass.',
66+
25,
67+
],
68+
]);
6069
}
6170

6271
public function testBug5223(): void
@@ -96,6 +105,10 @@ public function testBug4565(): void
96105
public function testBug12421(): void
97106
{
98107
$this->analyse([__DIR__ . '/data/bug-12421.php'], [
108+
[
109+
'Cannot unset Bug12421\RegularProperty::$y property which might get hooked in subclass.',
110+
7,
111+
],
99112
[
100113
'Cannot unset readonly Bug12421\NativeReadonlyClass::$y property.',
101114
11,
@@ -146,6 +159,10 @@ public function testUnsetHookedProperty(): void
146159
'Cannot unset hooked UnsetHookedProperty\Foo::$iii property.',
147160
10,
148161
],
162+
[
163+
'Cannot unset UnsetHookedProperty\NonFinalClass::$publicProperty property which might get hooked in subclass.',
164+
13,
165+
],
149166
]);
150167
}
151168

tests/PHPStan/Rules/Variables/data/unset-hooked-property.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
namespace UnsetHookedProperty;
44

5-
function doUnset(Foo $foo, User $user): void {
5+
function doUnset(Foo $foo, User $user, NonFinalClass $nonFinalClass, FinalClass $finalClass): void {
66
unset($user->name);
77
unset($user->fullName);
88

99
unset($foo->ii);
1010
unset($foo->iii);
11+
12+
unset($nonFinalClass->publicFinalProperty);
13+
unset($nonFinalClass->publicProperty);
14+
15+
unset($finalClass->publicFinalProperty);
16+
unset($finalClass->publicProperty);
1117
}
1218

1319
class User
@@ -39,3 +45,22 @@ abstract class Foo
3945
abstract public int $iii { get; }
4046
}
4147

48+
class NonFinalClass {
49+
private string $privateProperty;
50+
public string $publicProperty;
51+
final public string $publicFinalProperty;
52+
53+
function doFoo() {
54+
unset($this->privateProperty);
55+
}
56+
}
57+
58+
final class FinalClass {
59+
private string $privateProperty;
60+
public string $publicProperty;
61+
final public string $publicFinalProperty;
62+
63+
function doFoo() {
64+
unset($this->privateProperty);
65+
}
66+
}

0 commit comments

Comments
 (0)