From 810e50463f6d5f996b2cc0995f9995122b8e082d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 5 May 2021 14:00:36 +0200 Subject: [PATCH] MissingCheckedExceptionInThrowsCheck - report new catch position --- ...ngCheckedExceptionInFunctionThrowsRule.php | 3 +- ...singCheckedExceptionInMethodThrowsRule.php | 3 +- .../MissingCheckedExceptionInThrowsCheck.php | 49 ++++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php index 0598cd2c53..7e4b175e46 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php @@ -36,7 +36,7 @@ public function processNode(Node $node, Scope $scope): array } $errors = []; - foreach ($this->check->check($functionReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { + foreach ($this->check->check($functionReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode, $newCatchPosition]) { $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', $functionReflection->getName(), @@ -46,6 +46,7 @@ public function processNode(Node $node, Scope $scope): array ->identifier('exceptions.missingThrowsTag') ->metadata([ 'exceptionName' => $className, + 'newCatchPosition' => $newCatchPosition, 'statementDepth' => $throwPointNode->getAttribute('statementDepth'), 'statementOrder' => $throwPointNode->getAttribute('statementOrder'), ]) diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php index 7c42e37645..e18c0c1d97 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php @@ -36,7 +36,7 @@ public function processNode(Node $node, Scope $scope): array } $errors = []; - foreach ($this->check->check($methodReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { + foreach ($this->check->check($methodReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode, $newCatchPosition]) { $errors[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', $methodReflection->getDeclaringClass()->getDisplayName(), @@ -47,6 +47,7 @@ public function processNode(Node $node, Scope $scope): array ->identifier('exceptions.missingThrowsTag') ->metadata([ 'exceptionName' => $className, + 'newCatchPosition' => $newCatchPosition, 'statementDepth' => $throwPointNode->getAttribute('statementDepth'), 'statementOrder' => $throwPointNode->getAttribute('statementOrder'), ]) diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php index 38f7f34de6..650c517671 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php @@ -7,6 +7,7 @@ use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; @@ -24,7 +25,7 @@ public function __construct(ExceptionTypeResolver $exceptionTypeResolver) /** * @param Type|null $throwType * @param ThrowPoint[] $throwPoints - * @return array + * @return array */ public function check(?Type $throwType, array $throwPoints): array { @@ -53,11 +54,55 @@ public function check(?Type $throwType, array $throwPoints): array continue; } - $classes[] = [$throwPointType->describe(VerbosityLevel::typeOnly()), $throwPoint->getNode()]; + $classes[] = [$throwPointType->describe(VerbosityLevel::typeOnly()), $throwPoint->getNode(), $this->getNewCatchPosition($throwPointType, $throwPoint->getNode())]; } } return $classes; } + private function getNewCatchPosition(Type $throwPointType, Node $throwPointNode): ?int + { + if ($throwPointType instanceof TypeWithClassName) { + // to get rid of type subtraction + $throwPointType = new ObjectType($throwPointType->getClassName()); + } + $tryCatch = $this->findTryCatch($throwPointNode); + if ($tryCatch === null) { + return null; + } + + $position = 0; + foreach ($tryCatch->catches as $catch) { + $type = TypeCombinator::union(...array_map(static function (Node\Name $class): ObjectType { + return new ObjectType($class->toString()); + }, $catch->types)); + if (!$throwPointType->isSuperTypeOf($type)->yes()) { + continue; + } + + $position++; + } + + return $position; + } + + private function findTryCatch(Node $node): ?Node\Stmt\TryCatch + { + if ($node instanceof Node\FunctionLike) { + return null; + } + + if ($node instanceof Node\Stmt\TryCatch) { + return $node; + } + + $parent = $node->getAttribute('parent'); + if ($parent === null) { + return null; + } + + return $this->findTryCatch($parent); + } + }