Skip to content

Commit

Permalink
[DowngradePhp72] Handle anonymous class override class method on Down…
Browse files Browse the repository at this point in the history
…gradeParameterTypeWideningRector (#777)

* Another failing test case for DowngradeParameterTypeWideningRector.

* Fixes test.

* Closes #762 Fixes rectorphp/rector#6664

* move skip method

* rectify

* [ci-review] Rector Rectify

* move Sealed Class check to SealedClassAnalyzer

* final touch

* final touch

Co-authored-by: Bobab12 <bobab12@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
  • Loading branch information
3 people committed Aug 27, 2021
1 parent 646b207 commit b2c44bf
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Rector\Tests\DowngradePhp72\Rector\ClassMethod\DowngradeParameterTypeWideningRector\Fixture;

class SomeClass
{
public function hello(string $world = 'world') {
printf('Hello %s', $world);
}
}

class SomeOtherClassUsingAnAnonymousClass
{
public function doSomething(): void
{
$class = new class () extends SomeClass {
public function hello(string $world = 'world') {
printf('Hi %s', $world);
}
};
}
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp72\Rector\ClassMethod\DowngradeParameterTypeWideningRector\Fixture;

class SomeClass
{
/**
* @param string $world
*/
public function hello($world = 'world') {
printf('Hello %s', $world);
}
}

class SomeOtherClassUsingAnAnonymousClass
{
public function doSomething(): void
{
$class = new class () extends SomeClass {
/**
* @param string $world
*/
public function hello($world = 'world') {
printf('Hi %s', $world);
}
};
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Rector\Tests\DowngradePhp72\Rector\ClassMethod\DowngradeParameterTypeWideningRector\Fixture;

class SomeClass2
{
private function hello(string $world = 'world') {
printf('Hello %s', $world);
}
}

class SkipAnonymousClassPrivateMethod
{
public function doSomething(): void
{
$class = new class () extends SomeClass2 {
private function hello(string $world = 'world') {
printf('Hi %s', $world);
}
};
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Rector\DowngradePhp72\NodeAnalyzer;

use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Reflection\Php\PhpMethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use Rector\Core\NodeAnalyzer\ClassAnalyzer;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;

final class OverrideFromAnonymousClassMethodAnalyzer
{
public function __construct(
private ClassAnalyzer $classAnalyzer,
private NodeNameResolver $nodeNameResolver,
private ReflectionProvider $reflectionProvider
) {
}

public function isOverrideParentMethod(ClassLike $classLike, ClassMethod $classMethod): bool
{
if (! $this->classAnalyzer->isAnonymousClass($classLike)) {
return false;
}

/** @var Class_ $classLike */
if (! $classLike->extends instanceof FullyQualified) {
return false;
}

$extendsClass = $classLike->extends->toString();
if (! $this->reflectionProvider->hasClass($extendsClass)) {
return false;
}

$classReflection = $this->reflectionProvider->getClass($extendsClass);
$methodName = $this->nodeNameResolver->getName($classMethod);

if (! $classReflection->hasMethod($methodName)) {
return false;
}

$scope = $classMethod->getAttribute(AttributeKey::SCOPE);
$method = $classReflection->getMethod($methodName, $scope);

if (! $method instanceof PhpMethodReflection) {
return false;
}

return ! $method->isPrivate();
}
}
26 changes: 26 additions & 0 deletions rules/DowngradePhp72/NodeAnalyzer/SealedClassAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Rector\DowngradePhp72\NodeAnalyzer;

use PHPStan\Reflection\ClassReflection;

final class SealedClassAnalyzer
{
/**
* This method is perfectly sealed, nothing to downgrade here
*/
public function isSealedClass(ClassReflection $classReflection): bool
{
if (! $classReflection->isClass()) {
return false;
}

if (! $classReflection->isFinal()) {
return false;
}

return count($classReflection->getAncestors()) === 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\PhpParser\AstResolver;
use Rector\Core\Rector\AbstractRector;
use Rector\DowngradePhp72\NodeAnalyzer\BuiltInMethodAnalyzer;
use Rector\DowngradePhp72\NodeAnalyzer\OverrideFromAnonymousClassMethodAnalyzer;
use Rector\DowngradePhp72\NodeAnalyzer\SealedClassAnalyzer;
use Rector\DowngradePhp72\PhpDoc\NativeParamToPhpDocDecorator;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\TypeDeclaration\NodeAnalyzer\AutowiredClassMethodOrPropertyAnalyzer;
Expand Down Expand Up @@ -51,7 +54,10 @@ public function __construct(
private NativeParamToPhpDocDecorator $nativeParamToPhpDocDecorator,
private ReflectionProvider $reflectionProvider,
private AutowiredClassMethodOrPropertyAnalyzer $autowiredClassMethodOrPropertyAnalyzer,
private BuiltInMethodAnalyzer $builtInMethodAnalyzer
private BuiltInMethodAnalyzer $builtInMethodAnalyzer,
private OverrideFromAnonymousClassMethodAnalyzer $overrideFromAnonymousClassMethodAnalyzer,
private AstResolver $astResolver,
private SealedClassAnalyzer $sealedClassAnalyzer
) {
}

Expand Down Expand Up @@ -116,42 +122,38 @@ public function refactor(Node $node): ?Node
return null;
}

$className = $this->nodeNameResolver->getName($classLike);
if ($className === null) {
return null;
}
if ($this->overrideFromAnonymousClassMethodAnalyzer->isOverrideParentMethod($classLike, $node)) {
$classReflection = $this->reflectionProvider->getClass($classLike->extends->toString());
$methodName = $this->nodeNameResolver->getName($node);
/** @var ClassMethod $classMethod */
$classMethod = $this->astResolver->resolveClassMethod($classReflection->getName(), $methodName);

if (! $this->reflectionProvider->hasClass($className)) {
return null;
}
if ($this->shouldSkip($classReflection, $classMethod)) {
return null;
}

$classReflection = $this->reflectionProvider->getClass($className);
if ($this->isSealedClass($classReflection)) {
return null;
return $this->processRemoveParamTypeFromMethod($node);
}

if ($this->isSafeType($classReflection, $node)) {
$className = $this->nodeNameResolver->getName($classLike);
if ($className === null) {
return null;
}

if ($node->isPrivate()) {
if (! $this->reflectionProvider->hasClass($className)) {
return null;
}

if ($this->shouldSkipClassMethod($node)) {
$classReflection = $this->reflectionProvider->getClass($className);
if ($this->shouldSkip($classReflection, $node)) {
return null;
}

if ($this->builtInMethodAnalyzer->isImplementsBuiltInInterface($classReflection, $node)) {
return null;
}

// Downgrade every scalar parameter, just to be sure
foreach (array_keys($node->params) as $paramPosition) {
$this->removeParamTypeFromMethod($node, $paramPosition);
}

return $node;
return $this->processRemoveParamTypeFromMethod($node);
}

/**
Expand All @@ -173,6 +175,33 @@ public function configure(array $configuration): void
$this->safeTypesToMethods = $safeTypesToMethods;
}

private function shouldSkip(ClassReflection $classReflection, ClassMethod $classMethod): bool
{
if ($this->sealedClassAnalyzer->isSealedClass($classReflection)) {
return true;
}

if ($this->isSafeType($classReflection, $classMethod)) {
return true;
}

if ($classMethod->isPrivate()) {
return true;
}

return $this->shouldSkipClassMethod($classMethod);
}

private function processRemoveParamTypeFromMethod(ClassMethod $classMethod): ClassMethod
{
// Downgrade every scalar parameter, just to be sure
foreach (array_keys($classMethod->params) as $paramPosition) {
$this->removeParamTypeFromMethod($classMethod, $paramPosition);
}

return $classMethod;
}

private function removeParamTypeFromMethod(ClassMethod $classMethod, int $paramPosition): void
{
$param = $classMethod->params[$paramPosition] ?? null;
Expand Down Expand Up @@ -213,22 +242,6 @@ private function shouldSkipClassMethod(ClassMethod $classMethod): bool
return true;
}

/**
* This method is perfectly sealed, nothing to downgrade here
*/
private function isSealedClass(ClassReflection $classReflection): bool
{
if (! $classReflection->isClass()) {
return false;
}

if (! $classReflection->isFinal()) {
return false;
}

return count($classReflection->getAncestors()) === 1;
}

private function isSafeType(ClassReflection $classReflection, ClassMethod $classMethod): bool
{
foreach ($this->safeTypes as $safeType) {
Expand Down

0 comments on commit b2c44bf

Please sign in to comment.