Skip to content

Commit

Permalink
ThrowExprTypeRule - level 3
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Dec 29, 2023
1 parent 981c7ba commit 0359ebc
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/config.level3.neon
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ rules:
- PHPStan\Rules\Arrays\OffsetAccessAssignOpRule
- PHPStan\Rules\Arrays\OffsetAccessValueAssignmentRule
- PHPStan\Rules\Arrays\UnpackIterableInArrayRule
- PHPStan\Rules\Exceptions\ThrowExprTypeRule
- PHPStan\Rules\Functions\ArrowFunctionReturnTypeRule
- PHPStan\Rules\Functions\ClosureReturnTypeRule
- PHPStan\Rules\Functions\ReturnTypeRule
Expand Down
62 changes: 62 additions & 0 deletions src/Rules/Exceptions/ThrowExprTypeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Exceptions;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Type\ErrorType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use Throwable;
use function sprintf;

/**
* @implements Rule<Node\Expr\Throw_>
*/
class ThrowExprTypeRule implements Rule
{

public function __construct(
private RuleLevelHelper $ruleLevelHelper,
)
{
}

public function getNodeType(): string
{
return Node\Expr\Throw_::class;
}

public function processNode(Node $node, Scope $scope): array
{
$throwableType = new ObjectType(Throwable::class);
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->expr,
'Throwing object of an unknown class %s.',
static fn (Type $type): bool => $throwableType->isSuperTypeOf($type)->yes(),
);

$foundType = $typeResult->getType();
if ($foundType instanceof ErrorType) {
return $typeResult->getUnknownClassErrors();
}

$isSuperType = $throwableType->isSuperTypeOf($foundType);
if ($isSuperType->yes()) {
return [];
}

return [
RuleErrorBuilder::message(sprintf(
'Invalid type %s to throw.',
$foundType->describe(VerbosityLevel::typeOnly()),
))->build(),
];
}

}
74 changes: 74 additions & 0 deletions tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<ThrowExprTypeRule>
*/
class ThrowExprTypeRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new ThrowExprTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false));
}

public function testRule(): void
{
$this->analyse(
[__DIR__ . '/data/throw-values.php'],
[
/*[
'Invalid type int to throw.',
29,
],
[
'Invalid type ThrowValues\InvalidException to throw.',
32,
],
[
'Invalid type ThrowValues\InvalidInterfaceException to throw.',
35,
],
[
'Invalid type Exception|null to throw.',
38,
],
[
'Throwing object of an unknown class ThrowValues\NonexistentClass.',
44,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],*/
[
'Invalid type int to throw.',
65,
],
],
);
}

public function testClassExists(): void
{
$this->analyse([__DIR__ . '/data/throw-class-exists.php'], []);
}

public function testRuleWithNullsafeVariant(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Test requires PHP 8.0.');
}

$this->analyse([__DIR__ . '/data/throw-values-nullsafe.php'], [
/*[
'Invalid type Exception|null to throw.',
17,
],*/
]);
}

}
19 changes: 19 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace ThrowExprClassExists;

use function class_exists;

class Foo
{

public function doFoo(): void
{
if (!class_exists(Bar::class)) {
return;
}

throw new Bar();
}

}
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/throw-values-nullsafe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php // lint >= 8.0

namespace ThrowExprValuesNullsafe;

class Bar
{

function doException(): \Exception
{
return new \Exception();
}

}

function doFoo(?Bar $bar)
{
throw $bar?->doException();
}
66 changes: 66 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/throw-values.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace ThrowExprValues;

class InvalidException {};
interface InvalidInterfaceException {};
interface ValidInterfaceException extends \Throwable {};

/**
* @template T of \Exception
* @param class-string<T> $genericExceptionClassName
* @param T $genericException
*/
function test($genericExceptionClassName, $genericException) {
/** @var ValidInterfaceException $validInterface */
$validInterface = new \Exception();
/** @var InvalidInterfaceException $invalidInterface */
$invalidInterface = new \Exception();
/** @var \Exception|null $nullableException */
$nullableException = new \Exception();

if (rand(0, 1)) {
throw new \Exception();
}
if (rand(0, 1)) {
throw $validInterface;
}
if (rand(0, 1)) {
throw 123;
}
if (rand(0, 1)) {
throw new InvalidException();
}
if (rand(0, 1)) {
throw $invalidInterface;
}
if (rand(0, 1)) {
throw $nullableException;
}
if (rand(0, 1)) {
throw foo();
}
if (rand(0, 1)) {
throw new NonexistentClass();
}
if (rand(0, 1)) {
throw new $genericExceptionClassName;
}
if (rand(0, 1)) {
throw $genericException;
}
}

function (\stdClass $foo) {
/** @var \Exception $foo */
throw $foo;
};

function (\stdClass $foo) {
/** @var \Exception */
throw $foo;
};

function (?\stdClass $foo) {
echo $foo ?? throw 1;
};

0 comments on commit 0359ebc

Please sign in to comment.