Skip to content

Commit 677e33e

Browse files
committed
ThrowsVoidPropertyHookWithExplicitThrowPointRule - level 3
1 parent 00a45b4 commit 677e33e

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed

conf/config.level3.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ services:
6969
tags:
7070
- phpstan.rules.rule
7171

72+
-
73+
class: PHPStan\Rules\Exceptions\ThrowsVoidPropertyHookWithExplicitThrowPointRule
74+
arguments:
75+
exceptionTypeResolver: @exceptionTypeResolver
76+
missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows%
77+
tags:
78+
- phpstan.rules.rule
79+
7280
-
7381
class: PHPStan\Rules\Generators\YieldFromTypeRule
7482
arguments:
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 PHPStan\TrinaryLogic;
12+
use PHPStan\Type\TypeUtils;
13+
use PHPStan\Type\VerbosityLevel;
14+
use function sprintf;
15+
use function ucfirst;
16+
17+
/**
18+
* @implements Rule<PropertyHookReturnStatementsNode>
19+
*/
20+
final class ThrowsVoidPropertyHookWithExplicitThrowPointRule implements Rule
21+
{
22+
23+
public function __construct(
24+
private ExceptionTypeResolver $exceptionTypeResolver,
25+
private bool $missingCheckedExceptionInThrows,
26+
)
27+
{
28+
}
29+
30+
public function getNodeType(): string
31+
{
32+
return PropertyHookReturnStatementsNode::class;
33+
}
34+
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
$statementResult = $node->getStatementResult();
38+
$hookReflection = $node->getHookReflection();
39+
40+
if ($hookReflection->getThrowType() === null || !$hookReflection->getThrowType()->isVoid()->yes()) {
41+
return [];
42+
}
43+
44+
if ($hookReflection->getPropertyHookName() === null) {
45+
throw new ShouldNotHappenException();
46+
}
47+
48+
$errors = [];
49+
foreach ($statementResult->getThrowPoints() as $throwPoint) {
50+
if (!$throwPoint->isExplicit()) {
51+
continue;
52+
}
53+
54+
foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) {
55+
$isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd(
56+
$throwPointType->getObjectClassNames(),
57+
fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())),
58+
);
59+
if ($isCheckedException->yes()) {
60+
continue;
61+
}
62+
63+
$errors[] = RuleErrorBuilder::message(sprintf(
64+
'%s hook for property %s::$%s throws exception %s but the PHPDoc contains @throws void.',
65+
ucfirst($hookReflection->getPropertyHookName()),
66+
$hookReflection->getDeclaringClass()->getDisplayName(),
67+
$hookReflection->getHookedPropertyName(),
68+
$throwPointType->describe(VerbosityLevel::typeOnly()),
69+
))
70+
->line($throwPoint->getNode()->getStartLine())
71+
->identifier('throws.void')
72+
->build();
73+
}
74+
}
75+
76+
return $errors;
77+
}
78+
79+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use ThrowsVoidPropertyHook\MyException;
8+
use const PHP_VERSION_ID;
9+
10+
/**
11+
* @extends RuleTestCase<ThrowsVoidPropertyHookWithExplicitThrowPointRule>
12+
*/
13+
class ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest extends RuleTestCase
14+
{
15+
16+
private bool $missingCheckedExceptionInThrows;
17+
18+
/** @var string[] */
19+
private array $checkedExceptionClasses;
20+
21+
protected function getRule(): Rule
22+
{
23+
return new ThrowsVoidPropertyHookWithExplicitThrowPointRule(new DefaultExceptionTypeResolver(
24+
$this->createReflectionProvider(),
25+
[],
26+
[],
27+
[],
28+
$this->checkedExceptionClasses,
29+
), $this->missingCheckedExceptionInThrows);
30+
}
31+
32+
public function dataRule(): array
33+
{
34+
return [
35+
[
36+
true,
37+
[],
38+
[],
39+
],
40+
[
41+
false,
42+
['DifferentException'],
43+
[
44+
[
45+
'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.',
46+
18,
47+
],
48+
],
49+
],
50+
[
51+
true,
52+
[MyException::class],
53+
[],
54+
],
55+
[
56+
true,
57+
['DifferentException'],
58+
[
59+
[
60+
'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.',
61+
18,
62+
],
63+
],
64+
],
65+
[
66+
false,
67+
[],
68+
[
69+
[
70+
'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.',
71+
18,
72+
],
73+
],
74+
],
75+
[
76+
false,
77+
[MyException::class],
78+
[
79+
[
80+
'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.',
81+
18,
82+
],
83+
],
84+
],
85+
];
86+
}
87+
88+
/**
89+
* @dataProvider dataRule
90+
* @param string[] $checkedExceptionClasses
91+
* @param list<array{0: string, 1: int, 2?: string}> $errors
92+
*/
93+
public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void
94+
{
95+
if (PHP_VERSION_ID < 80400) {
96+
$this->markTestSkipped('Test requires PHP 8.4.');
97+
}
98+
99+
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
100+
$this->checkedExceptionClasses = $checkedExceptionClasses;
101+
$this->analyse([__DIR__ . '/data/throws-void-property-hook.php'], $errors);
102+
}
103+
104+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace ThrowsVoidPropertyHook;
4+
5+
class MyException extends \Exception
6+
{
7+
8+
}
9+
10+
class Foo
11+
{
12+
13+
public int $i {
14+
/**
15+
* @throws void
16+
*/
17+
get {
18+
throw new MyException();
19+
}
20+
}
21+
22+
}

0 commit comments

Comments
 (0)