Skip to content

Commit e0a6b9a

Browse files
committed
ThrowsVoidPropertyHookWithExplicitThrowPointRule - level 3
1 parent 691994e commit e0a6b9a

File tree

4 files changed

+212
-0
lines changed

4 files changed

+212
-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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<ThrowsVoidPropertyHookWithExplicitThrowPointRule>
11+
*/
12+
class ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest extends RuleTestCase
13+
{
14+
15+
private bool $missingCheckedExceptionInThrows;
16+
17+
/** @var string[] */
18+
private array $checkedExceptionClasses;
19+
20+
protected function getRule(): Rule
21+
{
22+
return new ThrowsVoidPropertyHookWithExplicitThrowPointRule(new DefaultExceptionTypeResolver(
23+
$this->createReflectionProvider(),
24+
[],
25+
[],
26+
[],
27+
$this->checkedExceptionClasses,
28+
), $this->missingCheckedExceptionInThrows);
29+
}
30+
31+
public function dataRule(): array
32+
{
33+
return [
34+
[
35+
true,
36+
[],
37+
[],
38+
],
39+
[
40+
false,
41+
['DifferentException'],
42+
[
43+
[
44+
'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.',
45+
18,
46+
],
47+
],
48+
],
49+
[
50+
true,
51+
['ThrowsVoidPropertyHook\\MyException'],
52+
[],
53+
],
54+
[
55+
true,
56+
['DifferentException'],
57+
[
58+
[
59+
'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.',
60+
18,
61+
],
62+
],
63+
],
64+
[
65+
false,
66+
[],
67+
[
68+
[
69+
'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.',
70+
18,
71+
],
72+
],
73+
],
74+
[
75+
false,
76+
['ThrowsVoidPropertyHook\\MyException'],
77+
[
78+
[
79+
'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.',
80+
18,
81+
],
82+
],
83+
],
84+
];
85+
}
86+
87+
/**
88+
* @dataProvider dataRule
89+
* @param string[] $checkedExceptionClasses
90+
* @param list<array{0: string, 1: int, 2?: string}> $errors
91+
*/
92+
public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void
93+
{
94+
if (PHP_VERSION_ID < 80400) {
95+
$this->markTestSkipped('Test requires PHP 8.4.');
96+
}
97+
98+
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
99+
$this->checkedExceptionClasses = $checkedExceptionClasses;
100+
$this->analyse([__DIR__ . '/data/throws-void-property-hook.php'], $errors);
101+
}
102+
103+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php // lint >= 8.4
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)