Skip to content

Commit

Permalink
ForbidNotNormalizedTypeRule: support also @throws (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal committed Apr 2, 2024
1 parent 7769c17 commit a919257
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 13 deletions.
89 changes: 76 additions & 13 deletions src/Rule/ForbidNotNormalizedTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PHPStan\PhpDocParser\Ast\Node as PhpDocRootNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
Expand All @@ -41,6 +42,7 @@
use function get_object_vars;
use function implode;
use function is_array;
use function is_int;
use function is_object;
use function is_string;
use function spl_object_id;
Expand Down Expand Up @@ -92,7 +94,7 @@ public function processNode(
{
if ($node instanceof FunctionLike) {
return array_merge(
$this->checkParamAndReturnPhpDoc($node, $scope),
$this->checkParamAndReturnAndThrowsPhpDoc($node, $scope),
$this->checkParamAndReturnNativeType($node, $scope),
);
}
Expand Down Expand Up @@ -123,7 +125,7 @@ private function checkCatchNativeType(Catch_ $node, Scope $scope): array
/**
* @return list<RuleError>
*/
private function checkParamAndReturnPhpDoc(
private function checkParamAndReturnAndThrowsPhpDoc(
FunctionLike $node,
Scope $scope
): array
Expand All @@ -147,6 +149,7 @@ private function checkParamAndReturnPhpDoc(
$errors,
$this->processParamTags($node, $phpdocNode->getParamTagValues(), $nameScope),
$this->processReturnTags($node, $phpdocNode->getReturnTagValues(), $nameScope),
$this->processThrowsTags($node, $phpdocNode->getThrowsTagValues(), $nameScope),
);
}

Expand Down Expand Up @@ -315,12 +318,13 @@ public function processParamTags(
$errors = [];

foreach ($paramTagValues as $paramTagValue) {
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($paramTagValue->type) as $multiTypeNode) {
$line = $this->getPhpDocLine($sourceNode, $paramTagValue);

foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($paramTagValue->type, $line) as $multiTypeNode) {
$newErrors = $this->processMultiTypePhpDocNode(
$multiTypeNode,
$nameSpace,
"parameter {$paramTagValue->parameterName}",
$this->getPhpDocLine($sourceNode, $paramTagValue),
);
$errors = array_merge($errors, $newErrors);
}
Expand All @@ -342,7 +346,9 @@ public function processVarTags(
$errors = [];

foreach ($varTagValues as $varTagValue) {
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($varTagValue->type) as $multiTypeNode) {
$line = $this->getPhpDocLine($originalNode, $varTagValue);

foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($varTagValue->type, $line) as $multiTypeNode) {
$identification = $varTagValue->variableName !== ''
? "variable {$varTagValue->variableName}"
: null;
Expand All @@ -351,7 +357,6 @@ public function processVarTags(
$multiTypeNode,
$nameSpace,
$identification,
$this->getPhpDocLine($originalNode, $varTagValue),
);
$errors = array_merge($errors, $newErrors);
}
Expand All @@ -373,20 +378,57 @@ public function processReturnTags(
$errors = [];

foreach ($returnTagValues as $returnTagValue) {
foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($returnTagValue->type) as $multiTypeNode) {
$newErrors = $this->processMultiTypePhpDocNode($multiTypeNode, $nameSpace, 'return', $this->getPhpDocLine($originalNode, $returnTagValue));
$line = $this->getPhpDocLine($originalNode, $returnTagValue);

foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($returnTagValue->type, $line) as $multiTypeNode) {
$newErrors = $this->processMultiTypePhpDocNode($multiTypeNode, $nameSpace, 'return');
$errors = array_merge($errors, $newErrors);
}
}

return $errors;
}

/**
* @param array<ThrowsTagValueNode> $throwsTagValues
* @return list<RuleError>
*/
public function processThrowsTags(
PhpParserNode $originalNode,
array $throwsTagValues,
NameScope $nameSpace
): array
{
$thrownTypes = [];

foreach ($throwsTagValues as $throwsTagValue) {
$line = $this->getPhpDocLine($originalNode, $throwsTagValue);
$multiTypeNodes = $this->extractUnionAndIntersectionPhpDocTypeNodes($throwsTagValue->type, $line);

if ($multiTypeNodes === []) {
$innerType = $throwsTagValue->type;
$innerType->setAttribute('line', $line);

$thrownTypes[] = $innerType;
} else {
foreach ($multiTypeNodes as $multiTypeNode) {
foreach ($multiTypeNode->types as $typeNode) {
$thrownTypes[] = $typeNode;
}
}
}
}

$unionNode = new UnionTypeNode($thrownTypes);
return $this->processMultiTypePhpDocNode($unionNode, $nameSpace, 'throws');
}

/**
* @return list<UnionTypeNode|IntersectionTypeNode>
*/
private function extractUnionAndIntersectionPhpDocTypeNodes(TypeNode $typeNode): array
private function extractUnionAndIntersectionPhpDocTypeNodes(TypeNode $typeNode, int $line): array
{
/** @var list<UnionTypeNode|IntersectionTypeNode> $nodes */
$nodes = [];
$this->traversePhpDocTypeNode($typeNode, static function (TypeNode $typeNode) use (&$nodes): void {
if ($typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode) {
Expand All @@ -397,6 +439,13 @@ private function extractUnionAndIntersectionPhpDocTypeNodes(TypeNode $typeNode):
$nodes[] = new UnionTypeNode([$typeNode->type, new IdentifierTypeNode('null')]);
}
});

foreach ($nodes as $node) {
foreach ($node->types as $innerType) {
$innerType->setAttribute('line', $line);
}
}

return $nodes;
}

Expand Down Expand Up @@ -513,8 +562,7 @@ private function printPhpParserNode(PhpParserNode $node): string
private function processMultiTypePhpDocNode(
TypeNode $multiTypeNode,
NameScope $nameSpace,
?string $identification,
int $line
?string $identification
): array
{
$errors = [];
Expand All @@ -525,6 +573,7 @@ private function processMultiTypePhpDocNode(
foreach ($multiTypeNode->types as $type) {
if ($type instanceof UnionTypeNode) {
$dnf = $this->typeNodeResolver->resolve($multiTypeNode, $nameSpace)->describe(VerbosityLevel::typeOnly());
$line = $this->extractLineFromPhpDocTypeNode($type);

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

$typeALine = $this->extractLineFromPhpDocTypeNode($typeNodeA);
$typeBLine = $this->extractLineFromPhpDocTypeNode($typeNodeB);

if ($typeA->isSuperTypeOf($typeB)->yes()) {
$errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNode}{$forWhat}: {$typeNodeB} is a subtype of {$typeNodeA}.")
->line($line)
->line($typeBLine)
->identifier('shipmonk.nonNormalizedType')
->build();
continue;
}

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

private function extractLineFromPhpDocTypeNode(TypeNode $node): int
{
$line = $node->getAttribute('line');

if (!is_int($line)) {
throw new LogicException('Missing custom line attribute in node: ' . $node);
}

return $line;
}

private function getPropertyNameFromNativeNode(Property $node): string
{
$propertyNames = [];
Expand Down
23 changes: 23 additions & 0 deletions tests/Rule/data/ForbidNotNormalizedTypeRule/code.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,27 @@ public function testCatch()
}
}

/**
* @throws InterfaceImplementor // error: Found non-normalized type (InterfaceImplementor | MyInterface) for throws: InterfaceImplementor is a subtype of MyInterface.
* @throws MyInterface
*/
public function testThrows()
{
}

/**
* @throws InterfaceImplementor|MyInterface // error: Found non-normalized type (InterfaceImplementor | MyInterface) for throws: InterfaceImplementor is a subtype of MyInterface.
*/
public function testThrowsUnion()
{
}

/**
* @throws MyInterface|ChildOne
* @throws InterfaceImplementor // error: Found non-normalized type (MyInterface | ChildOne | InterfaceImplementor) for throws: InterfaceImplementor is a subtype of MyInterface.
*/
public function testThrowsUnionCombined()
{
}

}

0 comments on commit a919257

Please sign in to comment.