Skip to content

Commit 0359ebc

Browse files
committed
ThrowExprTypeRule - level 3
1 parent 981c7ba commit 0359ebc

File tree

6 files changed

+240
-0
lines changed

6 files changed

+240
-0
lines changed

conf/config.level3.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ rules:
1616
- PHPStan\Rules\Arrays\OffsetAccessAssignOpRule
1717
- PHPStan\Rules\Arrays\OffsetAccessValueAssignmentRule
1818
- PHPStan\Rules\Arrays\UnpackIterableInArrayRule
19+
- PHPStan\Rules\Exceptions\ThrowExprTypeRule
1920
- PHPStan\Rules\Functions\ArrowFunctionReturnTypeRule
2021
- PHPStan\Rules\Functions\ClosureReturnTypeRule
2122
- PHPStan\Rules\Functions\ReturnTypeRule
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\Rules\Rule;
8+
use PHPStan\Rules\RuleErrorBuilder;
9+
use PHPStan\Rules\RuleLevelHelper;
10+
use PHPStan\Type\ErrorType;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\VerbosityLevel;
14+
use Throwable;
15+
use function sprintf;
16+
17+
/**
18+
* @implements Rule<Node\Expr\Throw_>
19+
*/
20+
class ThrowExprTypeRule implements Rule
21+
{
22+
23+
public function __construct(
24+
private RuleLevelHelper $ruleLevelHelper,
25+
)
26+
{
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return Node\Expr\Throw_::class;
32+
}
33+
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
$throwableType = new ObjectType(Throwable::class);
37+
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
38+
$scope,
39+
$node->expr,
40+
'Throwing object of an unknown class %s.',
41+
static fn (Type $type): bool => $throwableType->isSuperTypeOf($type)->yes(),
42+
);
43+
44+
$foundType = $typeResult->getType();
45+
if ($foundType instanceof ErrorType) {
46+
return $typeResult->getUnknownClassErrors();
47+
}
48+
49+
$isSuperType = $throwableType->isSuperTypeOf($foundType);
50+
if ($isSuperType->yes()) {
51+
return [];
52+
}
53+
54+
return [
55+
RuleErrorBuilder::message(sprintf(
56+
'Invalid type %s to throw.',
57+
$foundType->describe(VerbosityLevel::typeOnly()),
58+
))->build(),
59+
];
60+
}
61+
62+
}
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 PHPStan\Rules\Rule;
6+
use PHPStan\Rules\RuleLevelHelper;
7+
use PHPStan\Testing\RuleTestCase;
8+
use const PHP_VERSION_ID;
9+
10+
/**
11+
* @extends RuleTestCase<ThrowExprTypeRule>
12+
*/
13+
class ThrowExprTypeRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new ThrowExprTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false));
19+
}
20+
21+
public function testRule(): void
22+
{
23+
$this->analyse(
24+
[__DIR__ . '/data/throw-values.php'],
25+
[
26+
/*[
27+
'Invalid type int to throw.',
28+
29,
29+
],
30+
[
31+
'Invalid type ThrowValues\InvalidException to throw.',
32+
32,
33+
],
34+
[
35+
'Invalid type ThrowValues\InvalidInterfaceException to throw.',
36+
35,
37+
],
38+
[
39+
'Invalid type Exception|null to throw.',
40+
38,
41+
],
42+
[
43+
'Throwing object of an unknown class ThrowValues\NonexistentClass.',
44+
44,
45+
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
46+
],*/
47+
[
48+
'Invalid type int to throw.',
49+
65,
50+
],
51+
],
52+
);
53+
}
54+
55+
public function testClassExists(): void
56+
{
57+
$this->analyse([__DIR__ . '/data/throw-class-exists.php'], []);
58+
}
59+
60+
public function testRuleWithNullsafeVariant(): void
61+
{
62+
if (PHP_VERSION_ID < 80000) {
63+
$this->markTestSkipped('Test requires PHP 8.0.');
64+
}
65+
66+
$this->analyse([__DIR__ . '/data/throw-values-nullsafe.php'], [
67+
/*[
68+
'Invalid type Exception|null to throw.',
69+
17,
70+
],*/
71+
]);
72+
}
73+
74+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace ThrowExprClassExists;
4+
5+
use function class_exists;
6+
7+
class Foo
8+
{
9+
10+
public function doFoo(): void
11+
{
12+
if (!class_exists(Bar::class)) {
13+
return;
14+
}
15+
16+
throw new Bar();
17+
}
18+
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php // lint >= 8.0
2+
3+
namespace ThrowExprValuesNullsafe;
4+
5+
class Bar
6+
{
7+
8+
function doException(): \Exception
9+
{
10+
return new \Exception();
11+
}
12+
13+
}
14+
15+
function doFoo(?Bar $bar)
16+
{
17+
throw $bar?->doException();
18+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace ThrowExprValues;
4+
5+
class InvalidException {};
6+
interface InvalidInterfaceException {};
7+
interface ValidInterfaceException extends \Throwable {};
8+
9+
/**
10+
* @template T of \Exception
11+
* @param class-string<T> $genericExceptionClassName
12+
* @param T $genericException
13+
*/
14+
function test($genericExceptionClassName, $genericException) {
15+
/** @var ValidInterfaceException $validInterface */
16+
$validInterface = new \Exception();
17+
/** @var InvalidInterfaceException $invalidInterface */
18+
$invalidInterface = new \Exception();
19+
/** @var \Exception|null $nullableException */
20+
$nullableException = new \Exception();
21+
22+
if (rand(0, 1)) {
23+
throw new \Exception();
24+
}
25+
if (rand(0, 1)) {
26+
throw $validInterface;
27+
}
28+
if (rand(0, 1)) {
29+
throw 123;
30+
}
31+
if (rand(0, 1)) {
32+
throw new InvalidException();
33+
}
34+
if (rand(0, 1)) {
35+
throw $invalidInterface;
36+
}
37+
if (rand(0, 1)) {
38+
throw $nullableException;
39+
}
40+
if (rand(0, 1)) {
41+
throw foo();
42+
}
43+
if (rand(0, 1)) {
44+
throw new NonexistentClass();
45+
}
46+
if (rand(0, 1)) {
47+
throw new $genericExceptionClassName;
48+
}
49+
if (rand(0, 1)) {
50+
throw $genericException;
51+
}
52+
}
53+
54+
function (\stdClass $foo) {
55+
/** @var \Exception $foo */
56+
throw $foo;
57+
};
58+
59+
function (\stdClass $foo) {
60+
/** @var \Exception */
61+
throw $foo;
62+
};
63+
64+
function (?\stdClass $foo) {
65+
echo $foo ?? throw 1;
66+
};

0 commit comments

Comments
 (0)