|
2 | 2 |
|
3 | 3 | namespace PHPStan\Rules\Exceptions; |
4 | 4 |
|
| 5 | +use PhpParser\Comment\Doc; |
5 | 6 | use PhpParser\Node; |
6 | 7 | use PHPStan\Analyser\Scope; |
7 | 8 | use PHPStan\Node\MethodReturnStatementsNode; |
| 9 | +use PHPStan\Reflection\ClassReflection; |
8 | 10 | use PHPStan\Rules\Rule; |
9 | 11 | use PHPStan\Rules\RuleErrorBuilder; |
10 | 12 | use PHPStan\Type\FileTypeMapper; |
| 13 | +use PHPStan\Type\TypeUtils; |
| 14 | +use PHPStan\Type\VerbosityLevel; |
| 15 | +use function array_diff; |
| 16 | +use function array_map; |
| 17 | +use function count; |
| 18 | +use function implode; |
11 | 19 | use function sprintf; |
12 | 20 |
|
13 | 21 | /** |
@@ -53,44 +61,60 @@ public function processNode(Node $node, Scope $scope): array |
53 | 61 | } |
54 | 62 |
|
55 | 63 | $unusedThrowClasses = $this->check->check($throwType, $statementResult->getThrowPoints()); |
56 | | - if (!$this->tooWideImplicitThrows) { |
57 | | - $docComment = $node->getDocComment(); |
58 | | - if ($docComment === null) { |
59 | | - return []; |
60 | | - } |
| 64 | + if (count($unusedThrowClasses) === 0) { |
| 65 | + return []; |
| 66 | + } |
61 | 67 |
|
62 | | - $classReflection = $node->getClassReflection(); |
63 | | - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( |
64 | | - $scope->getFile(), |
65 | | - $classReflection->getName(), |
66 | | - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, |
67 | | - $method->getName(), |
68 | | - $docComment->getText(), |
69 | | - ); |
| 68 | + $isThrowTypeExplicit = $this->isThrowTypeExplicit( |
| 69 | + $node->getDocComment(), |
| 70 | + $scope, |
| 71 | + $node->getClassReflection(), |
| 72 | + $method->getName(), |
| 73 | + ); |
70 | 74 |
|
71 | | - if ($resolvedPhpDoc->getThrowsTag() === null) { |
72 | | - return []; |
73 | | - } |
74 | | - |
75 | | - $explicitThrowType = $resolvedPhpDoc->getThrowsTag()->getType(); |
76 | | - if ($explicitThrowType->equals($throwType)) { |
77 | | - return []; |
78 | | - } |
| 75 | + if (!$isThrowTypeExplicit && !$this->tooWideImplicitThrows) { |
| 76 | + return []; |
79 | 77 | } |
80 | 78 |
|
| 79 | + $throwClasses = array_map(static fn ($type) => $type->describe(VerbosityLevel::typeOnly()), TypeUtils::flattenTypes($throwType)); |
| 80 | + $usedClasses = array_diff($throwClasses, $unusedThrowClasses); |
| 81 | + |
81 | 82 | $errors = []; |
82 | 83 | foreach ($unusedThrowClasses as $throwClass) { |
83 | | - $errors[] = RuleErrorBuilder::message(sprintf( |
| 84 | + $builder = RuleErrorBuilder::message(sprintf( |
84 | 85 | 'Method %s::%s() has %s in PHPDoc @throws tag but it\'s not thrown.', |
85 | 86 | $method->getDeclaringClass()->getDisplayName(), |
86 | 87 | $method->getName(), |
87 | 88 | $throwClass, |
88 | | - )) |
89 | | - ->identifier('throws.unusedType') |
90 | | - ->build(); |
| 89 | + ))->identifier('throws.unusedType'); |
| 90 | + |
| 91 | + if (!$isThrowTypeExplicit) { |
| 92 | + $builder->tip(sprintf( |
| 93 | + 'You can narrow the thrown type with PHPDoc tag @throws %s.', |
| 94 | + count($usedClasses) === 0 ? 'void' : implode('|', $usedClasses), |
| 95 | + )); |
| 96 | + } |
| 97 | + $errors[] = $builder->build(); |
91 | 98 | } |
92 | 99 |
|
93 | 100 | return $errors; |
94 | 101 | } |
95 | 102 |
|
| 103 | + private function isThrowTypeExplicit(?Doc $docComment, Scope $scope, ClassReflection $classReflection, string $methodName): bool |
| 104 | + { |
| 105 | + if ($docComment === null) { |
| 106 | + return false; |
| 107 | + } |
| 108 | + |
| 109 | + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( |
| 110 | + $scope->getFile(), |
| 111 | + $classReflection->getName(), |
| 112 | + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, |
| 113 | + $methodName, |
| 114 | + $docComment->getText(), |
| 115 | + ); |
| 116 | + |
| 117 | + return $resolvedPhpDoc->getThrowsTag() !== null; |
| 118 | + } |
| 119 | + |
96 | 120 | } |
0 commit comments