Skip to content

Commit

Permalink
AddReturnTypeDeclarationBasedOnParentClassMethodRector
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinMystikJonas committed Jul 15, 2022
1 parent db7012e commit b5281b4
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 19 deletions.
21 changes: 20 additions & 1 deletion packages/VendorLocker/ParentClassMethodTypeOverrideGuard.php
Expand Up @@ -15,6 +15,8 @@
use Rector\Core\Reflection\ReflectionResolver;
use Rector\Core\ValueObject\MethodName;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\TypeComparator\TypeComparator;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Rector\TypeDeclaration\TypeInferer\ParamTypeInferer;
use Symplify\SmartFileSystem\Normalizer\PathNormalizer;

Expand All @@ -25,7 +27,9 @@ public function __construct(
private readonly PathNormalizer $pathNormalizer,
private readonly AstResolver $astResolver,
private readonly ParamTypeInferer $paramTypeInferer,
private readonly ReflectionResolver $reflectionResolver
private readonly ReflectionResolver $reflectionResolver,
private readonly TypeComparator $typeComparator,
private readonly StaticTypeMapper $staticTypeMapper
) {
}

Expand Down Expand Up @@ -138,4 +142,19 @@ public function getParentClassMethod(ClassMethod $classMethod): ?MethodReflectio

return null;
}

public function shouldSkipReturnTypeChange(ClassMethod $classMethod, Type $parentType): bool
{
if ($classMethod->returnType === null) {
return false;
}

$currentReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($classMethod->returnType);

if ($this->typeComparator->isSubtype($currentReturnType, $parentType)) {
return true;
}

return $this->typeComparator->areTypesEqual($currentReturnType, $parentType);
}
}
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector;

use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;

final class AddReturnTypeDeclarationBasedOnParentClassMethodRectorTest 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';
}
}
@@ -0,0 +1,25 @@
<?php

use Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source\SomeClassWithReturnType;

class MyClass extends SomeClassWithReturnType
{
public function run()
{
}
}

?>
-----
<?php

use Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source\SomeClassWithReturnType;

class MyClass extends SomeClassWithReturnType
{
public function run(): int
{
}
}

?>
@@ -0,0 +1,25 @@
<?php

use Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source\SomeClassWithoutReturnType;

class MyClass extends SomeClassWithoutReturnType
{
public function run()
{
}
}

?>
-----
<?php

use Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source\SomeClassWithoutReturnType;

class MyClass extends SomeClassWithoutReturnType
{
public function run()
{
}
}

?>
@@ -0,0 +1,25 @@
<?php

use Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source\SomeInterfaceWithReturnType;

abstract class MyClass implements SomeInterfaceWithReturnType
{
public function run()
{
}
}

?>
-----
<?php

use Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source\SomeInterfaceWithReturnType;

abstract class MyClass implements SomeInterfaceWithReturnType
{
public function run(): string
{
}
}

?>
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source;

class SomeClassWithReturnType
{
public function run(): int
{
return 5;
}
}
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source;

class SomeClassWithoutReturnType
{
public function run()
{
return 5;
}
}
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\Source;

interface SomeInterfaceWithReturnType
{
public function run(): string;
}
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(AddReturnTypeDeclarationBasedOnParentClassMethodRector::class);

$rectorConfig->phpVersion(PhpVersionFeature::MIXED_TYPE);
};
@@ -0,0 +1,160 @@
<?php

declare(strict_types=1);

namespace Rector\TypeDeclaration\Rector\ClassMethod;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Rector\Core\Php\PhpVersionProvider;
use Rector\Core\PhpParser\AstResolver;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\MethodName;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Rector\VendorLocker\ParentClassMethodTypeOverrideGuard;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @changelog https://wiki.php.net/rfc/lsp_errors
* @see \Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector\AddReturnTypeDeclarationBasedOnParentClassMethodRectorTest
*/
final class AddReturnTypeDeclarationBasedOnParentClassMethodRector extends AbstractRector implements MinPhpVersionInterface
{
public function __construct(
private readonly ParentClassMethodTypeOverrideGuard $parentClassMethodTypeOverrideGuard,
private readonly AstResolver $astResolver,
private readonly PhpVersionProvider $phpVersionProvider,
) {
}

public function provideMinPhpVersion(): int
{
return PhpVersionFeature::FATAL_ERROR_ON_INCOMPATIBLE_METHOD_SIGNATURE;
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Add missing return type declaration based on parent class method', [
new CodeSample(
<<<'CODE_SAMPLE'
class A
{
public function execute($foo): int
{
}
}
class B extends A{
public function execute()
{
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class A
{
public function execute($foo): int
{
}
}
class B extends A{
public function execute($foo): int
{
}
}
CODE_SAMPLE
),
]);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}

/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if ($this->nodeNameResolver->isName($node, MethodName::CONSTRUCT)) {
return null;
}

$parentMethodReflection = $this->parentClassMethodTypeOverrideGuard->getParentClassMethod($node);
if (! $parentMethodReflection instanceof MethodReflection) {
return null;
}

$parentClassMethod = $this->astResolver->resolveClassMethodFromMethodReflection($parentMethodReflection);

if (! $parentClassMethod instanceof ClassMethod) {
return null;
}

if ($parentClassMethod->isPrivate()) {
return null;
}

$parentClassMethodReturnType = $parentClassMethod->getReturnType();
if ($parentClassMethodReturnType === null) {
return null;
}

$parentClassMethodReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType(
$parentClassMethodReturnType
);

return $this->processClassMethodReturnType($node, $parentClassMethodReturnType);
}

private function processClassMethodReturnType(ClassMethod $classMethod, Type $parentType): ?ClassMethod
{
if ($parentType instanceof MixedType) {
$class = $classMethod->getAttribute(AttributeKey::PARENT_NODE);
if (! $class instanceof Class_) {
return null;
}

$className = (string) $this->nodeNameResolver->getName($class);
$currentObjectType = new ObjectType($className);
if (! $parentType->equals($currentObjectType) && $classMethod->returnType !== null) {
return null;
}
}

// remove it
if ($parentType instanceof MixedType && ! $this->phpVersionProvider->isAtLeastPhpVersion(
PhpVersionFeature::MIXED_TYPE
)) {
$classMethod->returnType = null;
return null;
}

// already set and sub type or equal → no change
if ($this->parentClassMethodTypeOverrideGuard->shouldSkipReturnTypeChange($classMethod, $parentType)) {
return null;
}

$classMethod->returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode(
$parentType,
TypeKind::RETURN
);

return $classMethod;
}
}

0 comments on commit b5281b4

Please sign in to comment.