Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
forbidPhpDocNullabilityMismatchWithNativeTypehint (#64)
* forbidPhpDocNullabilityMismatchWithNativeTypehint * better docs
- Loading branch information
Showing
10 changed files
with
338 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
src/Rule/ForbidPhpDocNullabilityMismatchWithNativeTypehintRule.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace ShipMonk\PHPStan\Rule; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr\Variable; | ||
use PhpParser\Node\FunctionLike; | ||
use PhpParser\Node\Stmt\Property; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\PhpDoc\ResolvedPhpDocBlock; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Type\FileTypeMapper; | ||
use PHPStan\Type\NullType; | ||
use PHPStan\Type\Type; | ||
use function array_merge; | ||
use function is_string; | ||
|
||
/** | ||
* @implements Rule<Node> | ||
*/ | ||
class ForbidPhpDocNullabilityMismatchWithNativeTypehintRule implements Rule | ||
{ | ||
|
||
private FileTypeMapper $fileTypeMapper; | ||
|
||
public function __construct( | ||
FileTypeMapper $fileTypeMapper | ||
) | ||
{ | ||
$this->fileTypeMapper = $fileTypeMapper; | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Node::class; | ||
} | ||
|
||
/** | ||
* @return list<string> | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if ($node instanceof FunctionLike) { | ||
return [ | ||
...$this->checkReturnTypes($node, $scope), | ||
...$this->checkParamTypes($node, $scope), | ||
]; | ||
} | ||
|
||
if ($node instanceof Property) { | ||
return $this->checkPropertyTypes($node, $scope); | ||
} | ||
|
||
return []; | ||
} | ||
|
||
/** | ||
* @return list<string> | ||
*/ | ||
private function checkReturnTypes(FunctionLike $node, Scope $scope): array | ||
{ | ||
$phpDocReturnType = $this->getFunctionPhpDocReturnType($node, $scope); | ||
$nativeReturnType = $this->getFunctionNativeReturnType($node, $scope); | ||
|
||
return $this->comparePhpDocAndNativeType($phpDocReturnType, $nativeReturnType, $scope, '@return'); | ||
} | ||
|
||
/** | ||
* @return list<string> | ||
*/ | ||
private function checkPropertyTypes(Property $node, Scope $scope): array | ||
{ | ||
$phpDocReturnType = $this->getPropertyPhpDocType($node, $scope); | ||
$nativeReturnType = $this->getPropertyNativeType($node, $scope); | ||
|
||
return $this->comparePhpDocAndNativeType($phpDocReturnType, $nativeReturnType, $scope, '@var'); | ||
} | ||
|
||
/** | ||
* @return list<string> | ||
*/ | ||
private function checkParamTypes(FunctionLike $node, Scope $scope): array | ||
{ | ||
$errors = []; | ||
|
||
foreach ($node->getParams() as $param) { | ||
if (!$param->var instanceof Variable || !is_string($param->var->name)) { | ||
continue; | ||
} | ||
|
||
$paramName = $param->var->name; | ||
|
||
$phpDocParamType = $this->getPhpDocParamType($node, $scope, $paramName); | ||
$nativeParamType = $scope->getFunctionType($param->type, false, false); | ||
|
||
$errors = array_merge( | ||
$errors, | ||
$this->comparePhpDocAndNativeType($phpDocParamType, $nativeParamType, $scope, "@param \$$paramName"), | ||
); | ||
} | ||
|
||
return $errors; | ||
} | ||
|
||
private function getPropertyNativeType(Property $node, Scope $scope): ?Type | ||
{ | ||
if ($node->type === null) { | ||
return null; | ||
} | ||
|
||
return $scope->getFunctionType($node->type, false, false); | ||
} | ||
|
||
private function getFunctionNativeReturnType(FunctionLike $node, Scope $scope): ?Type | ||
{ | ||
if ($node->getReturnType() === null) { | ||
return null; | ||
} | ||
|
||
return $scope->getFunctionType($node->getReturnType(), false, false); | ||
} | ||
|
||
private function getPropertyPhpDocType(Property $node, Scope $scope): ?Type | ||
{ | ||
$resolvedPhpDoc = $this->resolvePhpDoc($node, $scope); | ||
|
||
if ($resolvedPhpDoc === null) { | ||
return null; | ||
} | ||
|
||
$varTags = $resolvedPhpDoc->getVarTags(); | ||
|
||
foreach ($varTags as $varTag) { | ||
return $varTag->getType(); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
private function getFunctionPhpDocReturnType(FunctionLike $node, Scope $scope): ?Type | ||
{ | ||
$resolvedPhpDoc = $this->resolvePhpDoc($node, $scope); | ||
|
||
if ($resolvedPhpDoc === null) { | ||
return null; | ||
} | ||
|
||
$returnTag = $resolvedPhpDoc->getReturnTag(); | ||
|
||
if ($returnTag === null) { | ||
return null; | ||
} | ||
|
||
return $returnTag->getType(); | ||
} | ||
|
||
private function resolvePhpDoc(Node $node, Scope $scope): ?ResolvedPhpDocBlock | ||
{ | ||
$docComment = $node->getDocComment(); | ||
|
||
if ($docComment === null) { | ||
return null; | ||
} | ||
|
||
return $this->fileTypeMapper->getResolvedPhpDoc( | ||
$scope->getFile(), | ||
$scope->getClassReflection() === null ? null : $scope->getClassReflection()->getName(), | ||
$scope->getTraitReflection() === null ? null : $scope->getTraitReflection()->getName(), | ||
$scope->getFunctionName(), | ||
$docComment->getText(), | ||
); | ||
} | ||
|
||
private function getPhpDocParamType(FunctionLike $node, Scope $scope, string $parameterName): ?Type | ||
{ | ||
$resolvedPhpDoc = $this->resolvePhpDoc($node, $scope); | ||
|
||
if ($resolvedPhpDoc === null) { | ||
return null; | ||
} | ||
|
||
$paramTags = $resolvedPhpDoc->getParamTags(); | ||
|
||
foreach ($paramTags as $paramTagName => $paramTag) { | ||
if ($paramTagName === $parameterName) { | ||
return $paramTag->getType(); | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @return list<string> | ||
*/ | ||
private function comparePhpDocAndNativeType(?Type $phpDocReturnType, ?Type $nativeReturnType, Scope $scope, string $phpDocIdentification): array | ||
{ | ||
if ($phpDocReturnType === null || $nativeReturnType === null) { | ||
return []; | ||
} | ||
|
||
$strictTypes = $scope->isDeclareStrictTypes(); | ||
$nullType = new NullType(); | ||
|
||
// the inverse check is performed by native PHPStan rule checking that phpdoc is subtype of native type | ||
if (!$phpDocReturnType->accepts($nullType, $strictTypes)->yes() && $nativeReturnType->accepts($nullType, $strictTypes)->yes()) { | ||
return ["The $phpDocIdentification phpdoc does not contain null, but native return type does"]; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
tests/Rule/ForbidPhpDocNullabilityMismatchWithNativeTypehintRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace ShipMonk\PHPStan\Rule; | ||
|
||
use PHPStan\Rules\Rule; | ||
use PHPStan\Type\FileTypeMapper; | ||
use ShipMonk\PHPStan\RuleTestCase; | ||
|
||
/** | ||
* @extends RuleTestCase<ForbidPhpDocNullabilityMismatchWithNativeTypehintRule> | ||
*/ | ||
class ForbidPhpDocNullabilityMismatchWithNativeTypehintRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new ForbidPhpDocNullabilityMismatchWithNativeTypehintRule( | ||
self::getContainer()->getByType(FileTypeMapper::class), | ||
); | ||
} | ||
|
||
public function testBasic(): void | ||
{ | ||
$this->analyseFile(__DIR__ . '/data/ForbidPhpDocNullabilityMismatchWithNativeTypehintRule/code.php'); | ||
} | ||
|
||
} |
Oops, something went wrong.