[CodeQuality] Add NarrowUnionTypeDocRector (#6262)

Co-authored-by: kaizen-ci <info@kaizen-ci.org>
This commit is contained in:
Abdul Malik Ikhsan 2021-04-30 15:28:36 +07:00 committed by GitHub
parent d43e40aa6e
commit e6157560c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 315 additions and 33 deletions

View File

@ -5,12 +5,18 @@ declare(strict_types=1);
namespace Rector\PHPStanStaticTypeMapper\TypeAnalyzer;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IterableType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StringType;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
use Rector\PHPStanStaticTypeMapper\ValueObject\UnionTypeAnalysis;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Traversable;
final class UnionTypeAnalyzer
@ -58,4 +64,54 @@ final class UnionTypeAnalyzer
return true;
}
public function hasObjectWithoutClassType(UnionType $unionType): bool
{
$types = $unionType->getTypes();
foreach ($types as $type) {
if ($type instanceof ObjectWithoutClassType) {
return true;
}
}
return false;
}
public function hasObjectWithoutClassTypeWithOnlyFullyQualifiedObjectType(UnionType $unionType): bool
{
$types = $unionType->getTypes();
foreach ($types as $type) {
if ($type instanceof ObjectWithoutClassType) {
continue;
}
if (! $type instanceof FullyQualifiedObjectType) {
return false;
}
}
return true;
}
public function isScalar(UnionType $unionType): bool
{
$types = $unionType->getTypes();
foreach ($types as $type) {
if ($type instanceof StringType) {
continue;
}
if ($type instanceof FloatType) {
continue;
}
if ($type instanceof IntegerType) {
continue;
}
if ($type instanceof BooleanType) {
continue;
}
return false;
}
return true;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Tests\CodeQuality\Rector\ClassMethod\NarrowUnionTypeDocRector\Fixture;
class Fixture {
/**
* @param object|DateTime $message
*/
public function getMessage(object $message)
{
}
}
?>
-----
<?php
namespace Rector\Tests\CodeQuality\Rector\ClassMethod\NarrowUnionTypeDocRector\Fixture;
class Fixture {
/**
* @param DateTime $message
*/
public function getMessage(object $message)
{
}
}
?>

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Tests\CodeQuality\Rector\ClassMethod\NarrowUnionTypeDocRector\Fixture;
class ParamScalar {
/**
* @param int|string|bool|float $value
*/
public function getMessage($value)
{
}
}
?>
-----
<?php
namespace Rector\Tests\CodeQuality\Rector\ClassMethod\NarrowUnionTypeDocRector\Fixture;
class ParamScalar {
/**
* @param scalar $value
*/
public function getMessage($value)
{
}
}
?>

View File

@ -0,0 +1,15 @@
<?php
namespace Rector\Tests\CodeQuality\Rector\ClassMethod\NarrowUnionTypeDocRector\Fixture;
class SkipNonUnion {
/**
* @param DateTime $message
*/
public function getMessage(object $message)
{
}
}
?>

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\CodeQuality\Rector\ClassMethod\NarrowUnionTypeDocRector;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;
final class NarrowUnionTypeDocRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}
/**
* @return Iterator<SmartFileInfo>
*/
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
use Rector\CodeQuality\Rector\ClassMethod\NarrowUnionTypeDocRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(NarrowUnionTypeDocRector::class);
};

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Rector\CodeQuality\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\Type\UnionType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\Core\Rector\AbstractRector;
use Rector\PHPStanStaticTypeMapper\TypeAnalyzer\UnionTypeAnalyzer;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
* @see \Rector\Tests\CodeQuality\Rector\ClassMethod\NarrowUnionTypeDocRector\NarrowUnionTypeDocRectorTest
*/
final class NarrowUnionTypeDocRector extends AbstractRector
{
/**
* @var UnionTypeAnalyzer
*/
private $unionTypeAnalyzer;
public function __construct(UnionTypeAnalyzer $unionTypeAnalyzer)
{
$this->unionTypeAnalyzer = $unionTypeAnalyzer;
}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Changes docblock by narrowing type', [
new CodeSample(
<<<'CODE_SAMPLE'
class SomeClass {
/**
* @param object|DateTime $message
*/
public function getMessage(object $message)
{
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeClass {
/**
* @param DateTime $message
*/
public function getMessage(object $message)
{
}
}
CODE_SAMPLE
),
]);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
$params = $node->getParams();
foreach ($params as $key => $param) {
/** @var string $paramName */
$paramName = $this->getName($param->var);
$paramType = $phpDocInfo->getParamType($paramName);
if (! $paramType instanceof UnionType) {
continue;
}
if ($this->unionTypeAnalyzer->isScalar($paramType)) {
$this->changeDocObjectScalar($key, $phpDocInfo);
continue;
}
if ($this->unionTypeAnalyzer->hasObjectWithoutClassType($paramType)) {
$this->changeDocObjectWithoutClassType($paramType, $key, $phpDocInfo);
continue;
}
}
if ($phpDocInfo->hasChanged()) {
return $node;
}
return null;
}
private function changeDocObjectWithoutClassType(
UnionType $unionType,
int $key,
PhpDocInfo $phpDocInfo
): void {
if (! $this->unionTypeAnalyzer->hasObjectWithoutClassTypeWithOnlyFullyQualifiedObjectType($unionType)) {
return;
}
$types = $unionType->getTypes();
$resultType = '';
foreach ($types as $type) {
if ($type instanceof FullyQualifiedObjectType) {
$resultType .= $type->getClassName() . '|';
}
}
$resultType = rtrim($resultType, '|');
$paramTagValueNodes = $phpDocInfo->getParamTagValueNodes();
$paramTagValueNodes[$key]->type = new IdentifierTypeNode($resultType);
}
private function changeDocObjectScalar(int $key, PhpDocInfo $phpDocInfo): void {
$paramTagValueNodes = $phpDocInfo->getParamTagValueNodes();
$paramTagValueNodes[$key]->type = new IdentifierTypeNode('scalar');
}
}

View File

@ -13,13 +13,12 @@ use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\UnionType as PhpParserUnionType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\UnionType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\Core\Rector\AbstractRector;
use Rector\DeadCode\PhpDoc\TagRemover\ParamTagRemover;
use Rector\DeadCode\PhpDoc\TagRemover\ReturnTagRemover;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Rector\PHPStanStaticTypeMapper\TypeAnalyzer\UnionTypeAnalyzer;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodParamVendorLockResolver;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@ -44,14 +43,21 @@ final class UnionTypesRector extends AbstractRector
*/
private $paramTagRemover;
/**
* @var UnionTypeAnalyzer
*/
private $unionTypeAnalyzer;
public function __construct(
ReturnTagRemover $returnTagRemover,
ParamTagRemover $paramTagRemover,
ClassMethodParamVendorLockResolver $classMethodParamVendorLockResolver
ClassMethodParamVendorLockResolver $classMethodParamVendorLockResolver,
UnionTypeAnalyzer $unionTypeAnalyzer
) {
$this->returnTagRemover = $returnTagRemover;
$this->paramTagRemover = $paramTagRemover;
$this->classMethodParamVendorLockResolver = $classMethodParamVendorLockResolver;
$this->unionTypeAnalyzer = $unionTypeAnalyzer;
}
public function getRuleDefinition(): RuleDefinition
@ -136,7 +142,7 @@ CODE_SAMPLE
continue;
}
if ($this->hasObjectWithoutClassType($paramType)) {
if ($this->unionTypeAnalyzer->hasObjectWithoutClassType($paramType)) {
$this->changeObjectWithoutClassType($param, $paramType);
continue;
}
@ -152,41 +158,13 @@ CODE_SAMPLE
private function changeObjectWithoutClassType(Param $param, UnionType $unionType): void
{
if (! $this->hasObjectWithoutClassTypeWithOnlyFullyQualifiedObjectType($unionType)) {
if (! $this->unionTypeAnalyzer->hasObjectWithoutClassTypeWithOnlyFullyQualifiedObjectType($unionType)) {
return;
}
$param->type = new Name('object');
}
private function hasObjectWithoutClassType(UnionType $unionType): bool
{
$types = $unionType->getTypes();
foreach ($types as $type) {
if ($type instanceof ObjectWithoutClassType) {
return true;
}
}
return false;
}
private function hasObjectWithoutClassTypeWithOnlyFullyQualifiedObjectType(UnionType $unionType): bool
{
$types = $unionType->getTypes();
foreach ($types as $type) {
if ($type instanceof ObjectWithoutClassType) {
continue;
}
if (! $type instanceof FullyQualifiedObjectType) {
return false;
}
}
return true;
}
/**
* @param ClassMethod|Function_|Closure|ArrowFunction $functionLike
*/