Skip to content

Commit 31cfe22

Browse files
committed
MissingCheckedExceptionInPropertyHookThrowsRule
1 parent e0a6b9a commit 31cfe22

File tree

4 files changed

+153
-0
lines changed

4 files changed

+153
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ conditionalTags:
209209
phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows%
210210
PHPStan\Rules\Exceptions\MissingCheckedExceptionInMethodThrowsRule:
211211
phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows%
212+
PHPStan\Rules\Exceptions\MissingCheckedExceptionInPropertyHookThrowsRule:
213+
phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows%
212214

213215
services:
214216
-
@@ -906,6 +908,9 @@ services:
906908
-
907909
class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInMethodThrowsRule
908910

911+
-
912+
class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInPropertyHookThrowsRule
913+
909914
-
910915
class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInThrowsCheck
911916
arguments:
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\PropertyHookReturnStatementsNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\ShouldNotHappenException;
11+
use function sprintf;
12+
use function ucfirst;
13+
14+
/**
15+
* @implements Rule<PropertyHookReturnStatementsNode>
16+
*/
17+
final class MissingCheckedExceptionInPropertyHookThrowsRule implements Rule
18+
{
19+
20+
public function __construct(private MissingCheckedExceptionInThrowsCheck $check)
21+
{
22+
}
23+
24+
public function getNodeType(): string
25+
{
26+
return PropertyHookReturnStatementsNode::class;
27+
}
28+
29+
public function processNode(Node $node, Scope $scope): array
30+
{
31+
$statementResult = $node->getStatementResult();
32+
$hookReflection = $node->getHookReflection();
33+
34+
if (!$hookReflection->isPropertyHook()) {
35+
throw new ShouldNotHappenException();
36+
}
37+
38+
$errors = [];
39+
foreach ($this->check->check($hookReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) {
40+
$errors[] = RuleErrorBuilder::message(sprintf(
41+
'%s hook for property %s::$%s throws checked exception %s but it\'s missing from the PHPDoc @throws tag.',
42+
ucfirst($hookReflection->getPropertyHookName()),
43+
$hookReflection->getDeclaringClass()->getDisplayName(),
44+
$hookReflection->getHookedPropertyName(),
45+
$className,
46+
))
47+
->line($throwPointNode->getStartLine())
48+
->identifier('missingType.checkedException')
49+
->build();
50+
}
51+
52+
return $errors;
53+
}
54+
55+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\ShouldNotHappenException;
7+
use PHPStan\Testing\RuleTestCase;
8+
use const PHP_VERSION_ID;
9+
10+
/**
11+
* @extends RuleTestCase<MissingCheckedExceptionInPropertyHookThrowsRule>
12+
*/
13+
class MissingCheckedExceptionInPropertyHookThrowsRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new MissingCheckedExceptionInPropertyHookThrowsRule(
19+
new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver(
20+
$this->createReflectionProvider(),
21+
[],
22+
[ShouldNotHappenException::class],
23+
[],
24+
[],
25+
)),
26+
);
27+
}
28+
29+
public function testRule(): void
30+
{
31+
if (PHP_VERSION_ID < 80400) {
32+
$this->markTestSkipped('Test requires PHP 8.4.');
33+
}
34+
35+
$this->analyse([__DIR__ . '/data/missing-exception-property-hook-throws.php'], [
36+
[
37+
'Get hook for property MissingExceptionPropertyHookThrows\Foo::$k throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.',
38+
25,
39+
],
40+
[
41+
'Set hook for property MissingExceptionPropertyHookThrows\Foo::$l throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.',
42+
32,
43+
],
44+
[
45+
'Get hook for property MissingExceptionPropertyHookThrows\Foo::$m throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.',
46+
38,
47+
],
48+
]);
49+
}
50+
51+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php // lint >= 8.4
2+
3+
namespace MissingExceptionPropertyHookThrows;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
/** @throws \InvalidArgumentException */
10+
get {
11+
throw new \InvalidArgumentException(); // ok
12+
}
13+
}
14+
15+
public int $j {
16+
/** @throws \LogicException */
17+
set {
18+
throw new \InvalidArgumentException(); // ok
19+
}
20+
}
21+
22+
public int $k {
23+
/** @throws \RuntimeException */
24+
get {
25+
throw new \InvalidArgumentException(); // error
26+
}
27+
}
28+
29+
public int $l {
30+
/** @throws \RuntimeException */
31+
set {
32+
throw new \InvalidArgumentException(); // error
33+
}
34+
}
35+
36+
public int $m {
37+
get {
38+
throw new \InvalidArgumentException(); // error
39+
}
40+
}
41+
42+
}

0 commit comments

Comments
 (0)