Skip to content

Commit

Permalink
Bleeding edge - check that function with @throws void does not have…
Browse files Browse the repository at this point in the history
… an explicit throw point
  • Loading branch information
ondrejmirtes committed Sep 12, 2021
1 parent 3624e66 commit 8b3382a
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 1 deletion.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ parameters:
classConstants: true
privateStaticCall: true
overridingProperty: true
throwsVoid: true
stubFiles:
- ../stubs/arrayFunctions.stub
16 changes: 16 additions & 0 deletions conf/config.level3.neon
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ rules:
conditionalTags:
PHPStan\Rules\Arrays\ArrayDestructuringRule:
phpstan.rules.rule: %featureToggles.arrayDestructuring%
PHPStan\Rules\Exceptions\ThrowsVoidFunctionWithExplicitThrowPointRule:
phpstan.rules.rule: %featureToggles.throwsVoid%
PHPStan\Rules\Exceptions\ThrowsVoidMethodWithExplicitThrowPointRule:
phpstan.rules.rule: %featureToggles.throwsVoid%

parameters:
checkPhpDocMethodSignatures: true
Expand Down Expand Up @@ -56,6 +60,18 @@ services:
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Exceptions\ThrowsVoidFunctionWithExplicitThrowPointRule
arguments:
exceptionTypeResolver: @exceptionTypeResolver
missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows%

-
class: PHPStan\Rules\Exceptions\ThrowsVoidMethodWithExplicitThrowPointRule
arguments:
exceptionTypeResolver: @exceptionTypeResolver
missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows%

-
class: PHPStan\Rules\Functions\ReturnTypeRule
arguments:
Expand Down
4 changes: 3 additions & 1 deletion conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ parameters:
classConstants: false
privateStaticCall: false
overridingProperty: false
throwsVoid: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -237,7 +238,8 @@ parametersSchema:
finalByPhpDocTag: bool(),
classConstants: bool(),
privateStaticCall: bool(),
overridingProperty: bool()
overridingProperty: bool(),
throwsVoid: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Exceptions;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\FunctionReturnStatementsNode;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\VerbosityLevel;
use PHPStan\Type\VoidType;

/**
* @implements Rule<FunctionReturnStatementsNode>
*/
class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule
{

private ExceptionTypeResolver $exceptionTypeResolver;

private bool $missingCheckedExceptionInThrows;

public function __construct(
ExceptionTypeResolver $exceptionTypeResolver,
bool $missingCheckedExceptionInThrows
)
{
$this->exceptionTypeResolver = $exceptionTypeResolver;
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
}

public function getNodeType(): string
{
return FunctionReturnStatementsNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
if ($this->missingCheckedExceptionInThrows) {
return [];
}

$statementResult = $node->getStatementResult();
$functionReflection = $scope->getFunction();
if (!$functionReflection instanceof FunctionReflection) {
throw new \PHPStan\ShouldNotHappenException();
}

if (!$functionReflection->getThrowType() instanceof VoidType) {
return [];
}

$errors = [];
foreach ($statementResult->getThrowPoints() as $throwPoint) {
if (!$throwPoint->isExplicit()) {
continue;
}

foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) {
if (
$throwPointType instanceof TypeWithClassName
&& $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope())
) {
continue;
}

$errors[] = RuleErrorBuilder::message(sprintf(
'Function %s() throws exception %s but the PHPDoc contains @throws void.',
$functionReflection->getName(),
$throwPointType->describe(VerbosityLevel::typeOnly())
))->line($throwPoint->getNode()->getLine())->build();
}
}

return $errors;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Exceptions;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\MethodReturnStatementsNode;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\VerbosityLevel;
use PHPStan\Type\VoidType;

/**
* @implements Rule<MethodReturnStatementsNode>
*/
class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule
{

private ExceptionTypeResolver $exceptionTypeResolver;

private bool $missingCheckedExceptionInThrows;

public function __construct(
ExceptionTypeResolver $exceptionTypeResolver,
bool $missingCheckedExceptionInThrows
)
{
$this->exceptionTypeResolver = $exceptionTypeResolver;
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
}

public function getNodeType(): string
{
return MethodReturnStatementsNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
if ($this->missingCheckedExceptionInThrows) {
return [];
}

$statementResult = $node->getStatementResult();
$methodReflection = $scope->getFunction();
if (!$methodReflection instanceof MethodReflection) {
throw new \PHPStan\ShouldNotHappenException();
}

if (!$methodReflection->getThrowType() instanceof VoidType) {
return [];
}

$errors = [];
foreach ($statementResult->getThrowPoints() as $throwPoint) {
if (!$throwPoint->isExplicit()) {
continue;
}

foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) {
if (
$throwPointType instanceof TypeWithClassName
&& $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope())
) {
continue;
}

$errors[] = RuleErrorBuilder::message(sprintf(
'Method %s::%s() throws exception %s but the PHPDoc contains @throws void.',
$methodReflection->getDeclaringClass()->getDisplayName(),
$methodReflection->getName(),
$throwPointType->describe(VerbosityLevel::typeOnly())
))->line($throwPoint->getNode()->getLine())->build();
}
}

return $errors;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ThrowsVoidFunctionWithExplicitThrowPointRule>
*/
class ThrowsVoidFunctionWithExplicitThrowPointRuleTest extends RuleTestCase
{

/** @var bool */
private $missingCheckedExceptionInThrows;

/** @var string[] */
private $checkedExceptionClasses;

protected function getRule(): Rule
{
return new ThrowsVoidFunctionWithExplicitThrowPointRule(new DefaultExceptionTypeResolver(
$this->createReflectionProvider(),
[],
[],
[],
$this->checkedExceptionClasses
), $this->missingCheckedExceptionInThrows);
}

public function dataRule(): array
{
return [
[
true,
[],
[],
],
[
false,
['DifferentException'],
[
[
'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.',
15,
],
],
],
[
false,
[\ThrowsVoidFunction\MyException::class],
[],
],
];
}

/**
* @dataProvider dataRule
* @param bool $missingCheckedExceptionInThrows
* @param string[] $checkedExceptionClasses
* @param mixed[] $errors
*/
public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void
{
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
$this->checkedExceptionClasses = $checkedExceptionClasses;
$this->analyse([__DIR__ . '/data/throws-void-function.php'], $errors);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ThrowsVoidMethodWithExplicitThrowPointRule>
*/
class ThrowsVoidMethodWithExplicitThrowPointRuleTest extends RuleTestCase
{

/** @var bool */
private $missingCheckedExceptionInThrows;

/** @var string[] */
private $checkedExceptionClasses;

protected function getRule(): Rule
{
return new ThrowsVoidMethodWithExplicitThrowPointRule(new DefaultExceptionTypeResolver(
$this->createReflectionProvider(),
[],
[],
[],
$this->checkedExceptionClasses
), $this->missingCheckedExceptionInThrows);
}

public function dataRule(): array
{
return [
[
true,
[],
[],
],
[
false,
['DifferentException'],
[
[
'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.',
18,
],
],
],
[
false,
[\ThrowsVoidMethod\MyException::class],
[],
],
];
}

/**
* @dataProvider dataRule
* @param bool $missingCheckedExceptionInThrows
* @param string[] $checkedExceptionClasses
* @param mixed[] $errors
*/
public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void
{
$this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows;
$this->checkedExceptionClasses = $checkedExceptionClasses;
$this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors);
}

}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/throws-void-function.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace ThrowsVoidFunction;

class MyException extends \Exception
{

}

/**
* @throws void
*/
function foo(): void
{
throw new MyException();
}
Loading

0 comments on commit 8b3382a

Please sign in to comment.