diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index 628b3e62df..26c68b89ba 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -142,7 +142,45 @@ public function resolvePhpDocForMethod( return $this->resolveMethodPhpDocFromParentClass($traitMethod, $resolvedPhpDocBlock, $declaringClass, $trait, $currentResolvedPhpDoc, $currentPositionalParameterNames); } - return $currentResolvedPhpDoc; + $result = $currentResolvedPhpDoc; + + // Inherit @throws from non-abstract trait methods + if ($result === null || $result->getThrowsTag() === null) { + foreach ($declaringClass->getTraits() as $trait) { + if (!$trait->hasNativeMethod($methodName)) { + continue; + } + + $traitMethod = $trait->getNativeMethod($methodName); + if ($traitMethod->getDocComment() === null) { + continue; + } + if ($declaringClass->getFileName() === null) { + continue; + } + + $traitResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $declaringClass->getFileName(), + $declaringClass->getName(), + $trait->getName(), + $methodName, + $traitMethod->getDocComment(), + ); + + $throwsTag = $traitResolvedPhpDoc->getThrowsTag(); + if ($throwsTag === null) { + continue; + } + + if ($result === null) { + $result = ResolvedPhpDocBlock::createEmpty(); + } + $result = $result->withThrowsTag($throwsTag); + break; + } + } + + return $result; } /** diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index be6da4fc46..1b87457e7c 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -621,6 +621,14 @@ public function getThrowsTag(): ?ThrowsTag return $this->throwsTag; } + public function withThrowsTag(ThrowsTag $throwsTag): self + { + $result = clone $this; + $result->throwsTag = $throwsTag; + + return $result; + } + /** * @return array */ diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php index 421fdb719d..83bb8ed534 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php @@ -36,6 +36,11 @@ public function testRule(): void ]); } + public function testBug10315(): void + { + $this->analyse([__DIR__ . '/data/bug-10315.php'], []); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-10315.php b/tests/PHPStan/Rules/Exceptions/data/bug-10315.php new file mode 100644 index 0000000000..12252abd60 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-10315.php @@ -0,0 +1,62 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug10315; + +class MyException extends \RuntimeException +{ +} + +trait DriverPoolAbstractTrait +{ + /** + * @throws MyException + */ + protected function driverReadMultiple(): array + { + throw new MyException(); + } +} + +trait CacheItemPoolTrait +{ + public function getItems(): array + { + try { + $result = $this->driverReadMultiple(); + } catch (MyException) { + $result = []; + } + + return $result; + } +} + +// Scenario A: base uses DriverPoolAbstractTrait, child uses CacheItemPoolTrait and overrides +abstract class AbstractPoolA +{ + use DriverPoolAbstractTrait; +} + +class RedisDriverA extends AbstractPoolA +{ + use CacheItemPoolTrait; + + protected function driverReadMultiple(): array + { + return ['key' => 'value']; + } +} + +// Scenario B: class uses both traits and overrides the method directly +class DirectDriverB +{ + use DriverPoolAbstractTrait; + use CacheItemPoolTrait; + + protected function driverReadMultiple(): array + { + return ['key' => 'value']; + } +}