Skip to content

Commit 8031040

Browse files
committed
Useful tip about what @throws should be added when the implicit one is too wide
1 parent 3eaad7f commit 8031040

File tree

2 files changed

+52
-25
lines changed

2 files changed

+52
-25
lines changed

src/Rules/Exceptions/TooWideMethodThrowTypeRule.php

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22

33
namespace PHPStan\Rules\Exceptions;
44

5+
use PhpParser\Comment\Doc;
56
use PhpParser\Node;
67
use PHPStan\Analyser\Scope;
78
use PHPStan\Node\MethodReturnStatementsNode;
9+
use PHPStan\Reflection\ClassReflection;
810
use PHPStan\Rules\Rule;
911
use PHPStan\Rules\RuleErrorBuilder;
1012
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;
1119
use function sprintf;
1220

1321
/**
@@ -53,44 +61,60 @@ public function processNode(Node $node, Scope $scope): array
5361
}
5462

5563
$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+
}
6167

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+
);
7074

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 [];
7977
}
8078

79+
$throwClasses = array_map(static fn ($type) => $type->describe(VerbosityLevel::typeOnly()), TypeUtils::flattenTypes($throwType));
80+
$usedClasses = array_diff($throwClasses, $unusedThrowClasses);
81+
8182
$errors = [];
8283
foreach ($unusedThrowClasses as $throwClass) {
83-
$errors[] = RuleErrorBuilder::message(sprintf(
84+
$builder = RuleErrorBuilder::message(sprintf(
8485
'Method %s::%s() has %s in PHPDoc @throws tag but it\'s not thrown.',
8586
$method->getDeclaringClass()->getDisplayName(),
8687
$method->getName(),
8788
$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();
9198
}
9299

93100
return $errors;
94101
}
95102

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+
96120
}

tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public function testRule(): void
5656
[
5757
'Method TooWideThrowsMethod\ChildClass::doFoo() has LogicException in PHPDoc @throws tag but it\'s not thrown.',
5858
87,
59+
'You can narrow the thrown type with PHPDoc tag @throws void.',
5960
],
6061
[
6162
'Method TooWideThrowsMethod\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.',
@@ -138,6 +139,7 @@ public static function dataAlwaysCheckFinal(): iterable
138139
[
139140
'Method TooWideThrowTypeAlwaysCheckFinal\Baz::doFoo() has RuntimeException in PHPDoc @throws tag but it\'s not thrown.',
140141
38,
142+
'You can narrow the thrown type with PHPDoc tag @throws LogicException.',
141143
],
142144
[
143145
'Method TooWideThrowTypeAlwaysCheckFinal\Baz::doBar() has RuntimeException in PHPDoc @throws tag but it\'s not thrown.',
@@ -171,6 +173,7 @@ public static function dataTooWideImplicitThrows(): iterable
171173
[
172174
'Method TooWideImplicitThrows\Bar::doFoo() has LogicException in PHPDoc @throws tag but it\'s not thrown.',
173175
23,
176+
'You can narrow the thrown type with PHPDoc tag @throws void.',
174177
],
175178
],
176179
];

0 commit comments

Comments
 (0)