diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 8dd3eb2315..a467561ba9 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -18,6 +18,8 @@ rules: - PHPStan\Rules\TooWideTypehints\TooWideFunctionReturnTypehintRule conditionalTags: + PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule: + phpstan.rules.rule: %featureToggles.preciseExceptionTracking% PHPStan\Rules\DeadCode\UnusedPrivateConstantRule: phpstan.rules.rule: %featureToggles.unusedClassElements% PHPStan\Rules\DeadCode\UnusedPrivateMethodRule: @@ -147,6 +149,9 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule + - class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule arguments: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0ff069f5eb..d3ed5a8d23 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -110,6 +110,16 @@ parameters: count: 1 path: src/PhpDoc/Tag/VarTag.php + - + message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#" + count: 2 + path: src/Reflection/BetterReflection/BetterReflectionProvider.php + + - + message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\NodeCompiler\\\\Exception\\\\UnableToCompileNode\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAClassReflection\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAnInterfaceReflection is never thrown in the try block\\.$#" + count: 1 + path: src/Reflection/BetterReflection/BetterReflectionProvider.php + - message: "#^Only booleans are allowed in a negated boolean, int\\|false given\\.$#" count: 1 diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 65f4e33e9a..249b4e03da 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -55,6 +55,7 @@ use PHPStan\File\FileReader; use PHPStan\Node\BooleanAndNode; use PHPStan\Node\BooleanOrNode; +use PHPStan\Node\CatchWithUnthrownExceptionNode; use PHPStan\Node\ClassConstantsNode; use PHPStan\Node\ClassMethodsNode; use PHPStan\Node\ClassPropertiesNode; @@ -1183,6 +1184,7 @@ private function processStmtNode( $throwPoints = $newThrowPoints; if (count($matchingThrowPoints) === 0) { + $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType), $scope); continue; } diff --git a/src/Node/CatchWithUnthrownExceptionNode.php b/src/Node/CatchWithUnthrownExceptionNode.php new file mode 100644 index 0000000000..765a6f5f6e --- /dev/null +++ b/src/Node/CatchWithUnthrownExceptionNode.php @@ -0,0 +1,46 @@ +getAttributes()); + $this->originalNode = $originalNode; + $this->caughtType = $caughtType; + } + + public function getOriginalNode(): Catch_ + { + return $this->originalNode; + } + + public function getCaughtType(): Type + { + return $this->caughtType; + } + + public function getType(): string + { + return 'PHPStan_Node_CatchWithUnthrownExceptionNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php new file mode 100644 index 0000000000..025e87690d --- /dev/null +++ b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php @@ -0,0 +1,32 @@ + + */ +class CatchWithUnthrownExceptionRule implements Rule +{ + + public function getNodeType(): string + { + return CatchWithUnthrownExceptionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message( + sprintf('Dead catch - %s is never thrown in the try block.', $node->getCaughtType()->describe(VerbosityLevel::typeOnly())) + )->line($node->getLine())->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php new file mode 100644 index 0000000000..dfdd38b488 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -0,0 +1,38 @@ +analyse([__DIR__ . '/data/unthrown-exception.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 12, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 21, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 38, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 47, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php new file mode 100644 index 0000000000..95c6bc1df7 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -0,0 +1,63 @@ +throwIae(); + } catch (\InvalidArgumentException $e) { + + } catch (\Throwable $e) { + // dead + } + } + + public function doLorem(): void + { + try { + $this->throwIae(); + } catch (\RuntimeException $e) { + // dead + } catch (\Throwable $e) { + + } + } + + public function doIpsum(): void + { + try { + $this->throwIae(); + } catch (\Throwable $e) { + + } + } + +}