Skip to content

Commit 8b3382a

Browse files
committed
Bleeding edge - check that function with @throws void does not have an explicit throw point
1 parent 3624e66 commit 8b3382a

9 files changed

+361
-1
lines changed

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ parameters:
2727
classConstants: true
2828
privateStaticCall: true
2929
overridingProperty: true
30+
throwsVoid: true
3031
stubFiles:
3132
- ../stubs/arrayFunctions.stub

conf/config.level3.neon

+16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ rules:
2020
conditionalTags:
2121
PHPStan\Rules\Arrays\ArrayDestructuringRule:
2222
phpstan.rules.rule: %featureToggles.arrayDestructuring%
23+
PHPStan\Rules\Exceptions\ThrowsVoidFunctionWithExplicitThrowPointRule:
24+
phpstan.rules.rule: %featureToggles.throwsVoid%
25+
PHPStan\Rules\Exceptions\ThrowsVoidMethodWithExplicitThrowPointRule:
26+
phpstan.rules.rule: %featureToggles.throwsVoid%
2327

2428
parameters:
2529
checkPhpDocMethodSignatures: true
@@ -56,6 +60,18 @@ services:
5660
tags:
5761
- phpstan.rules.rule
5862

63+
-
64+
class: PHPStan\Rules\Exceptions\ThrowsVoidFunctionWithExplicitThrowPointRule
65+
arguments:
66+
exceptionTypeResolver: @exceptionTypeResolver
67+
missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows%
68+
69+
-
70+
class: PHPStan\Rules\Exceptions\ThrowsVoidMethodWithExplicitThrowPointRule
71+
arguments:
72+
exceptionTypeResolver: @exceptionTypeResolver
73+
missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows%
74+
5975
-
6076
class: PHPStan\Rules\Functions\ReturnTypeRule
6177
arguments:

conf/config.neon

+3-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ parameters:
5252
classConstants: false
5353
privateStaticCall: false
5454
overridingProperty: false
55+
throwsVoid: false
5556
fileExtensions:
5657
- php
5758
checkAdvancedIsset: false
@@ -237,7 +238,8 @@ parametersSchema:
237238
finalByPhpDocTag: bool(),
238239
classConstants: bool(),
239240
privateStaticCall: bool(),
240-
overridingProperty: bool()
241+
overridingProperty: bool(),
242+
throwsVoid: bool()
241243
])
242244
fileExtensions: listOf(string())
243245
checkAdvancedIsset: bool()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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\Node\FunctionReturnStatementsNode;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Type\TypeUtils;
12+
use PHPStan\Type\TypeWithClassName;
13+
use PHPStan\Type\VerbosityLevel;
14+
use PHPStan\Type\VoidType;
15+
16+
/**
17+
* @implements Rule<FunctionReturnStatementsNode>
18+
*/
19+
class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule
20+
{
21+
22+
private ExceptionTypeResolver $exceptionTypeResolver;
23+
24+
private bool $missingCheckedExceptionInThrows;
25+
26+
public function __construct(
27+
ExceptionTypeResolver $exceptionTypeResolver,
28+
bool $missingCheckedExceptionInThrows
29+
)
30+
{
31+
$this->exceptionTypeResolver = $exceptionTypeResolver;
32+
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return FunctionReturnStatementsNode::class;
38+
}
39+
40+
public function processNode(Node $node, Scope $scope): array
41+
{
42+
if ($this->missingCheckedExceptionInThrows) {
43+
return [];
44+
}
45+
46+
$statementResult = $node->getStatementResult();
47+
$functionReflection = $scope->getFunction();
48+
if (!$functionReflection instanceof FunctionReflection) {
49+
throw new \PHPStan\ShouldNotHappenException();
50+
}
51+
52+
if (!$functionReflection->getThrowType() instanceof VoidType) {
53+
return [];
54+
}
55+
56+
$errors = [];
57+
foreach ($statementResult->getThrowPoints() as $throwPoint) {
58+
if (!$throwPoint->isExplicit()) {
59+
continue;
60+
}
61+
62+
foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) {
63+
if (
64+
$throwPointType instanceof TypeWithClassName
65+
&& $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope())
66+
) {
67+
continue;
68+
}
69+
70+
$errors[] = RuleErrorBuilder::message(sprintf(
71+
'Function %s() throws exception %s but the PHPDoc contains @throws void.',
72+
$functionReflection->getName(),
73+
$throwPointType->describe(VerbosityLevel::typeOnly())
74+
))->line($throwPoint->getNode()->getLine())->build();
75+
}
76+
}
77+
78+
return $errors;
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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\Node\MethodReturnStatementsNode;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Type\TypeUtils;
12+
use PHPStan\Type\TypeWithClassName;
13+
use PHPStan\Type\VerbosityLevel;
14+
use PHPStan\Type\VoidType;
15+
16+
/**
17+
* @implements Rule<MethodReturnStatementsNode>
18+
*/
19+
class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule
20+
{
21+
22+
private ExceptionTypeResolver $exceptionTypeResolver;
23+
24+
private bool $missingCheckedExceptionInThrows;
25+
26+
public function __construct(
27+
ExceptionTypeResolver $exceptionTypeResolver,
28+
bool $missingCheckedExceptionInThrows
29+
)
30+
{
31+
$this->exceptionTypeResolver = $exceptionTypeResolver;
32+
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return MethodReturnStatementsNode::class;
38+
}
39+
40+
public function processNode(Node $node, Scope $scope): array
41+
{
42+
if ($this->missingCheckedExceptionInThrows) {
43+
return [];
44+
}
45+
46+
$statementResult = $node->getStatementResult();
47+
$methodReflection = $scope->getFunction();
48+
if (!$methodReflection instanceof MethodReflection) {
49+
throw new \PHPStan\ShouldNotHappenException();
50+
}
51+
52+
if (!$methodReflection->getThrowType() instanceof VoidType) {
53+
return [];
54+
}
55+
56+
$errors = [];
57+
foreach ($statementResult->getThrowPoints() as $throwPoint) {
58+
if (!$throwPoint->isExplicit()) {
59+
continue;
60+
}
61+
62+
foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) {
63+
if (
64+
$throwPointType instanceof TypeWithClassName
65+
&& $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope())
66+
) {
67+
continue;
68+
}
69+
70+
$errors[] = RuleErrorBuilder::message(sprintf(
71+
'Method %s::%s() throws exception %s but the PHPDoc contains @throws void.',
72+
$methodReflection->getDeclaringClass()->getDisplayName(),
73+
$methodReflection->getName(),
74+
$throwPointType->describe(VerbosityLevel::typeOnly())
75+
))->line($throwPoint->getNode()->getLine())->build();
76+
}
77+
}
78+
79+
return $errors;
80+
}
81+
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<ThrowsVoidFunctionWithExplicitThrowPointRule>
10+
*/
11+
class ThrowsVoidFunctionWithExplicitThrowPointRuleTest extends RuleTestCase
12+
{
13+
14+
/** @var bool */
15+
private $missingCheckedExceptionInThrows;
16+
17+
/** @var string[] */
18+
private $checkedExceptionClasses;
19+
20+
protected function getRule(): Rule
21+
{
22+
return new ThrowsVoidFunctionWithExplicitThrowPointRule(new DefaultExceptionTypeResolver(
23+
$this->createReflectionProvider(),
24+
[],
25+
[],
26+
[],
27+
$this->checkedExceptionClasses
28+
), $this->missingCheckedExceptionInThrows);
29+
}
30+
31+
public function dataRule(): array
32+
{
33+
return [
34+
[
35+
true,
36+
[],
37+
[],
38+
],
39+
[
40+
false,
41+
['DifferentException'],
42+
[
43+
[
44+
'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.',
45+
15,
46+
],
47+
],
48+
],
49+
[
50+
false,
51+
[\ThrowsVoidFunction\MyException::class],
52+
[],
53+
],
54+
];
55+
}
56+
57+
/**
58+
* @dataProvider dataRule
59+
* @param bool $missingCheckedExceptionInThrows
60+
* @param string[] $checkedExceptionClasses
61+
* @param mixed[] $errors
62+
*/
63+
public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void
64+
{
65+
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
66+
$this->checkedExceptionClasses = $checkedExceptionClasses;
67+
$this->analyse([__DIR__ . '/data/throws-void-function.php'], $errors);
68+
}
69+
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<ThrowsVoidMethodWithExplicitThrowPointRule>
10+
*/
11+
class ThrowsVoidMethodWithExplicitThrowPointRuleTest extends RuleTestCase
12+
{
13+
14+
/** @var bool */
15+
private $missingCheckedExceptionInThrows;
16+
17+
/** @var string[] */
18+
private $checkedExceptionClasses;
19+
20+
protected function getRule(): Rule
21+
{
22+
return new ThrowsVoidMethodWithExplicitThrowPointRule(new DefaultExceptionTypeResolver(
23+
$this->createReflectionProvider(),
24+
[],
25+
[],
26+
[],
27+
$this->checkedExceptionClasses
28+
), $this->missingCheckedExceptionInThrows);
29+
}
30+
31+
public function dataRule(): array
32+
{
33+
return [
34+
[
35+
true,
36+
[],
37+
[],
38+
],
39+
[
40+
false,
41+
['DifferentException'],
42+
[
43+
[
44+
'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.',
45+
18,
46+
],
47+
],
48+
],
49+
[
50+
false,
51+
[\ThrowsVoidMethod\MyException::class],
52+
[],
53+
],
54+
];
55+
}
56+
57+
/**
58+
* @dataProvider dataRule
59+
* @param bool $missingCheckedExceptionInThrows
60+
* @param string[] $checkedExceptionClasses
61+
* @param mixed[] $errors
62+
*/
63+
public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void
64+
{
65+
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
66+
$this->checkedExceptionClasses = $checkedExceptionClasses;
67+
$this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors);
68+
}
69+
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace ThrowsVoidFunction;
4+
5+
class MyException extends \Exception
6+
{
7+
8+
}
9+
10+
/**
11+
* @throws void
12+
*/
13+
function foo(): void
14+
{
15+
throw new MyException();
16+
}

0 commit comments

Comments
 (0)