Skip to content

Commit 44b4680

Browse files
pepakrizondrejmirtes
authored andcommitted
Validate types to throw
1 parent e54d7bf commit 44b4680

File tree

9 files changed

+217
-0
lines changed

9 files changed

+217
-0
lines changed

conf/config.level3.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ rules:
1010
- PHPStan\Rules\Methods\ReturnTypeRule
1111
- PHPStan\Rules\Properties\DefaultValueTypesAssignedToPropertiesRule
1212
- PHPStan\Rules\Properties\TypesAssignedToPropertiesRule
13+
- PHPStan\Rules\Variables\ThrowTypeRule
1314
- PHPStan\Rules\Variables\VariableCloningRule
1415

1516
services:
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Variables;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\RuleLevelHelper;
8+
use PHPStan\Type\ErrorType;
9+
use PHPStan\Type\ObjectType;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\VerbosityLevel;
12+
13+
class ThrowTypeRule implements \PHPStan\Rules\Rule
14+
{
15+
16+
/** @var \PHPStan\Rules\RuleLevelHelper */
17+
private $ruleLevelHelper;
18+
19+
public function __construct(
20+
RuleLevelHelper $ruleLevelHelper
21+
)
22+
{
23+
$this->ruleLevelHelper = $ruleLevelHelper;
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return \PhpParser\Node\Stmt\Throw_::class;
29+
}
30+
31+
/**
32+
* @param \PhpParser\Node\Stmt\Throw_ $node
33+
* @param \PHPStan\Analyser\Scope $scope
34+
* @return string[]
35+
*/
36+
public function processNode(Node $node, Scope $scope): array
37+
{
38+
$throwableType = new ObjectType(\Throwable::class);
39+
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
40+
$scope,
41+
$node->expr,
42+
'Throwing object of an unknown class %s.',
43+
function (Type $type) use ($throwableType): bool {
44+
return $throwableType->isSuperTypeOf($type)->yes();
45+
}
46+
);
47+
48+
$foundType = $typeResult->getType();
49+
if ($foundType instanceof ErrorType) {
50+
return $typeResult->getUnknownClassErrors();
51+
}
52+
53+
$isSuperType = $throwableType->isSuperTypeOf($foundType);
54+
if ($isSuperType->yes()) {
55+
return [];
56+
}
57+
58+
return [
59+
sprintf('Invalid type %s to throw.', $foundType->describe(VerbosityLevel::typeOnly())),
60+
];
61+
}
62+
63+
}

tests/PHPStan/Levels/LevelsIntegrationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public function dataTopics(): array
1919
['clone'],
2020
['iterable'],
2121
['binaryOps'],
22+
['throwValues'],
2223
];
2324
}
2425

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[
2+
{
3+
"message": "Invalid type int to throw.",
4+
"line": 39,
5+
"ignorable": true
6+
},
7+
{
8+
"message": "Invalid type Levels\\ThrowValues\\InvalidException to throw.",
9+
"line": 40,
10+
"ignorable": true
11+
},
12+
{
13+
"message": "Invalid type Levels\\ThrowValues\\InvalidInterfaceException to throw.",
14+
"line": 41,
15+
"ignorable": true
16+
},
17+
{
18+
"message": "Invalid type int|string to throw.",
19+
"line": 44,
20+
"ignorable": true
21+
}
22+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Invalid type int|Throwable to throw.",
4+
"line": 43,
5+
"ignorable": true
6+
}
7+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Invalid type Exception|null to throw.",
4+
"line": 42,
5+
"ignorable": true
6+
}
7+
]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace Levels\ThrowValues;
4+
5+
class InvalidException
6+
{
7+
8+
}
9+
10+
interface InvalidInterfaceException
11+
{
12+
13+
}
14+
15+
interface ValidInterfaceException extends \Throwable
16+
{
17+
18+
}
19+
20+
class Foo
21+
{
22+
23+
/**
24+
* @param ValidInterfaceException $validInterface
25+
* @param InvalidInterfaceException $invalidInterface
26+
* @param \Exception|null $nullableException
27+
* @param \Throwable|int $throwableOrInt
28+
* @param int|string $intOrString
29+
*/
30+
public function doFoo(
31+
ValidInterfaceException $validInterface,
32+
InvalidInterfaceException $invalidInterface,
33+
?\Exception $nullableException,
34+
$throwableOrInt,
35+
$intOrString
36+
) {
37+
throw new \Exception();
38+
throw $validInterface;
39+
throw 123;
40+
throw new InvalidException();
41+
throw $invalidInterface;
42+
throw $nullableException;
43+
throw $throwableOrInt;
44+
throw $intOrString;
45+
}
46+
47+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Variables;
4+
5+
use PHPStan\Rules\RuleLevelHelper;
6+
7+
class ThrowTypeRuleTest extends \PHPStan\Testing\RuleTestCase
8+
{
9+
10+
protected function getRule(): \PHPStan\Rules\Rule
11+
{
12+
return new ThrowTypeRule(new RuleLevelHelper($this->createBroker(), true, false, true));
13+
}
14+
15+
public function testAccessPropertiesOnPossiblyNullRuleTest(): void
16+
{
17+
$this->analyse(
18+
[__DIR__ . '/data/throw-values.php'],
19+
[
20+
[
21+
'Invalid type int to throw.',
22+
19,
23+
],
24+
[
25+
'Invalid type ThrowValues\InvalidException to throw.',
26+
20,
27+
],
28+
[
29+
'Invalid type ThrowValues\InvalidInterfaceException to throw.',
30+
21,
31+
],
32+
[
33+
'Invalid type Exception|null to throw.',
34+
22,
35+
],
36+
[
37+
'Throwing object of an unknown class ThrowValues\NonexistentClass.',
38+
24,
39+
],
40+
]
41+
);
42+
}
43+
44+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace ThrowValues;
4+
5+
class InvalidException {};
6+
interface InvalidInterfaceException {};
7+
interface ValidInterfaceException extends \Throwable {};
8+
9+
function () {
10+
/** @var ValidInterfaceException $validInterface */
11+
$validInterface = new \Exception();
12+
/** @var InvalidInterfaceException $invalidInterface */
13+
$invalidInterface = new \Exception();
14+
/** @var \Exception|null $nullableException */
15+
$nullableException = new \Exception();
16+
17+
throw new \Exception();
18+
throw $validInterface;
19+
throw 123;
20+
throw new InvalidException();
21+
throw $invalidInterface;
22+
throw $nullableException;
23+
throw foo();
24+
throw new NonexistentClass();
25+
};

0 commit comments

Comments
 (0)