Skip to content

Commit 5ee375e

Browse files
authored
InvalidThrowsPhpDocValueRule: support @phpstan-require-extends
1 parent 11268e5 commit 5ee375e

File tree

3 files changed

+127
-2
lines changed

3 files changed

+127
-2
lines changed

src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use PHPStan\Rules\RuleErrorBuilder;
99
use PHPStan\Type\FileTypeMapper;
1010
use PHPStan\Type\ObjectType;
11+
use PHPStan\Type\Type;
12+
use PHPStan\Type\TypeCombinator;
13+
use PHPStan\Type\UnionType;
1114
use PHPStan\Type\VerbosityLevel;
1215
use Throwable;
1316
use function sprintf;
@@ -60,8 +63,7 @@ public function processNode(Node $node, Scope $scope): array
6063
return [];
6164
}
6265

63-
$isThrowsSuperType = (new ObjectType(Throwable::class))->isSuperTypeOf($phpDocThrowsType);
64-
if ($isThrowsSuperType->yes()) {
66+
if ($this->isThrowsValid($phpDocThrowsType)) {
6567
return [];
6668
}
6769

@@ -73,4 +75,29 @@ public function processNode(Node $node, Scope $scope): array
7375
];
7476
}
7577

78+
private function isThrowsValid(Type $phpDocThrowsType): bool
79+
{
80+
$throwType = new ObjectType(Throwable::class);
81+
if ($phpDocThrowsType instanceof UnionType) {
82+
foreach ($phpDocThrowsType->getTypes() as $innerType) {
83+
if (!$this->isThrowsValid($innerType)) {
84+
return false;
85+
}
86+
}
87+
88+
return true;
89+
}
90+
91+
$toIntersectWith = [];
92+
foreach ($phpDocThrowsType->getObjectClassReflections() as $classReflection) {
93+
foreach ($classReflection->getRequireExtendsTags() as $requireExtendsTag) {
94+
$toIntersectWith[] = $requireExtendsTag->getType();
95+
}
96+
}
97+
98+
return $throwType->isSuperTypeOf(
99+
TypeCombinator::intersect($phpDocThrowsType, ...$toIntersectWith),
100+
)->yes();
101+
}
102+
76103
}

tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,28 @@ public function testInheritedPhpDocs(): void
7777
]);
7878
}
7979

80+
public function testThrowsWithRequireExtends(): void
81+
{
82+
$this->analyse([__DIR__ . '/data/throws-with-require.php'], [
83+
[
84+
'PHPDoc tag @throws with type ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable',
85+
25,
86+
],
87+
[
88+
'PHPDoc tag @throws with type DateTimeInterface|ThrowsWithRequire\\RequiresExtendsExceptionInterface is not subtype of Throwable',
89+
39,
90+
],
91+
[
92+
'PHPDoc tag @throws with type Exception|ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable',
93+
46,
94+
],
95+
[
96+
'PHPDoc tag @throws with type Iterator&ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable',
97+
74,
98+
],
99+
]);
100+
}
101+
80102
public function dataMergeInheritedPhpDocs(): array
81103
{
82104
return [
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace ThrowsWithRequire;
4+
5+
/**
6+
* @phpstan-require-extends \Exception
7+
*/
8+
interface RequiresExtendsExceptionInterface {}
9+
10+
/**
11+
* @phpstan-require-extends \stdClass
12+
*/
13+
interface RequiresExtendsStdClassInterface {}
14+
15+
/**
16+
* @throws RequiresExtendsExceptionInterface
17+
*/
18+
function requiresExtendsExceptionThrows()
19+
{
20+
}
21+
22+
/**
23+
* @throws RequiresExtendsStdClassInterface
24+
*/
25+
function requiresExtendsStdClassThrows()
26+
{
27+
}
28+
29+
/**
30+
* @throws \Exception|RequiresExtendsExceptionInterface
31+
*/
32+
function unionExceptionAndRequiresExtendsExceptionThrows()
33+
{
34+
}
35+
36+
/**
37+
* @throws \DateTimeInterface|RequiresExtendsExceptionInterface
38+
*/
39+
function notThrowableUnionDateTimeInterfaceAndRequiresExtendsExceptionThrows()
40+
{
41+
}
42+
43+
/**
44+
* @throws \Exception|RequiresExtendsStdClassInterface
45+
*/
46+
function notThrowableUnionExceptionAndRequiresExtendsStdClassThrows()
47+
{
48+
}
49+
50+
/**
51+
* @throws \Exception&RequiresExtendsExceptionInterface
52+
*/
53+
function intersectionExceptionAndRequiresExtendsExceptionThrows()
54+
{
55+
}
56+
57+
/**
58+
* @throws \DateTimeInterface&RequiresExtendsExceptionInterface
59+
*/
60+
function intersectionDateTimeInterfaceAndRequiresExtendsExceptionThrows()
61+
{
62+
}
63+
64+
/**
65+
* @throws \Exception&RequiresExtendsStdClassInterface
66+
*/
67+
function intersectionExceptionAndRequiresExtendsStdClassThrows()
68+
{
69+
}
70+
71+
/**
72+
* @throws \Iterator&RequiresExtendsStdClassInterface
73+
*/
74+
function notThrowableIntersectionIteratorAndRequiresExtendsStdClassThrows()
75+
{
76+
}

0 commit comments

Comments
 (0)