Skip to content

Commit 691994e

Browse files
committed
TooWidePropertyHookThrowTypeRule - level 4
1 parent c5c5839 commit 691994e

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed

conf/config.level4.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ conditionalTags:
3131
phpstan.rules.rule: %exceptions.check.tooWideThrowType%
3232
PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule:
3333
phpstan.rules.rule: %exceptions.check.tooWideThrowType%
34+
PHPStan\Rules\Exceptions\TooWidePropertyHookThrowTypeRule:
35+
phpstan.rules.rule: %exceptions.check.tooWideThrowType%
3436

3537
parameters:
3638
checkAdvancedIsset: true
@@ -241,6 +243,9 @@ services:
241243
-
242244
class: PHPStan\Rules\Exceptions\TooWideMethodThrowTypeRule
243245

246+
-
247+
class: PHPStan\Rules\Exceptions\TooWidePropertyHookThrowTypeRule
248+
244249
-
245250
class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule
246251
arguments:
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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\Type\FileTypeMapper;
12+
use function sprintf;
13+
use function ucfirst;
14+
15+
/**
16+
* @implements Rule<PropertyHookReturnStatementsNode>
17+
*/
18+
final class TooWidePropertyHookThrowTypeRule implements Rule
19+
{
20+
21+
public function __construct(private FileTypeMapper $fileTypeMapper, private TooWideThrowTypeCheck $check)
22+
{
23+
}
24+
25+
public function getNodeType(): string
26+
{
27+
return PropertyHookReturnStatementsNode::class;
28+
}
29+
30+
public function processNode(Node $node, Scope $scope): array
31+
{
32+
$docComment = $node->getDocComment();
33+
if ($docComment === null) {
34+
return [];
35+
}
36+
37+
$statementResult = $node->getStatementResult();
38+
$hookReflection = $node->getHookReflection();
39+
if ($hookReflection->getPropertyHookName() === null) {
40+
throw new ShouldNotHappenException();
41+
}
42+
43+
$classReflection = $node->getClassReflection();
44+
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
45+
$scope->getFile(),
46+
$classReflection->getName(),
47+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
48+
$hookReflection->getName(),
49+
$docComment->getText(),
50+
);
51+
52+
if ($resolvedPhpDoc->getThrowsTag() === null) {
53+
return [];
54+
}
55+
56+
$throwType = $resolvedPhpDoc->getThrowsTag()->getType();
57+
58+
$errors = [];
59+
foreach ($this->check->check($throwType, $statementResult->getThrowPoints()) as $throwClass) {
60+
$errors[] = RuleErrorBuilder::message(sprintf(
61+
'%s hook for property %s::$%s has %s in PHPDoc @throws tag but it\'s not thrown.',
62+
ucfirst($hookReflection->getPropertyHookName()),
63+
$hookReflection->getDeclaringClass()->getDisplayName(),
64+
$hookReflection->getHookedPropertyName(),
65+
$throwClass,
66+
))
67+
->identifier('throws.unusedType')
68+
->build();
69+
}
70+
71+
return $errors;
72+
}
73+
74+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 PHPStan\Type\FileTypeMapper;
8+
use const PHP_VERSION_ID;
9+
10+
/**
11+
* @extends RuleTestCase<TooWidePropertyHookThrowTypeRule>
12+
*/
13+
class TooWidePropertyHookThrowTypeRuleTest extends RuleTestCase
14+
{
15+
16+
private bool $implicitThrows = true;
17+
18+
protected function getRule(): Rule
19+
{
20+
return new TooWidePropertyHookThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck($this->implicitThrows));
21+
}
22+
23+
public function testRule(): void
24+
{
25+
if (PHP_VERSION_ID < 80400) {
26+
$this->markTestSkipped('Test requires PHP 8.4.');
27+
}
28+
29+
$this->analyse([__DIR__ . '/data/too-wide-throws-property-hook.php'], [
30+
[
31+
'Get hook for property TooWideThrowsPropertyHook\Foo::$d has DomainException in PHPDoc @throws tag but it\'s not thrown.',
32+
33,
33+
],
34+
[
35+
'Get hook for property TooWideThrowsPropertyHook\Foo::$g has DomainException in PHPDoc @throws tag but it\'s not thrown.',
36+
58,
37+
],
38+
[
39+
'Get hook for property TooWideThrowsPropertyHook\Foo::$h has DomainException in PHPDoc @throws tag but it\'s not thrown.',
40+
68,
41+
],
42+
[
43+
'Get hook for property TooWideThrowsPropertyHook\Foo::$j has DomainException in PHPDoc @throws tag but it\'s not thrown.',
44+
76,
45+
],
46+
]);
47+
}
48+
49+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php // lint >= 8.4
2+
3+
namespace TooWideThrowsPropertyHook;
4+
5+
use DomainException;
6+
7+
class Foo
8+
{
9+
10+
public int $a {
11+
/** @throws \InvalidArgumentException */
12+
get {
13+
throw new \InvalidArgumentException();
14+
}
15+
}
16+
17+
public int $b {
18+
/** @throws \LogicException */
19+
get {
20+
throw new \InvalidArgumentException();
21+
}
22+
}
23+
24+
public int $c {
25+
/** @throws \InvalidArgumentException */
26+
get {
27+
throw new \LogicException();
28+
}
29+
}
30+
31+
public int $d {
32+
/** @throws \InvalidArgumentException|\DomainException */
33+
get { // error - DomainException unused
34+
throw new \InvalidArgumentException();
35+
}
36+
}
37+
38+
public int $e {
39+
/** @throws void */
40+
get { // ok - picked up by different rule
41+
throw new \InvalidArgumentException();
42+
}
43+
}
44+
45+
public int $f {
46+
/** @throws \InvalidArgumentException|\DomainException */
47+
get {
48+
if (rand(0, 1)) {
49+
throw new \InvalidArgumentException();
50+
}
51+
52+
throw new DomainException();
53+
}
54+
}
55+
56+
public int $g {
57+
/** @throws \DomainException */
58+
get { // error - DomainException unused
59+
throw new \InvalidArgumentException();
60+
}
61+
}
62+
63+
public int $h {
64+
/**
65+
* @throws \InvalidArgumentException
66+
* @throws \DomainException
67+
*/
68+
get { // error - DomainException unused
69+
throw new \InvalidArgumentException();
70+
}
71+
}
72+
73+
74+
public int $j {
75+
/** @throws \DomainException */
76+
get { // error - DomainException unused
77+
78+
}
79+
}
80+
81+
}

0 commit comments

Comments
 (0)