Skip to content

Commit c5c5839

Browse files
committed
Hooked properties can throw custom exceptions
1 parent 4ba8fcb commit c5c5839

8 files changed

+504
-3
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
152152
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
153153
use PHPStan\Reflection\Php\PhpMethodReflection;
154+
use PHPStan\Reflection\Php\PhpPropertyReflection;
154155
use PHPStan\Reflection\ReflectionProvider;
155156
use PHPStan\Reflection\SignatureMap\SignatureMapProvider;
156157
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
@@ -2973,6 +2974,7 @@ static function (): void {
29732974
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
29742975
$impurePoints = array_merge($impurePoints, $result->getImpurePoints());
29752976
} elseif ($expr instanceof PropertyFetch) {
2977+
$scopeBeforeVar = $scope;
29762978
$result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep());
29772979
$hasYield = $result->hasYield();
29782980
$throwPoints = $result->getThrowPoints();
@@ -2984,6 +2986,20 @@ static function (): void {
29842986
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
29852987
$impurePoints = array_merge($impurePoints, $result->getImpurePoints());
29862988
$scope = $result->getScope();
2989+
if ($this->phpVersion->supportsPropertyHooks()) {
2990+
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2991+
}
2992+
} else {
2993+
$propertyName = $expr->name->toString();
2994+
$propertyHolderType = $scopeBeforeVar->getType($expr->var);
2995+
$propertyReflection = $scopeBeforeVar->getPropertyReflection($propertyHolderType, $propertyName);
2996+
if ($propertyReflection !== null) {
2997+
$propertyDeclaringClass = $propertyReflection->getDeclaringClass();
2998+
if ($propertyDeclaringClass->hasNativeProperty($propertyName)) {
2999+
$nativeProperty = $propertyDeclaringClass->getNativeProperty($propertyName);
3000+
$throwPoints = array_merge($throwPoints, $this->getPropertyReadThrowPointsFromGetHook($scopeBeforeVar, $expr, $nativeProperty));
3001+
}
3002+
}
29873003
}
29883004
} elseif ($expr instanceof Expr\NullsafePropertyFetch) {
29893005
$nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var);
@@ -4224,6 +4240,83 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, P
42244240
return null;
42254241
}
42264242

4243+
/**
4244+
* @return ThrowPoint[]
4245+
*/
4246+
private function getPropertyReadThrowPointsFromGetHook(
4247+
MutatingScope $scope,
4248+
PropertyFetch $propertyFetch,
4249+
PhpPropertyReflection $propertyReflection,
4250+
): array
4251+
{
4252+
return $this->getThrowPointsFromPropertyHook($scope, $propertyFetch, $propertyReflection, 'get');
4253+
}
4254+
4255+
/**
4256+
* @return ThrowPoint[]
4257+
*/
4258+
private function getPropertyAssignThrowPointsFromSetHook(
4259+
MutatingScope $scope,
4260+
PropertyFetch $propertyFetch,
4261+
PhpPropertyReflection $propertyReflection,
4262+
): array
4263+
{
4264+
return $this->getThrowPointsFromPropertyHook($scope, $propertyFetch, $propertyReflection, 'set');
4265+
}
4266+
4267+
/**
4268+
* @param 'get'|'set' $hookName
4269+
* @return ThrowPoint[]
4270+
*/
4271+
private function getThrowPointsFromPropertyHook(
4272+
MutatingScope $scope,
4273+
PropertyFetch $propertyFetch,
4274+
PhpPropertyReflection $propertyReflection,
4275+
string $hookName,
4276+
): array
4277+
{
4278+
$scopeFunction = $scope->getFunction();
4279+
if (
4280+
$scopeFunction instanceof PhpMethodFromParserNodeReflection
4281+
&& $scopeFunction->isPropertyHook()
4282+
&& $propertyFetch->var instanceof Variable
4283+
&& $propertyFetch->var->name === 'this'
4284+
&& $propertyFetch->name instanceof Identifier
4285+
&& $propertyFetch->name->toString() === $scopeFunction->getHookedPropertyName()
4286+
) {
4287+
return [];
4288+
}
4289+
$declaringClass = $propertyReflection->getDeclaringClass();
4290+
if (!$propertyReflection->hasHook($hookName)) {
4291+
if (
4292+
$propertyReflection->isPrivate()
4293+
|| $propertyReflection->isFinal()->yes()
4294+
|| $declaringClass->isFinal()
4295+
) {
4296+
return [];
4297+
}
4298+
4299+
if ($this->implicitThrows) {
4300+
return [ThrowPoint::createImplicit($scope, $propertyFetch)];
4301+
}
4302+
4303+
return [];
4304+
}
4305+
4306+
$getHook = $propertyReflection->getHook($hookName);
4307+
$throwType = $getHook->getThrowType();
4308+
4309+
if ($throwType !== null) {
4310+
if (!$throwType->isVoid()->yes()) {
4311+
return [ThrowPoint::createExplicit($scope, $throwType, $propertyFetch, true)];
4312+
}
4313+
} elseif ($this->implicitThrows) {
4314+
return [ThrowPoint::createImplicit($scope, $propertyFetch)];
4315+
}
4316+
4317+
return [];
4318+
}
4319+
42274320
/**
42284321
* @return string[]
42294322
*/
@@ -5408,6 +5501,10 @@ static function (): void {
54085501
$impurePoints = array_merge($impurePoints, $result->getImpurePoints());
54095502
$scope = $result->getScope();
54105503

5504+
if ($var->name instanceof Expr && $this->phpVersion->supportsPropertyHooks()) {
5505+
$throwPoints[] = ThrowPoint::createImplicit($scope, $var);
5506+
}
5507+
54115508
$propertyHolderType = $scope->getType($var->var);
54125509
if ($propertyName !== null && $propertyHolderType->hasProperty($propertyName)->yes()) {
54135510
$propertyReflection = $propertyHolderType->getProperty($propertyName, $scope);
@@ -5424,6 +5521,9 @@ static function (): void {
54245521
) {
54255522
$throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false);
54265523
}
5524+
if ($this->phpVersion->supportsPropertyHooks()) {
5525+
$throwPoints = array_merge($throwPoints, $this->getPropertyAssignThrowPointsFromSetHook($scope, $var, $nativeProperty));
5526+
}
54275527
if ($enterExpressionAssign) {
54285528
$scope = $scope->assignInitializedProperty($propertyHolderType, $propertyName);
54295529
}

tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
77
use function array_merge;
8+
use const PHP_VERSION_ID;
89

910
/**
1011
* @extends RuleTestCase<CatchWithUnthrownExceptionRule>
@@ -33,6 +34,44 @@ public function testRule(): void
3334
]);
3435
}
3536

37+
public function testPropertyHooks(): void
38+
{
39+
if (PHP_VERSION_ID < 80400) {
40+
$this->markTestSkipped('Test requires PHP 8.4.');
41+
}
42+
43+
$this->analyse([__DIR__ . '/data/unthrown-exception-property-hooks-implicit-throws-disabled.php'], [
44+
[
45+
'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.',
46+
23,
47+
],
48+
[
49+
'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.',
50+
38,
51+
],
52+
[
53+
'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.',
54+
53,
55+
],
56+
[
57+
'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.',
58+
68,
59+
],
60+
[
61+
'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.',
62+
74,
63+
],
64+
[
65+
'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.',
66+
94,
67+
],
68+
[
69+
'Dead catch - UnthrownExceptionPropertyHooksImplicitThrowsDisabled\MyCustomException is never thrown in the try block.',
70+
115,
71+
],
72+
]);
73+
}
74+
3675
public static function getAdditionalConfigFiles(): array
3776
{
3877
return array_merge(

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,4 +612,46 @@ public function testBug9568(): void
612612
$this->analyse([__DIR__ . '/data/bug-9568.php'], []);
613613
}
614614

615+
public function testPropertyHooks(): void
616+
{
617+
if (PHP_VERSION_ID < 80400) {
618+
self::markTestSkipped('Test requires PHP 8.4.');
619+
}
620+
621+
$this->analyse([__DIR__ . '/data/unthrown-exception-property-hooks.php'], [
622+
[
623+
'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.',
624+
27,
625+
],
626+
[
627+
'Dead catch - UnthrownExceptionPropertyHooks\SomeException is never thrown in the try block.',
628+
39,
629+
],
630+
[
631+
'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.',
632+
53,
633+
],
634+
[
635+
'Dead catch - UnthrownExceptionPropertyHooks\SomeException is never thrown in the try block.',
636+
65,
637+
],
638+
[
639+
'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.',
640+
107,
641+
],
642+
[
643+
'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.',
644+
128,
645+
],
646+
[
647+
'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.',
648+
154,
649+
],
650+
[
651+
'Dead catch - UnthrownExceptionPropertyHooks\MyCustomException is never thrown in the try block.',
652+
175,
653+
],
654+
]);
655+
}
656+
615657
}

tests/PHPStan/Rules/Exceptions/data/bug-5903.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Bug5903;
44

5-
class Test
5+
final class Test
66
{
77
/** @var \Traversable<string> */
88
protected $traversable;

tests/PHPStan/Rules/Exceptions/data/bug-6791.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Bug6791;
44

5-
class Foo {
5+
final class Foo {
66
/** @var int[] */
77
public array $intArray;
88
/** @var \Ds\Set<int> */

tests/PHPStan/Rules/Exceptions/data/union-type-error.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace UnionTypeError;
66

7-
class Foo {
7+
final class Foo {
88
public string|int $stringOrInt;
99
public string|array $stringOrArray;
1010

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php // lint >= 8.4
2+
3+
namespace UnthrownExceptionPropertyHooksImplicitThrowsDisabled;
4+
5+
class MyCustomException extends \Exception
6+
{
7+
8+
}
9+
10+
class SomeException extends \Exception
11+
{
12+
13+
}
14+
15+
class Foo
16+
{
17+
public int $i;
18+
19+
public function doFoo(): void
20+
{
21+
try {
22+
echo $this->i;
23+
} catch (MyCustomException) { // unthrown - implicit @throws disabled
24+
25+
}
26+
}
27+
28+
public int $k {
29+
get {
30+
return 1;
31+
}
32+
}
33+
34+
public function doBaz(): void
35+
{
36+
try {
37+
echo $this->k;
38+
} catch (MyCustomException) { // unthrown - implicit @throws disabled
39+
40+
}
41+
}
42+
43+
private int $l {
44+
get {
45+
return $this->l;
46+
}
47+
}
48+
49+
public function doLorem(): void
50+
{
51+
try {
52+
echo $this->l;
53+
} catch (MyCustomException) { // unthrown - implicit @throws disabled
54+
55+
}
56+
}
57+
58+
final public int $m {
59+
get {
60+
return $this->m;
61+
}
62+
}
63+
64+
public function doIpsum(): void
65+
{
66+
try {
67+
echo $this->m;
68+
} catch (MyCustomException) { // unthrown - implicit @throws disabled
69+
70+
}
71+
72+
try {
73+
$this->m = 1;
74+
} catch (MyCustomException) { // unthrown - set hook does not exist
75+
76+
}
77+
}
78+
79+
}
80+
81+
final class FinalFoo
82+
{
83+
84+
public int $m {
85+
get {
86+
return $this->m;
87+
}
88+
}
89+
90+
public function doIpsum(): void
91+
{
92+
try {
93+
echo $this->m;
94+
} catch (MyCustomException) { // unthrown - implicit @throws disabled
95+
96+
}
97+
}
98+
99+
}
100+
101+
class ThrowsVoid
102+
{
103+
104+
public int $m {
105+
/** @throws void */
106+
get {
107+
return $this->m;
108+
}
109+
}
110+
111+
public function doIpsum(): void
112+
{
113+
try {
114+
echo $this->m;
115+
} catch (MyCustomException) { // unthrown
116+
117+
}
118+
}
119+
120+
}

0 commit comments

Comments
 (0)