Skip to content

Commit 9902bd7

Browse files
authored
ThrowsPhpDocInheritanceRule respects methodThrowTypeDeclarations (#26)
1 parent e208e2c commit 9902bd7

File tree

6 files changed

+200
-78
lines changed

6 files changed

+200
-78
lines changed

extension.neon

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ services:
1414
class: Pepakriz\PHPStanExceptionRules\CheckedExceptionService(%exceptionRules.checkedExceptions%, %exceptionRules.uncheckedExceptions%)
1515

1616
-
17-
class: Pepakriz\PHPStanExceptionRules\DefaultThrowTypeExtension(%exceptionRules.methodThrowTypeDeclarations%, %exceptionRules.functionThrowTypeDeclarations%)
17+
class: Pepakriz\PHPStanExceptionRules\DefaultThrowTypeService(%exceptionRules.methodThrowTypeDeclarations%, %exceptionRules.functionThrowTypeDeclarations%)
18+
19+
-
20+
class: Pepakriz\PHPStanExceptionRules\DefaultThrowTypeExtension
1821
tags:
1922
- exceptionRules.dynamicMethodThrowTypeExtension
2023
- exceptionRules.dynamicStaticMethodThrowTypeExtension

src/DefaultThrowTypeExtension.php

Lines changed: 8 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,75 +9,29 @@
99
use PHPStan\Analyser\Scope;
1010
use PHPStan\Reflection\FunctionReflection;
1111
use PHPStan\Reflection\MethodReflection;
12-
use PHPStan\Type\ObjectType;
1312
use PHPStan\Type\Type;
14-
use PHPStan\Type\TypeCombinator;
15-
use PHPStan\Type\VoidType;
16-
use function array_map;
17-
use function count;
1813

1914
class DefaultThrowTypeExtension implements DynamicFunctionThrowTypeExtension, DynamicMethodThrowTypeExtension, DynamicConstructorThrowTypeExtension, DynamicStaticMethodThrowTypeExtension
2015
{
2116

2217
/**
23-
* @var Type[][]
18+
* @var DefaultThrowTypeService
2419
*/
25-
private $methodThrowTypes = [];
20+
private $defaultThrowTypeService;
2621

27-
/**
28-
* @var Type[]
29-
*/
30-
private $functionThrowTypes = [];
31-
32-
/**
33-
* @param string[][][] $methodThrowTypes
34-
* @param string[][] $functionThrowTypes
35-
*/
3622
public function __construct(
37-
array $methodThrowTypes,
38-
array $functionThrowTypes
23+
DefaultThrowTypeService $defaultThrowTypeService
3924
)
4025
{
41-
foreach ($methodThrowTypes as $className => $methods) {
42-
foreach ($methods as $methodName => $throwTypes) {
43-
if (count($throwTypes) === 0) {
44-
$this->methodThrowTypes[$className][$methodName] = new VoidType();
45-
continue;
46-
}
47-
48-
$this->methodThrowTypes[$className][$methodName] = TypeCombinator::union(
49-
...array_map(static function (string $throwType): ObjectType {
50-
return new ObjectType($throwType);
51-
}, $throwTypes)
52-
);
53-
}
54-
}
55-
56-
foreach ($functionThrowTypes as $functionName => $throwTypes) {
57-
if (count($throwTypes) === 0) {
58-
$this->functionThrowTypes[$functionName] = new VoidType();
59-
continue;
60-
}
61-
62-
$this->functionThrowTypes[$functionName] = TypeCombinator::union(
63-
...array_map(static function (string $throwType): ObjectType {
64-
return new ObjectType($throwType);
65-
}, $throwTypes)
66-
);
67-
}
26+
$this->defaultThrowTypeService = $defaultThrowTypeService;
6827
}
6928

7029
/**
7130
* @throws UnsupportedFunctionException
7231
*/
7332
public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
7433
{
75-
$functionName = $functionReflection->getName();
76-
if (!isset($this->functionThrowTypes[$functionName])) {
77-
throw new UnsupportedFunctionException();
78-
}
79-
80-
return $this->functionThrowTypes[$functionName];
34+
return $this->defaultThrowTypeService->getFunctionThrowType($functionReflection);
8135
}
8236

8337
/**
@@ -86,7 +40,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect
8640
*/
8741
public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
8842
{
89-
return $this->getMethodThrowType($methodReflection);
43+
return $this->defaultThrowTypeService->getMethodThrowType($methodReflection);
9044
}
9145

9246
/**
@@ -95,37 +49,15 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M
9549
*/
9650
public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type
9751
{
98-
return $this->getMethodThrowType($methodReflection);
52+
return $this->defaultThrowTypeService->getMethodThrowType($methodReflection);
9953
}
10054

10155
/**
10256
* @throws UnsupportedClassException
10357
*/
10458
public function getThrowTypeFromConstructor(MethodReflection $methodReflection, New_ $newNode, Scope $scope): Type
10559
{
106-
try {
107-
return $this->getMethodThrowType($methodReflection);
108-
} catch (UnsupportedFunctionException $e) {
109-
throw new UnsupportedClassException();
110-
}
111-
}
112-
113-
/**
114-
* @throws UnsupportedClassException
115-
* @throws UnsupportedFunctionException
116-
*/
117-
private function getMethodThrowType(MethodReflection $methodReflection): Type
118-
{
119-
$className = $methodReflection->getDeclaringClass()->getName();
120-
if (!isset($this->methodThrowTypes[$className])) {
121-
throw new UnsupportedClassException();
122-
}
123-
124-
if (!isset($this->methodThrowTypes[$className][$methodReflection->getName()])) {
125-
throw new UnsupportedFunctionException();
126-
}
127-
128-
return $this->methodThrowTypes[$className][$methodReflection->getName()];
60+
return $this->defaultThrowTypeService->getConstructorThrowType($methodReflection);
12961
}
13062

13163
}

src/DefaultThrowTypeService.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Pepakriz\PHPStanExceptionRules;
4+
5+
use PHPStan\Reflection\FunctionReflection;
6+
use PHPStan\Reflection\MethodReflection;
7+
use PHPStan\Type\ObjectType;
8+
use PHPStan\Type\Type;
9+
use PHPStan\Type\TypeCombinator;
10+
use PHPStan\Type\VoidType;
11+
use function array_map;
12+
use function count;
13+
14+
class DefaultThrowTypeService
15+
{
16+
17+
/**
18+
* @var Type[][]
19+
*/
20+
private $methodThrowTypes = [];
21+
22+
/**
23+
* @var Type[]
24+
*/
25+
private $functionThrowTypes = [];
26+
27+
/**
28+
* @param string[][][] $methodThrowTypes
29+
* @param string[][] $functionThrowTypes
30+
*/
31+
public function __construct(
32+
array $methodThrowTypes,
33+
array $functionThrowTypes
34+
)
35+
{
36+
foreach ($methodThrowTypes as $className => $methods) {
37+
foreach ($methods as $methodName => $throwTypes) {
38+
if (count($throwTypes) === 0) {
39+
$this->methodThrowTypes[$className][$methodName] = new VoidType();
40+
continue;
41+
}
42+
43+
$this->methodThrowTypes[$className][$methodName] = TypeCombinator::union(
44+
...array_map(static function (string $throwType): ObjectType {
45+
return new ObjectType($throwType);
46+
}, $throwTypes)
47+
);
48+
}
49+
}
50+
51+
foreach ($functionThrowTypes as $functionName => $throwTypes) {
52+
if (count($throwTypes) === 0) {
53+
$this->functionThrowTypes[$functionName] = new VoidType();
54+
continue;
55+
}
56+
57+
$this->functionThrowTypes[$functionName] = TypeCombinator::union(
58+
...array_map(static function (string $throwType): ObjectType {
59+
return new ObjectType($throwType);
60+
}, $throwTypes)
61+
);
62+
}
63+
}
64+
65+
/**
66+
* @throws UnsupportedFunctionException
67+
*/
68+
public function getFunctionThrowType(FunctionReflection $functionReflection): Type
69+
{
70+
$functionName = $functionReflection->getName();
71+
if (!isset($this->functionThrowTypes[$functionName])) {
72+
throw new UnsupportedFunctionException();
73+
}
74+
75+
return $this->functionThrowTypes[$functionName];
76+
}
77+
78+
/**
79+
* @throws UnsupportedClassException
80+
*/
81+
public function getConstructorThrowType(MethodReflection $methodReflection): Type
82+
{
83+
try {
84+
return $this->getMethodThrowType($methodReflection);
85+
} catch (UnsupportedFunctionException $e) {
86+
throw new UnsupportedClassException();
87+
}
88+
}
89+
90+
/**
91+
* @throws UnsupportedClassException
92+
* @throws UnsupportedFunctionException
93+
*/
94+
public function getMethodThrowType(MethodReflection $methodReflection): Type
95+
{
96+
$className = $methodReflection->getDeclaringClass()->getName();
97+
if (!isset($this->methodThrowTypes[$className])) {
98+
throw new UnsupportedClassException();
99+
}
100+
101+
if (!isset($this->methodThrowTypes[$className][$methodReflection->getName()])) {
102+
throw new UnsupportedFunctionException();
103+
}
104+
105+
return $this->methodThrowTypes[$className][$methodReflection->getName()];
106+
}
107+
108+
}

src/Rules/ThrowsPhpDocInheritanceRule.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
namespace Pepakriz\PHPStanExceptionRules\Rules;
44

55
use Pepakriz\PHPStanExceptionRules\CheckedExceptionService;
6+
use Pepakriz\PHPStanExceptionRules\DefaultThrowTypeService;
7+
use Pepakriz\PHPStanExceptionRules\UnsupportedClassException;
8+
use Pepakriz\PHPStanExceptionRules\UnsupportedFunctionException;
69
use PhpParser\Node;
710
use PhpParser\Node\Stmt\ClassMethod;
811
use PHPStan\Analyser\Scope;
@@ -29,6 +32,11 @@ class ThrowsPhpDocInheritanceRule implements Rule
2932
*/
3033
private $checkedExceptionService;
3134

35+
/**
36+
* @var DefaultThrowTypeService
37+
*/
38+
private $defaultThrowTypeService;
39+
3240
/**
3341
* @var FileTypeMapper
3442
*/
@@ -41,11 +49,13 @@ class ThrowsPhpDocInheritanceRule implements Rule
4149

4250
public function __construct(
4351
CheckedExceptionService $checkedExceptionService,
52+
DefaultThrowTypeService $defaultThrowTypeService,
4453
FileTypeMapper $fileTypeMapper,
4554
Broker $broker
4655
)
4756
{
4857
$this->checkedExceptionService = $checkedExceptionService;
58+
$this->defaultThrowTypeService = $defaultThrowTypeService;
4959
$this->fileTypeMapper = $fileTypeMapper;
5060
$this->broker = $broker;
5161
}
@@ -116,7 +126,12 @@ public function processNode(Node $node, Scope $scope): array
116126
continue;
117127
}
118128

119-
$parentThrowType = $methodReflection->getThrowType();
129+
try {
130+
$parentThrowType = $this->defaultThrowTypeService->getMethodThrowType($methodReflection);
131+
} catch (UnsupportedClassException | UnsupportedFunctionException $e) {
132+
$parentThrowType = $methodReflection->getThrowType();
133+
}
134+
120135
if ($parentThrowType === null) {
121136
$messages[] = sprintf(
122137
'PHPDoc tag @throws with type %s is not compatible with parent',

tests/src/Rules/ThrowsPhpDocInheritanceRuleTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
namespace Pepakriz\PHPStanExceptionRules\Rules;
44

55
use Pepakriz\PHPStanExceptionRules\CheckedExceptionService;
6+
use Pepakriz\PHPStanExceptionRules\DefaultThrowTypeService;
7+
use Pepakriz\PHPStanExceptionRules\Rules\Data\InheritanceOverriding\BaseThrowsAnnotations;
8+
use Pepakriz\PHPStanExceptionRules\Rules\Data\InheritanceOverriding\ConcreteException;
69
use Pepakriz\PHPStanExceptionRules\RuleTestCase;
710
use PHPStan\Rules\Rule;
811
use PHPStan\Type\FileTypeMapper;
@@ -19,6 +22,16 @@ protected function getRule(): Rule
1922
RuntimeException::class,
2023
]
2124
),
25+
new DefaultThrowTypeService([
26+
BaseThrowsAnnotations::class => [
27+
'foo' => [
28+
RuntimeException::class,
29+
],
30+
'bar' => [
31+
ConcreteException::class,
32+
],
33+
],
34+
], []),
2235
self::getContainer()->getByType(FileTypeMapper::class),
2336
$this->createBroker()
2437
);
@@ -34,4 +47,9 @@ public function testInheritanceWithInterfaces(): void
3447
$this->analyse(__DIR__ . '/data/throws-inheritance-interfaces.php');
3548
}
3649

50+
public function testInheritanceWithOverriding(): void
51+
{
52+
$this->analyse(__DIR__ . '/data/throws-inheritance-overriding.php');
53+
}
54+
3755
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Pepakriz\PHPStanExceptionRules\Rules\Data\InheritanceOverriding;
4+
5+
use RuntimeException;
6+
7+
class ConcreteException extends RuntimeException {};
8+
9+
abstract class BaseThrowsAnnotations
10+
{
11+
12+
public function foo(): void
13+
{
14+
15+
}
16+
17+
/**
18+
* @throws RuntimeException
19+
*/
20+
public function bar(): void
21+
{
22+
23+
}
24+
25+
}
26+
27+
class ThrowsAnnotations extends BaseThrowsAnnotations
28+
{
29+
30+
/**
31+
* @throws RuntimeException
32+
*/
33+
public function foo(): void
34+
{
35+
36+
}
37+
38+
/**
39+
* @throws ConcreteException
40+
*/
41+
public function bar(): void
42+
{
43+
44+
}
45+
46+
}

0 commit comments

Comments
 (0)