Skip to content

Commit a919257

Browse files
authored
ForbidNotNormalizedTypeRule: support also @throws (#232)
1 parent 7769c17 commit a919257

File tree

2 files changed

+99
-13
lines changed

2 files changed

+99
-13
lines changed

src/Rule/ForbidNotNormalizedTypeRule.php

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PHPStan\PhpDocParser\Ast\Node as PhpDocRootNode;
2525
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
2626
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
27+
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
2728
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
2829
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
2930
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
@@ -41,6 +42,7 @@
4142
use function get_object_vars;
4243
use function implode;
4344
use function is_array;
45+
use function is_int;
4446
use function is_object;
4547
use function is_string;
4648
use function spl_object_id;
@@ -92,7 +94,7 @@ public function processNode(
9294
{
9395
if ($node instanceof FunctionLike) {
9496
return array_merge(
95-
$this->checkParamAndReturnPhpDoc($node, $scope),
97+
$this->checkParamAndReturnAndThrowsPhpDoc($node, $scope),
9698
$this->checkParamAndReturnNativeType($node, $scope),
9799
);
98100
}
@@ -123,7 +125,7 @@ private function checkCatchNativeType(Catch_ $node, Scope $scope): array
123125
/**
124126
* @return list<RuleError>
125127
*/
126-
private function checkParamAndReturnPhpDoc(
128+
private function checkParamAndReturnAndThrowsPhpDoc(
127129
FunctionLike $node,
128130
Scope $scope
129131
): array
@@ -147,6 +149,7 @@ private function checkParamAndReturnPhpDoc(
147149
$errors,
148150
$this->processParamTags($node, $phpdocNode->getParamTagValues(), $nameScope),
149151
$this->processReturnTags($node, $phpdocNode->getReturnTagValues(), $nameScope),
152+
$this->processThrowsTags($node, $phpdocNode->getThrowsTagValues(), $nameScope),
150153
);
151154
}
152155

@@ -315,12 +318,13 @@ public function processParamTags(
315318
$errors = [];
316319

317320
foreach ($paramTagValues as $paramTagValue) {
318-
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($paramTagValue->type) as $multiTypeNode) {
321+
$line = $this->getPhpDocLine($sourceNode, $paramTagValue);
322+
323+
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($paramTagValue->type, $line) as $multiTypeNode) {
319324
$newErrors = $this->processMultiTypePhpDocNode(
320325
$multiTypeNode,
321326
$nameSpace,
322327
"parameter {$paramTagValue->parameterName}",
323-
$this->getPhpDocLine($sourceNode, $paramTagValue),
324328
);
325329
$errors = array_merge($errors, $newErrors);
326330
}
@@ -342,7 +346,9 @@ public function processVarTags(
342346
$errors = [];
343347

344348
foreach ($varTagValues as $varTagValue) {
345-
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($varTagValue->type) as $multiTypeNode) {
349+
$line = $this->getPhpDocLine($originalNode, $varTagValue);
350+
351+
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($varTagValue->type, $line) as $multiTypeNode) {
346352
$identification = $varTagValue->variableName !== ''
347353
? "variable {$varTagValue->variableName}"
348354
: null;
@@ -351,7 +357,6 @@ public function processVarTags(
351357
$multiTypeNode,
352358
$nameSpace,
353359
$identification,
354-
$this->getPhpDocLine($originalNode, $varTagValue),
355360
);
356361
$errors = array_merge($errors, $newErrors);
357362
}
@@ -373,20 +378,57 @@ public function processReturnTags(
373378
$errors = [];
374379

375380
foreach ($returnTagValues as $returnTagValue) {
376-
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($returnTagValue->type) as $multiTypeNode) {
377-
$newErrors = $this->processMultiTypePhpDocNode($multiTypeNode, $nameSpace, 'return', $this->getPhpDocLine($originalNode, $returnTagValue));
381+
$line = $this->getPhpDocLine($originalNode, $returnTagValue);
382+
383+
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($returnTagValue->type, $line) as $multiTypeNode) {
384+
$newErrors = $this->processMultiTypePhpDocNode($multiTypeNode, $nameSpace, 'return');
378385
$errors = array_merge($errors, $newErrors);
379386
}
380387
}
381388

382389
return $errors;
383390
}
384391

392+
/**
393+
* @param array<ThrowsTagValueNode> $throwsTagValues
394+
* @return list<RuleError>
395+
*/
396+
public function processThrowsTags(
397+
PhpParserNode $originalNode,
398+
array $throwsTagValues,
399+
NameScope $nameSpace
400+
): array
401+
{
402+
$thrownTypes = [];
403+
404+
foreach ($throwsTagValues as $throwsTagValue) {
405+
$line = $this->getPhpDocLine($originalNode, $throwsTagValue);
406+
$multiTypeNodes = $this->extractUnionAndIntersectionPhpDocTypeNodes($throwsTagValue->type, $line);
407+
408+
if ($multiTypeNodes === []) {
409+
$innerType = $throwsTagValue->type;
410+
$innerType->setAttribute('line', $line);
411+
412+
$thrownTypes[] = $innerType;
413+
} else {
414+
foreach ($multiTypeNodes as $multiTypeNode) {
415+
foreach ($multiTypeNode->types as $typeNode) {
416+
$thrownTypes[] = $typeNode;
417+
}
418+
}
419+
}
420+
}
421+
422+
$unionNode = new UnionTypeNode($thrownTypes);
423+
return $this->processMultiTypePhpDocNode($unionNode, $nameSpace, 'throws');
424+
}
425+
385426
/**
386427
* @return list<UnionTypeNode|IntersectionTypeNode>
387428
*/
388-
private function extractUnionAndIntersectionPhpDocTypeNodes(TypeNode $typeNode): array
429+
private function extractUnionAndIntersectionPhpDocTypeNodes(TypeNode $typeNode, int $line): array
389430
{
431+
/** @var list<UnionTypeNode|IntersectionTypeNode> $nodes */
390432
$nodes = [];
391433
$this->traversePhpDocTypeNode($typeNode, static function (TypeNode $typeNode) use (&$nodes): void {
392434
if ($typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode) {
@@ -397,6 +439,13 @@ private function extractUnionAndIntersectionPhpDocTypeNodes(TypeNode $typeNode):
397439
$nodes[] = new UnionTypeNode([$typeNode->type, new IdentifierTypeNode('null')]);
398440
}
399441
});
442+
443+
foreach ($nodes as $node) {
444+
foreach ($node->types as $innerType) {
445+
$innerType->setAttribute('line', $line);
446+
}
447+
}
448+
400449
return $nodes;
401450
}
402451

@@ -513,8 +562,7 @@ private function printPhpParserNode(PhpParserNode $node): string
513562
private function processMultiTypePhpDocNode(
514563
TypeNode $multiTypeNode,
515564
NameScope $nameSpace,
516-
?string $identification,
517-
int $line
565+
?string $identification
518566
): array
519567
{
520568
$errors = [];
@@ -525,6 +573,7 @@ private function processMultiTypePhpDocNode(
525573
foreach ($multiTypeNode->types as $type) {
526574
if ($type instanceof UnionTypeNode) {
527575
$dnf = $this->typeNodeResolver->resolve($multiTypeNode, $nameSpace)->describe(VerbosityLevel::typeOnly());
576+
$line = $this->extractLineFromPhpDocTypeNode($type);
528577

529578
$errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNode}{$forWhat}: this is not disjunctive normal form, use {$dnf}")
530579
->line($line)
@@ -544,17 +593,20 @@ private function processMultiTypePhpDocNode(
544593
$typeA = $this->typeNodeResolver->resolve($typeNodeA, $nameSpace);
545594
$typeB = $this->typeNodeResolver->resolve($typeNodeB, $nameSpace);
546595

596+
$typeALine = $this->extractLineFromPhpDocTypeNode($typeNodeA);
597+
$typeBLine = $this->extractLineFromPhpDocTypeNode($typeNodeB);
598+
547599
if ($typeA->isSuperTypeOf($typeB)->yes()) {
548600
$errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNode}{$forWhat}: {$typeNodeB} is a subtype of {$typeNodeA}.")
549-
->line($line)
601+
->line($typeBLine)
550602
->identifier('shipmonk.nonNormalizedType')
551603
->build();
552604
continue;
553605
}
554606

555607
if ($typeB->isSuperTypeOf($typeA)->yes()) {
556608
$errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNode}{$forWhat}: {$typeNodeA} is a subtype of {$typeNodeB}.")
557-
->line($line)
609+
->line($typeALine)
558610
->identifier('shipmonk.nonNormalizedType')
559611
->build();
560612
}
@@ -564,6 +616,17 @@ private function processMultiTypePhpDocNode(
564616
return $errors;
565617
}
566618

619+
private function extractLineFromPhpDocTypeNode(TypeNode $node): int
620+
{
621+
$line = $node->getAttribute('line');
622+
623+
if (!is_int($line)) {
624+
throw new LogicException('Missing custom line attribute in node: ' . $node);
625+
}
626+
627+
return $line;
628+
}
629+
567630
private function getPropertyNameFromNativeNode(Property $node): string
568631
{
569632
$propertyNames = [];

tests/Rule/data/ForbidNotNormalizedTypeRule/code.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,27 @@ public function testCatch()
169169
}
170170
}
171171

172+
/**
173+
* @throws InterfaceImplementor // error: Found non-normalized type (InterfaceImplementor | MyInterface) for throws: InterfaceImplementor is a subtype of MyInterface.
174+
* @throws MyInterface
175+
*/
176+
public function testThrows()
177+
{
178+
}
179+
180+
/**
181+
* @throws InterfaceImplementor|MyInterface // error: Found non-normalized type (InterfaceImplementor | MyInterface) for throws: InterfaceImplementor is a subtype of MyInterface.
182+
*/
183+
public function testThrowsUnion()
184+
{
185+
}
186+
187+
/**
188+
* @throws MyInterface|ChildOne
189+
* @throws InterfaceImplementor // error: Found non-normalized type (MyInterface | ChildOne | InterfaceImplementor) for throws: InterfaceImplementor is a subtype of MyInterface.
190+
*/
191+
public function testThrowsUnionCombined()
192+
{
193+
}
194+
172195
}

0 commit comments

Comments
 (0)