1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add InvalidCatch and InvalidThrow to prevent erroneous exceptions

Fix #411 and fix #412
This commit is contained in:
Matthew Brown 2017-12-28 20:40:28 +01:00
parent dd0f046aee
commit b8c349166e
6 changed files with 106 additions and 5 deletions

View File

@ -106,6 +106,7 @@
<xs:element name="InvalidArrayAssignment" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidArrayOffset" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidCast" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidCatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidClass" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidClone" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidParamDefault" type="IssueHandlerType" minOccurs="0" />
@ -124,6 +125,7 @@
<xs:element name="InvalidScope" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidStaticInvocation" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidStaticVariable" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidThrow" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidToString" type="IssueHandlerType" minOccurs="0" />
<xs:element name="LessSpecificReturnStatement" type="IssueHandlerType" minOccurs="0" />
<xs:element name="LessSpecificReturnType" type="IssueHandlerType" minOccurs="0" />

View File

@ -2,15 +2,19 @@
namespace Psalm\Checker\Statements\Block;
use PhpParser;
use Psalm\Checker\ClassChecker;
use Psalm\Checker\ClassLikeChecker;
use Psalm\Checker\InterfaceChecker;
use Psalm\Checker\ScopeChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\InvalidCatch;
use Psalm\IssueBuffer;
use Psalm\Scope\LoopScope;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
class TryChecker
{
@ -111,6 +115,27 @@ class TryChecker
}
}
$exception_type = new Union([new TNamedObject('Exception'), new TNamedObject('Throwable')]);
if ((ClassChecker::classExists($project_checker, $fq_catch_class)
&& strtolower($fq_catch_class) !== 'exception'
&& !(ClassChecker::classExtends($project_checker, $fq_catch_class, 'Exception')
|| ClassChecker::classImplements($project_checker, $fq_catch_class, 'Throwable')))
|| (InterfaceChecker::interfaceExists($project_checker, $fq_catch_class)
&& strtolower($fq_catch_class) !== 'throwable'
&& !InterfaceChecker::interfaceExtends($project_checker, $fq_catch_class, 'Throwable'))
) {
if (IssueBuffer::accepts(
new InvalidCatch(
'Class/interface ' . $fq_catch_class . ' cannot be caught',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
$fq_catch_classes[] = $fq_catch_class;
}

View File

@ -22,6 +22,7 @@ use Psalm\Issue\ContinueOutsideLoop;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\InvalidGlobal;
use Psalm\Issue\InvalidReturnStatement;
use Psalm\Issue\InvalidThrow;
use Psalm\Issue\LessSpecificReturnStatement;
use Psalm\Issue\MissingFile;
use Psalm\Issue\UnevaluatedCode;
@ -31,6 +32,8 @@ use Psalm\IssueBuffer;
use Psalm\Scope\LoopScope;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
class StatementsChecker extends SourceChecker implements StatementsSource
{
@ -210,7 +213,7 @@ class StatementsChecker extends SourceChecker implements StatementsSource
$this->analyzeReturn($project_checker, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
$has_returned = true;
$this->analyzeThrow($stmt, $context);
$this->analyzeThrow($project_checker, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
SwitchChecker::analyze($this, $stmt, $context, $loop_scope);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
@ -1065,7 +1068,7 @@ class StatementsChecker extends SourceChecker implements StatementsSource
. 'type \'' . $this->local_return_type . '\' for ' . $cased_method_id,
new CodeLocation($this->source, $stmt)
),
$this->getSuppressedIssues()
$this->getSuppressedIssues()
)) {
return false;
}
@ -1084,9 +1087,29 @@ class StatementsChecker extends SourceChecker implements StatementsSource
*
* @return false|null
*/
private function analyzeThrow(PhpParser\Node\Stmt\Throw_ $stmt, Context $context)
private function analyzeThrow(ProjectChecker $project_checker, PhpParser\Node\Stmt\Throw_ $stmt, Context $context)
{
return ExpressionChecker::analyze($this, $stmt->expr, $context);
if (ExpressionChecker::analyze($this, $stmt->expr, $context) === false) {
return false;
}
if (isset($stmt->expr->inferredType) && !$stmt->expr->inferredType->isMixed()) {
$throw_type = $stmt->expr->inferredType;
$exception_type = new Union([new TNamedObject('Exception'), new TNamedObject('Throwable')]);
if (!TypeChecker::isContainedBy($project_checker, $throw_type, $exception_type)) {
if (IssueBuffer::accepts(
new InvalidThrow(
'Cannot throw ' . $throw_type,
new CodeLocation($this->source, $stmt)
),
$this->getSuppressedIssues()
)) {
return false;
}
}
}
}
/**

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class InvalidCatch extends CodeError
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class InvalidThrow extends CodeError
{
}

View File

@ -4,6 +4,7 @@ namespace Psalm\Tests;
class TryCatchTest extends TestCase
{
use Traits\FileCheckerValidCodeParseTestTrait;
use Traits\FileCheckerInvalidCodeParseTestTrait;
/**
* @return array
@ -11,17 +12,30 @@ class TryCatchTest extends TestCase
public function providerFileCheckerValidCodeParse()
{
return [
'PHP7-interfaceHasGetMessage' => [
'PHP7-addThrowableInterfaceType' => [
'<?php
interface CustomThrowable {}
class CustomException extends Exception implements CustomThrowable {}
/** @psalm-suppress InvalidCatch */
try {
throw new CustomException("Bad");
} catch (CustomThrowable $e) {
echo $e->getMessage();
}',
],
'PHP7-rethrowInterfaceExceptionWithoutInvalidThrow' => [
'<?php
interface CustomThrowable {}
class CustomException extends Exception implements CustomThrowable {}
/** @psalm-suppress InvalidCatch */
try {
throw new CustomException("Bad");
} catch (CustomThrowable $e) {
throw $e;
}',
],
'tryCatchVar' => [
'<?php
try {
@ -34,6 +48,31 @@ class TryCatchTest extends TestCase
'$worked' => 'bool',
],
],
];
}
/**
* @return array
*/
public function providerFileCheckerInvalidCodeParse()
{
return [
'invalidCatchClass' => [
'<?php
class A {}
try {
$worked = true;
}
catch (A $e) {}',
'error_message' => 'InvalidCatch',
],
'invalidThrowClass' => [
'<?php
class A {}
throw new A();',
'error_message' => 'InvalidThrow',
],
];
}
}