|
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