[Downgrade PHP 7.2] Improve covariant (#6207)

This commit is contained in:
Tomas Votruba 2021-04-23 11:03:45 +02:00 committed by GitHub
parent 91377902b2
commit e425c11d8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 389 additions and 206 deletions

29
.github/workflows/tests_debug.yaml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Tests Debug
on:
pull_request: null
env:
# see https://github.com/composer/composer/issues/9368#issuecomment-718112361
COMPOSER_ROOT_VERSION: "dev-main"
jobs:
tests_debug:
runs-on: ubuntu-latest
name: PHP ${{ matrix.php }} tests for ${{ matrix.path }}
steps:
- uses: actions/checkout@v2
-
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
coverage: none
- uses: "ramsey/composer-install@v1"
# this should pass :)
- run: vendor/bin/phpunit rules-tests/DowngradePhp72/Rector/Class_/DowngradeParameterTypeWideningRector/DowngradeParameterTypeWideningRectorTest.php --filter test#4-7
- run: vendor/bin/phpunit rules-tests/DowngradePhp72/Rector/Class_/DowngradeParameterTypeWideningRector/DowngradeParameterTypeWideningRectorTest.php

View File

@ -7,14 +7,14 @@ interface SomeContainerInterface
public function has(string $id);
}
class SomeContainer implements SomeContainerInterface
final class SomeContainerBuilder extends SomeUniqueContainer
{
public function has($id)
{
}
}
final class SomeContainerBuilder extends SomeContainer
class SomeUniqueContainer implements SomeContainerInterface
{
public function has($id)
{
@ -35,14 +35,14 @@ interface SomeContainerInterface
public function has($id);
}
class SomeContainer implements SomeContainerInterface
final class SomeContainerBuilder extends SomeUniqueContainer
{
public function has($id)
{
}
}
final class SomeContainerBuilder extends SomeContainer
class SomeUniqueContainer implements SomeContainerInterface
{
public function has($id)
{

View File

@ -14,7 +14,7 @@ class B implements A
}
}
class C implements A
final class MostChild implements A
{
public function test(array $input)
{
@ -42,7 +42,7 @@ class B implements A
}
}
class C implements A
final class MostChild implements A
{
/**
* @param mixed[] $input

View File

@ -1,22 +0,0 @@
<?php
namespace Rector\DowngradePhp72\Tests\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
trait StreamDecoratorTrait
{
protected function createStream()
{
// ...
}
}
class MultipartStream
{
use StreamDecoratorTrait;
protected function createStream(array $elements)
{
// ...
}
}
?>

View File

@ -1,6 +1,8 @@
<?php
final class SomeContainer implements ContainerInterface
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
final class SomeContainer implements AnyContainerInterface
{
use ServiceLocatorTrait;
}
@ -12,7 +14,7 @@ trait ServiceLocatorTrait
}
}
interface ContainerInterface
interface AnyContainerInterface
{
public function has(string $name);
}
@ -21,21 +23,29 @@ interface ContainerInterface
-----
<?php
final class SomeContainer implements ContainerInterface
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
final class SomeContainer implements AnyContainerInterface
{
use ServiceLocatorTrait;
}
trait ServiceLocatorTrait
{
public function has(string $name)
/**
* @param string $name
*/
public function has($name)
{
}
}
interface ContainerInterface
interface AnyContainerInterface
{
public function has(string $name);
/**
* @param string $name
*/
public function has($name);
}
?>

View File

@ -1,6 +1,8 @@
<?php
final class AnotherContainer implements AnotherContainerInterface
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
final class YetAnotherContainer implements AnotherContainerInterface
{
use AnotherServiceLocatorTrait;
}
@ -21,7 +23,9 @@ interface AnotherContainerInterface
-----
<?php
final class AnotherContainer implements AnotherContainerInterface
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
final class YetAnotherContainer implements AnotherContainerInterface
{
use AnotherServiceLocatorTrait;
}

View File

@ -2,12 +2,12 @@
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
interface SomeInterface
interface WhateverInterface
{
public function test(string $input);
}
abstract class AbstractSomeAncestorClass implements SomeInterface
abstract class AbstractSomeAncestorClass implements WhateverInterface
{
}
@ -25,7 +25,7 @@ class SomeChildClass extends AbstractSomeAncestorClass
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
interface SomeInterface
interface WhateverInterface
{
/**
* @param string $input
@ -33,7 +33,7 @@ interface SomeInterface
public function test($input);
}
abstract class AbstractSomeAncestorClass implements SomeInterface
abstract class AbstractSomeAncestorClass implements WhateverInterface
{
}

View File

@ -2,9 +2,9 @@
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
use Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Source\SomeContainerInterface;
use Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Source\SomeExternalContainerInterface;
class KeepInterfaceDonwgraded implements SomeContainerInterface
class KeepInterfaceDonwgraded implements SomeExternalContainerInterface
{
public function get($id)
{

View File

@ -2,14 +2,40 @@
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
interface SkipNothingHappens
interface JustToBeSure
{
public function test(array $input);
}
final class SkipNothingHappensClass implements SkipNothingHappens
final class UpdateBoth implements JustToBeSure
{
public function test(array $input)
{
}
}
?>
-----
<?php
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
interface JustToBeSure
{
/**
* @param mixed[] $input
*/
public function test($input);
}
final class UpdateBoth implements JustToBeSure
{
/**
* @param mixed[] $input
*/
public function test($input)
{
}
}
?>

View File

@ -8,10 +8,6 @@ use Rector\TypeDeclaration\Contract\TypeInferer\ParamTypeInfererInterface;
final class SkipRequiredRemoval implements ParamTypeInfererInterface
{
public function autowireSomething(\PHPStan\Type\Type $input)
{
}
public function inferParam(Param $param): Type
{
}

View File

@ -0,0 +1,50 @@
<?php
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
trait StreamDecoratorTrait
{
protected function createStream()
{
// ...
}
}
class MultipartStream
{
use StreamDecoratorTrait;
protected function createStream(array $elements)
{
// ...
}
}
?>
----
<?php
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Fixture;
trait StreamDecoratorTrait
{
protected function createStream()
{
// ...
}
}
class MultipartStream
{
use StreamDecoratorTrait;
/**
* @param mixed[] $elements
*/
protected function createStream($elements)
{
// ...
}
}
?>

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Rector\Tests\DowngradePhp72\Rector\Class_\DowngradeParameterTypeWideningRector\Source;
interface SomeContainerInterface
interface SomeExternalContainerInterface
{
public function get(string $id);
}

View File

@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Rector\DowngradePhp72\NodeAnalyzer;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Trait_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use Rector\NodeCollector\NodeCollector\NodeRepository;
@ -29,23 +29,24 @@ final class ClassLikeWithTraitsClassMethodResolver
*/
public function resolve(Class_ $class): array
{
$classMethods = $class->getMethods();
$scope = $class->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof Scope) {
return $classMethods;
return [];
}
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return $classMethods;
return [];
}
foreach ($classReflection->getTraits() as $traitClassReflection) {
$trait = $this->nodeRepository->findTrait($traitClassReflection->getName());
if ($trait instanceof Trait_) {
$classMethods = array_merge($classMethods, $trait->getMethods());
$classMethods = [];
foreach ($classReflection->getAncestors() as $ancestorClassReflection) {
$classLike = $this->nodeRepository->findClassLike($ancestorClassReflection->getName());
if (! $classLike instanceof ClassLike) {
continue;
}
$classMethods = array_merge($classMethods, $classLike->getMethods());
}
return $classMethods;

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Rector\DowngradePhp72\NodeAnalyzer;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\Type;
final class ParentChildClassMethodTypeResolver
{
/**
* @var NativeTypeClassTreeResolver
*/
private $nativeTypeClassTreeResolver;
public function __construct(NativeTypeClassTreeResolver $nativeTypeClassTreeResolver)
{
$this->nativeTypeClassTreeResolver = $nativeTypeClassTreeResolver;
}
/**
* @return array<class-string, Type>
*/
public function resolve(
ClassReflection $classReflection,
string $methodName,
int $position,
Scope $scope
): array {
$parameterTypesByClassName = [];
// include types of class scope in case of trait
if ($classReflection->isTrait()) {
$parameterTypesByInterfaceName = $this->resolveInterfaceTypeByClassName($scope, $methodName, $position);
$parameterTypesByClassName = array_merge($parameterTypesByClassName, $parameterTypesByInterfaceName);
}
foreach ($classReflection->getAncestors() as $ancestorClassReflection) {
if (! $ancestorClassReflection->hasMethod($methodName)) {
continue;
}
$parameterType = $this->nativeTypeClassTreeResolver->resolveParameterReflectionType(
$ancestorClassReflection,
$methodName,
$position
);
$parameterTypesByClassName[$ancestorClassReflection->getName()] = $parameterType;
}
return $parameterTypesByClassName;
}
/**
* @return array<class-string, Type>
*/
private function resolveInterfaceTypeByClassName(Scope $scope, string $methodName, int $position): array
{
$typesByClassName = [];
$currentClassReflection = $scope->getClassReflection();
if (! $currentClassReflection instanceof ClassReflection) {
return [];
}
foreach ($currentClassReflection->getInterfaces() as $interfaceClassReflection) {
if (! $interfaceClassReflection->hasMethod($methodName)) {
continue;
}
$parameterType = $this->nativeTypeClassTreeResolver->resolveParameterReflectionType(
$interfaceClassReflection,
$methodName,
$position
);
$typesByClassName[$interfaceClassReflection->getName()] = $parameterType;
}
return $typesByClassName;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Rector\DowngradePhp72\PhpDoc;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\StaticTypeMapper\StaticTypeMapper;
final class NativeParamToPhpDocDecorator
{
/**
* @var PhpDocInfoFactory
*/
private $phpDocInfoFactory;
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
/**
* @var StaticTypeMapper
*/
private $staticTypeMapper;
/**
* @var PhpDocTypeChanger
*/
private $phpDocTypeChanger;
public function __construct(
PhpDocInfoFactory $phpDocInfoFactory,
NodeNameResolver $nodeNameResolver,
StaticTypeMapper $staticTypeMapper,
PhpDocTypeChanger $phpDocTypeChanger
) {
$this->phpDocInfoFactory = $phpDocInfoFactory;
$this->nodeNameResolver = $nodeNameResolver;
$this->staticTypeMapper = $staticTypeMapper;
$this->phpDocTypeChanger = $phpDocTypeChanger;
}
public function decorate(ClassMethod $classMethod, Param $param): void
{
if ($param->type === null) {
return;
}
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod);
$paramName = $this->nodeNameResolver->getName($param);
$mappedCurrentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type);
$this->phpDocTypeChanger->changeParamType($phpDocInfo, $mappedCurrentParamType, $param, $paramName);
}
}

View File

@ -11,15 +11,15 @@ use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Type;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\ChangesReporting\ValueObject\RectorWithLineChange;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\Application\File;
use Rector\DowngradePhp72\NodeAnalyzer\ClassLikeWithTraitsClassMethodResolver;
use Rector\DowngradePhp72\NodeAnalyzer\NativeTypeClassTreeResolver;
use Rector\DowngradePhp72\NodeAnalyzer\ParentChildClassMethodTypeResolver;
use Rector\DowngradePhp72\PhpDoc\NativeParamToPhpDocDecorator;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@ -31,36 +31,36 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
*/
final class DowngradeParameterTypeWideningRector extends AbstractRector
{
/**
* @var PhpDocTypeChanger
*/
private $phpDocTypeChanger;
/**
* @var NativeTypeClassTreeResolver
*/
private $nativeTypeClassTreeResolver;
/**
* @var TypeFactory
*/
private $typeFactory;
/**
* @var ClassLikeWithTraitsClassMethodResolver
*/
private $classLikeWithTraitsClassMethodResolver;
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
/**
* @var ParentChildClassMethodTypeResolver
*/
private $parentChildClassMethodTypeResolver;
/**
* @var NativeParamToPhpDocDecorator
*/
private $nativeParamToPhpDocDecorator;
public function __construct(
PhpDocTypeChanger $phpDocTypeChanger,
NativeTypeClassTreeResolver $nativeTypeClassTreeResolver,
TypeFactory $typeFactory,
ClassLikeWithTraitsClassMethodResolver $classLikeWithTraitsClassMethodResolver
ClassLikeWithTraitsClassMethodResolver $classLikeWithTraitsClassMethodResolver,
ReflectionProvider $reflectionProvider,
ParentChildClassMethodTypeResolver $parentChildClassMethodTypeResolver,
NativeParamToPhpDocDecorator $nativeParamToPhpDocDecorator
) {
$this->phpDocTypeChanger = $phpDocTypeChanger;
$this->nativeTypeClassTreeResolver = $nativeTypeClassTreeResolver;
$this->typeFactory = $typeFactory;
$this->classLikeWithTraitsClassMethodResolver = $classLikeWithTraitsClassMethodResolver;
$this->reflectionProvider = $reflectionProvider;
$this->parentChildClassMethodTypeResolver = $parentChildClassMethodTypeResolver;
$this->nativeParamToPhpDocDecorator = $nativeParamToPhpDocDecorator;
}
public function getRuleDefinition(): RuleDefinition
@ -109,9 +109,16 @@ CODE_SAMPLE
*/
public function refactor(Node $node): ?Node
{
$classMethods = $this->classLikeWithTraitsClassMethodResolver->resolve($node);
$scope = $node->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof Scope) {
return null;
}
if ($this->isEmptyClassReflection($scope)) {
return null;
}
$classMethods = $this->classLikeWithTraitsClassMethodResolver->resolve($node);
foreach ($classMethods as $classMethod) {
$this->refactorClassMethod($classMethod, $scope);
}
@ -122,42 +129,50 @@ CODE_SAMPLE
/**
* The topmost class is the source of truth, so we go only down to avoid up/down collission
*/
private function refactorParamForSelfAndSiblings(ClassMethod $classMethod, int $position, Scope $classScope): void
private function refactorParamForSelfAndSiblings(ClassMethod $classMethod, int $position, Scope $scope): void
{
$classReflection = $classScope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
$class = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
if ($class === null) {
return;
}
if (count($classReflection->getAncestors()) === 1) {
$className = $this->getName($class);
if ($className === null) {
return;
}
if (! $this->reflectionProvider->hasClass($className)) {
return;
}
$classReflection = $this->reflectionProvider->getClass($className);
/** @var string $methodName */
$methodName = $this->nodeNameResolver->getName($classMethod);
// Remove the types in:
// - all ancestors + their descendant classes
// - all implemented interfaces + their implementing classes
$parameterTypesByParentClassLikes = $this->resolveParameterTypesByClassLike(
$parameterTypesByParentClassLikes = $this->parentChildClassMethodTypeResolver->resolve(
$classReflection,
$methodName,
$position
$position,
$scope
);
// we need at least 2 methods to have a possible conflict
if (count($parameterTypesByParentClassLikes) < 2) {
return;
// skip classes we cannot change
foreach (array_keys($parameterTypesByParentClassLikes) as $className) {
$classLike = $this->nodeRepository->findClassLike($className);
if (! $classLike instanceof ClassLike) {
return;
}
}
$uniqueParameterTypes = $this->typeFactory->uniquateTypes($parameterTypesByParentClassLikes);
// we need at least 2 unique types
if (count($uniqueParameterTypes) === 1) {
// we need at least 2 types = 2 occurances of same method
if (count($parameterTypesByParentClassLikes) <= 1) {
return;
}
$this->refactorClassWithAncestorsAndChildren($classReflection, $methodName, $position);
$this->refactorParameters($parameterTypesByParentClassLikes, $methodName, $position);
}
private function removeParamTypeFromMethod(
@ -184,10 +199,11 @@ CODE_SAMPLE
}
// Add the current type in the PHPDoc
$this->addPHPDocParamTypeToMethod($classMethod, $param);
$this->nativeParamToPhpDocDecorator->decorate($classMethod, $param);
// Remove the type
$param->type = null;
$param->setAttribute(AttributeKey::ORIGINAL_NODE, null);
// file from another file
$file = $param->getAttribute(AttributeKey::FILE);
@ -197,111 +213,6 @@ CODE_SAMPLE
}
}
private function removeParamTypeFromMethodForChildren(
string $parentClassName,
string $methodName,
int $position
): void {
$childrenClassLikes = $this->nodeRepository->findClassesAndInterfacesByType($parentClassName);
foreach ($childrenClassLikes as $childClassLike) {
$childClassName = $childClassLike->getAttribute(AttributeKey::CLASS_NAME);
if ($childClassName === null) {
continue;
}
$childClassMethod = $this->nodeRepository->findClassMethod($childClassName, $methodName);
if (! $childClassMethod instanceof ClassMethod) {
continue;
}
$this->removeParamTypeFromMethod($childClassLike, $position, $childClassMethod);
}
}
/**
* Add the current param type in the PHPDoc
*/
private function addPHPDocParamTypeToMethod(ClassMethod $classMethod, Param $param): void
{
if ($param->type === null) {
return;
}
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod);
$paramName = $this->getName($param);
$mappedCurrentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type);
$this->phpDocTypeChanger->changeParamType($phpDocInfo, $mappedCurrentParamType, $param, $paramName);
}
/**
* @return array<class-string, Type>
*/
private function resolveParameterTypesByClassLike(
ClassReflection $classReflection,
string $methodName,
int $position
): array {
$parameterTypesByParentClassLikes = [];
foreach ($classReflection->getAncestors() as $ancestorClassReflection) {
if ($ancestorClassReflection->isTrait()) {
continue;
}
if (! $ancestorClassReflection->hasMethod($methodName)) {
continue;
}
$parameterType = $this->nativeTypeClassTreeResolver->resolveParameterReflectionType(
$ancestorClassReflection,
$methodName,
$position
);
$parameterTypesByParentClassLikes[$ancestorClassReflection->getName()] = $parameterType;
}
return $parameterTypesByParentClassLikes;
}
private function refactorClassWithAncestorsAndChildren(
ClassReflection $classReflection,
string $methodName,
int $position
): void {
foreach ($classReflection->getAncestors() as $ancestorClassRelection) {
$classLike = $this->nodeRepository->findClassLike($ancestorClassRelection->getName());
if (! $classLike instanceof ClassLike) {
continue;
}
$currentClassMethod = $classLike->getMethod($methodName);
if (! $currentClassMethod instanceof ClassMethod) {
continue;
}
$className = $this->getName($classLike);
if ($className === null) {
continue;
}
/**
* If it doesn't find the method, it's because the method
* lives somewhere else.
* For instance, in test "interface_on_parent_class.php.inc",
* the ancestorClassReflection abstract class is also retrieved
* as containing the method, but it does not: it is
* in its implemented interface. That happens because
* `ReflectionMethod` doesn't allow to do do the distinction.
* The interface is also retrieve though, so that method
* will eventually be refactored.
*/
$this->removeParamTypeFromMethod($classLike, $position, $currentClassMethod);
$this->removeParamTypeFromMethodForChildren($className, $methodName, $position);
}
}
private function refactorClassMethod(ClassMethod $classMethod, Scope $classScope): void
{
if ($classMethod->isMagic()) {
@ -316,4 +227,37 @@ CODE_SAMPLE
$this->refactorParamForSelfAndSiblings($classMethod, (int) $position, $classScope);
}
}
private function isEmptyClassReflection(Scope $scope): bool
{
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return true;
}
return count($classReflection->getAncestors()) === 1;
}
/**
* @param array<class-string, Type> $parameterTypesByParentClassLikes
*/
private function refactorParameters(
array $parameterTypesByParentClassLikes,
string $methodName,
int $paramPosition
): void {
foreach (array_keys($parameterTypesByParentClassLikes) as $className) {
$classLike = $this->nodeRepository->findClassLike($className);
if (! $classLike instanceof ClassLike) {
continue;
}
$classMethod = $classLike->getMethod($methodName);
if (! $classMethod instanceof ClassMethod) {
continue;
}
$this->removeParamTypeFromMethod($classLike, $paramPosition, $classMethod);
}
}
}