[DeadCode] Add RemoveDeadRecursiveClassMethodRector

This commit is contained in:
TomasVotruba 2020-03-30 21:08:12 +02:00
parent 0c2cbca6f2
commit 9a9f777513
24 changed files with 533 additions and 59 deletions

View File

@ -7,8 +7,8 @@ services:
Rector\CodingStyle\Rector\Use_\RemoveUnusedAliasRector: null
# requires configuration
# Rector\CodingStyle\Rector\ClassMethod\YieldClassMethodToArrayClassMethodRector: null
# Rector\CodingStyle\Rector\ClassMethod\ReturnArrayClassMethodToYieldRector: null
# Rector\CodingStyle\Rector\ClassMethod\YieldClassMethodToArrayClassMethodRector: null
# Rector\CodingStyle\Rector\ClassMethod\ReturnArrayClassMethodToYieldRector: null
Rector\CodingStyle\Rector\String_\SymplifyQuoteEscapeRector: null
Rector\CodingStyle\Rector\ClassConst\SplitGroupedConstantsAndPropertiesRector: null
Rector\CodingStyle\Rector\String_\SplitStringClassConstantToClassConstFetchRector: null

View File

@ -38,3 +38,4 @@ services:
Rector\DeadCode\Rector\Function_\RemoveUnusedFunctionRector: null
Rector\DeadCode\Rector\If_\RemoveUnusedNonEmptyArrayBeforeForeachRector: null
Rector\DeadCode\Rector\Assign\RemoveAssignOfVoidReturnFunctionRector: null
Rector\DeadCode\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector: null

View File

@ -1,4 +1,4 @@
# All 475 Rectors Overview
# All 476 Rectors Overview
- [Projects](#projects)
- [General](#general)
@ -2645,6 +2645,25 @@ Remove if, foreach and for that does not do anything
<br>
### `RemoveDeadRecursiveClassMethodRector`
- class: [`Rector\DeadCode\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector`](/../master/rules/dead-code/src/Rector/ClassMethod/RemoveDeadRecursiveClassMethodRector.php)
- [test fixtures](/../master/rules/dead-code/tests/Rector/ClassMethod/RemoveDeadRecursiveClassMethodRector/Fixture)
Remove unused public method that only calls itself recursively
```diff
class SomeClass
{
- public function run()
- {
- return $this->run();
- }
}
```
<br>
### `RemoveDeadReturnRector`
- class: [`Rector\DeadCode\Rector\FunctionLike\RemoveDeadReturnRector`](/../master/rules/dead-code/src/Rector/FunctionLike/RemoveDeadReturnRector.php)

View File

@ -228,16 +228,9 @@ final class ParsedNodeCollector
public function findClassConstantByClassConstFetch(ClassConstFetch $classConstFetch): ?ClassConst
{
$class = $this->nodeNameResolver->getName($classConstFetch->class);
if ($class === 'self') {
/** @var string|null $class */
$class = $classConstFetch->getAttribute(AttributeKey::CLASS_NAME);
} elseif ($class === 'parent') {
/** @var string|null $class */
$class = $classConstFetch->getAttribute(AttributeKey::PARENT_CLASS_NAME);
}
$className = $this->nodeNameResolver->getName($classConstFetch->class);
$class = $this->resolveClassConstant($classConstFetch, $className);
if ($class === null) {
throw new NotImplementedException();
}
@ -284,4 +277,17 @@ final class ParsedNodeCollector
// PHPStan polution
return Strings::startsWith($classNode->name->toString(), 'AnonymousClass');
}
private function resolveClassConstant(ClassConstFetch $classConstFetch, ?string $className): ?string
{
if ($className === 'self') {
return $classConstFetch->getAttribute(AttributeKey::CLASS_NAME);
}
if ($className === 'parent') {
return $classConstFetch->getAttribute(AttributeKey::PARENT_CLASS_NAME);
}
return $className;
}
}

View File

@ -26,7 +26,7 @@ final class MethodCallParsedNodesFinder
/**
* @return MethodCall[]|StaticCall[]|ArrayCallable[]
*/
public function findClassMethodCalls(ClassMethod $classMethod): array
public function findByClassMethod(ClassMethod $classMethod): array
{
/** @var string|null $className */
$className = $classMethod->getAttribute(AttributeKey::CLASS_NAME);

View File

@ -1,6 +1,6 @@
services:
Rector\Privatization\Rector\MethodCall\PrivatizeLocalGetterToPropertyRector: null
# Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: null
Rector\DeadCode\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector: null
# Rector\Privatization\Rector\MethodCall\PrivatizeLocalGetterToPropertyRector: null
imports:
- { resource: "create-rector.yaml", ignore_errors: true }

View File

@ -6,10 +6,13 @@ namespace Rector\CodingStyle\Application;
use PhpParser\Node;
use PhpParser\Node\Name;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\CodingStyle\Node\NameImporter;
use Rector\Core\Configuration\Option;
use Rector\Core\Contract\PhpParser\Node\CommanderInterface;
use Rector\Core\PhpParser\NodeTraverser\CallableNodeTraverser;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockNameImporter;
use Symplify\PackageBuilder\Parameter\ParameterProvider;
final class NameImportingCommander implements CommanderInterface
@ -29,14 +32,28 @@ final class NameImportingCommander implements CommanderInterface
*/
private $callableNodeTraverser;
/**
* @var bool
*/
private $importDocBlocks = false;
/**
* @var DocBlockNameImporter
*/
private $docBlockNameImporter;
public function __construct(
ParameterProvider $parameterProvider,
NameImporter $nameImporter,
CallableNodeTraverser $callableNodeTraverser
CallableNodeTraverser $callableNodeTraverser,
DocBlockNameImporter $docBlockNameImporter,
bool $importDocBlocks
) {
$this->parameterProvider = $parameterProvider;
$this->nameImporter = $nameImporter;
$this->callableNodeTraverser = $callableNodeTraverser;
$this->importDocBlocks = $importDocBlocks;
$this->docBlockNameImporter = $docBlockNameImporter;
}
public function isActive(): bool
@ -50,12 +67,27 @@ final class NameImportingCommander implements CommanderInterface
*/
public function traverseNodes(array $nodes): array
{
$this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node): ?Name {
if (! $node instanceof Name) {
return null;
$this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node): ?Node {
if ($node instanceof Name) {
return $this->nameImporter->importName($node);
}
return $this->nameImporter->importName($node);
if ($this->importDocBlocks) {
/** @var PhpDocInfo|null $phpDocInfo */
$phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO);
if ($phpDocInfo === null) {
return null;
}
$hasChanged = $this->docBlockNameImporter->importNames($phpDocInfo, $node);
if (! $hasChanged) {
return null;
}
return $node;
}
return null;
});
return $nodes;

View File

@ -17,22 +17,17 @@ use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockNameImporter;
/**
* This rule must be last, as it shortens nodes before using them by other Rectors, thus breaking them.
*
* This file remains just for testing.
* Breaks other rectors by making name nodes short, FQN short names
*
* @see \Rector\CodingStyle\Tests\Rector\Namespace_\ImportFullyQualifiedNamesRector\ImportFullyQualifiedNamesRectorTest
* @see \Rector\CodingStyle\Tests\Rector\Namespace_\ImportFullyQualifiedNamesRector\NonNamespacedTest
* @see \Rector\CodingStyle\Tests\Rector\Namespace_\ImportFullyQualifiedNamesRector\ImportRootNamespaceClassesDisabledTest
*/
final class ImportFullyQualifiedNamesRector extends AbstractRector
{
/**
* @var bool
*/
private $importDocBlocks = true;
/**
* @var bool
*/
private $autoImportNames = false;
/**
* @var NameImporter
*/
@ -43,15 +38,9 @@ final class ImportFullyQualifiedNamesRector extends AbstractRector
*/
private $docBlockNameImporter;
public function __construct(
NameImporter $nameImporter,
bool $importDocBlocks,
bool $autoImportNames,
DocBlockNameImporter $docBlockNameImporter
) {
public function __construct(NameImporter $nameImporter, DocBlockNameImporter $docBlockNameImporter)
{
$this->nameImporter = $nameImporter;
$this->importDocBlocks = $importDocBlocks;
$this->autoImportNames = $autoImportNames;
$this->docBlockNameImporter = $docBlockNameImporter;
}
@ -108,8 +97,10 @@ PHP
*/
public function refactor(Node $node): ?Node
{
// this file remains just for testing
// breaks other rectors by making name nodes short, FQN → short names
/** prevents duplicated run with @see NameImportingCommander */
if ($this->autoImportNames && ! PHPUnitEnvironment::isPHPUnitRun()) {
if (! PHPUnitEnvironment::isPHPUnitRun()) {
return null;
}
@ -120,21 +111,17 @@ PHP
}
// process doc blocks
if ($this->importDocBlocks) {
/** @var PhpDocInfo|null $phpDocInfo */
$phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO);
if ($phpDocInfo === null) {
return null;
}
$hasChanged = $this->docBlockNameImporter->importNames($phpDocInfo, $node);
if (! $hasChanged) {
return null;
}
return $node;
/** @var PhpDocInfo|null $phpDocInfo */
$phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO);
if ($phpDocInfo === null) {
return null;
}
return null;
$hasChanged = $this->docBlockNameImporter->importNames($phpDocInfo, $node);
if (! $hasChanged) {
return null;
}
return $node;
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Rector\DeadCode\NodeManipulator;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\NodeTypeResolver;
final class ClassMethodAndCallMatcher
{
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
public function __construct(NodeNameResolver $nodeNameResolver, NodeTypeResolver $nodeTypeResolver)
{
$this->nodeNameResolver = $nodeNameResolver;
$this->nodeTypeResolver = $nodeTypeResolver;
}
public function isMethodLikeCallMatchingClassMethod(Node $node, ClassMethod $classMethod): bool
{
if ($node instanceof MethodCall) {
return $this->isMethodCallMatchingClassMethod($node, $classMethod);
}
if ($node instanceof StaticCall) {
return $this->isStaticCallMatchingClassMethod($node, $classMethod);
}
return false;
}
private function isMethodCallMatchingClassMethod(MethodCall $methodCall, ClassMethod $classMethod): bool
{
/** @var string $className */
$className = $classMethod->getAttribute(AttributeKey::CLASS_NAME);
/** @var string $classMethodName */
$classMethodName = $this->nodeNameResolver->getName($classMethod);
if (! $this->nodeNameResolver->isName($methodCall->name, $classMethodName)) {
return false;
}
$classMethodStaticType = new ObjectType($className);
$callerStaticType = $this->nodeTypeResolver->getStaticType($methodCall->var);
return $classMethodStaticType->isSuperTypeOf($callerStaticType)->yes();
}
private function isStaticCallMatchingClassMethod(StaticCall $staticCall, ClassMethod $classMethod): bool
{
/** @var string $className */
$className = $classMethod->getAttribute(AttributeKey::CLASS_NAME);
/** @var string $methodName */
$methodName = $this->nodeNameResolver->getName($classMethod);
if (! $this->nodeNameResolver->isName($staticCall->name, $methodName)) {
return false;
}
$classMethodStaticType = new ObjectType($className);
$callerStaticType = $this->nodeTypeResolver->resolve($staticCall->class);
if ($callerStaticType instanceof MixedType) {
return false;
}
return $classMethodStaticType->isSuperTypeOf($callerStaticType)->yes();
}
}

View File

@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Rector\DeadCode\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\DeadCode\NodeManipulator\ClassMethodAndCallMatcher;
use Rector\NodeCollector\NodeFinder\MethodCallParsedNodesFinder;
use Rector\NodeCollector\ValueObject\ArrayCallable;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodVendorLockResolver;
/**
* @see \Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\RemoveDeadRecursiveClassMethodRectorTest
*/
final class RemoveDeadRecursiveClassMethodRector extends AbstractRector
{
/**
* @var MethodCallParsedNodesFinder
*/
private $methodCallParsedNodesFinder;
/**
* @var ClassMethodAndCallMatcher
*/
private $classMethodAndCallMatcher;
/**
* @var ClassMethodVendorLockResolver
*/
private $classMethodVendorLockResolver;
public function __construct(
MethodCallParsedNodesFinder $methodCallParsedNodesFinder,
ClassMethodAndCallMatcher $classMethodAndCallMatcher,
ClassMethodVendorLockResolver $classMethodVendorLockResolver
) {
$this->methodCallParsedNodesFinder = $methodCallParsedNodesFinder;
$this->classMethodAndCallMatcher = $classMethodAndCallMatcher;
$this->classMethodVendorLockResolver = $classMethodVendorLockResolver;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Remove unused public method that only calls itself recursively', [
new CodeSample(
<<<'PHP'
class SomeClass
{
public function run()
{
return $this->run();
}
}
PHP
,
<<<'PHP'
class SomeClass
{
}
PHP
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
$class = $node->getAttribute(AttributeKey::CLASS_NODE);
if (! $class instanceof Class_) {
return null;
}
if (! $this->containsClassMethodAnyCalls($node)) {
return null;
}
$methodCalls = $this->methodCallParsedNodesFinder->findByClassMethod($node);
// handles remove dead methods rules
if ($methodCalls === []) {
return null;
}
foreach ($methodCalls as $methodCall) {
if ($this->shouldSkipCall($node, $methodCall)) {
return null;
}
}
$this->removeNode($node);
return null;
}
/**
* @param StaticCall|MethodCall|ArrayCallable $methodCall
*/
private function shouldSkipCall(ClassMethod $classMethod, object $methodCall): bool
{
if ($this->classMethodVendorLockResolver->isRemovalVendorLocked($classMethod)) {
return true;
}
if (! $methodCall instanceof MethodCall && ! $methodCall instanceof StaticCall) {
return true;
}
/** @var string $methodCallMethodName */
$methodCallMethodName = $methodCall->getAttribute(AttributeKey::METHOD_NAME);
// is method called not in itself
if (! $this->isName($methodCall->name, $methodCallMethodName)) {
return true;
}
// differnt class, probably inheritance
if ($methodCall->getAttribute(AttributeKey::CLASS_NAME) !== $classMethod->getAttribute(
AttributeKey::CLASS_NAME
)) {
return true;
}
return ! $this->classMethodAndCallMatcher->isMethodLikeCallMatchingClassMethod($methodCall, $classMethod);
}
private function containsClassMethodAnyCalls(ClassMethod $classMethod): bool
{
return $this->betterNodeFinder->hasInstancesOf($classMethod, [MethodCall::class, StaticCall::class]);
}
}

View File

@ -79,7 +79,7 @@ PHP
return null;
}
$classMethodCalls = $this->methodCallParsedNodesFinder->findClassMethodCalls($node);
$classMethodCalls = $this->methodCallParsedNodesFinder->findByClassMethod($node);
if ($classMethodCalls !== []) {
return null;
}

View File

@ -178,6 +178,10 @@ final class UnusedClassResolver
return $classNames;
}
/**
* @param string[] $items
* @return string[]
*/
private function sortAndUniqueArray(array $items): array
{
sort($items);

View File

@ -0,0 +1,23 @@
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class SomeClass
{
public function run()
{
return $this->run();
}
}
?>
-----
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class SomeClass
{
}
?>

View File

@ -0,0 +1,23 @@
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class PrivateToo
{
public function run()
{
return $this->run();
}
}
?>
-----
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class PrivateToo
{
}
?>

View File

@ -0,0 +1,39 @@
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
use Symplify\SmartFileSystem\SmartFileInfo;
class FileRemover
{
/**
* @var SmartFileInfo[]
*/
private $removedFiles;
public function removeFile(SmartFileInfo $smartFileInfo): void
{
$this->removedFiles[$smartFileInfo->getRealPath()] = $smartFileInfo;
}
}
abstract class AbstractClassUsingFileRemover
{
/**
* @var FileRemover
*/
private $removeFile;
protected function removeFile(SmartFileInfo $smartFileInfo): void
{
$this->removeFile->removeFile($smartFileInfo);
}
}
class ClassExtendingClass extends AbstractClassUsingFileRemover
{
public function run($smartFileInfo)
{
$this->removeFile($smartFileInfo);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
interface SkipInterface
{
public function process();
}
class ImplementerOfTheInterface implements SkipInterface
{
public function process()
{
$this->process();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class SkipUsed
{
public function run()
{
return $this->run();
}
public function skipUsed()
{
$this->run();
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class StaticCall
{
public static function run()
{
return self::run();
}
}
?>
-----
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class StaticCall
{
}
?>

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector;
use Iterator;
use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;
use Rector\DeadCode\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector;
final class RemoveDeadRecursiveClassMethodRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(string $file): void
{
$this->doTestFile($file);
}
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getRectorClass(): string
{
return RemoveDeadRecursiveClassMethodRector::class;
}
}

View File

@ -155,7 +155,7 @@ PHP
private function hasExternalCall(ClassMethod $classMethod): bool
{
$methodCalls = $this->methodCallParsedNodesFinder->findClassMethodCalls($classMethod);
$methodCalls = $this->methodCallParsedNodesFinder->findByClassMethod($classMethod);
$methodName = $this->getName($classMethod);
if ($this->isArrayCallable($classMethod, $methodCalls, $methodName)) {

View File

@ -98,7 +98,7 @@ PHP
*/
public function refactor(Node $node): ?Node
{
$classMethodCalls = $this->methodCallParsedNodesFinder->findClassMethodCalls($node);
$classMethodCalls = $this->methodCallParsedNodesFinder->findByClassMethod($node);
$classParameterTypes = $this->getCallTypesByPosition($classMethodCalls);
foreach ($classParameterTypes as $position => $argumentStaticType) {

View File

@ -25,11 +25,12 @@ final class ExclusionManager
public function isNodeSkippedByRector(PhpRectorInterface $phpRector, Node $onNode): bool
{
foreach ($this->exclusionChecks as $check) {
if ($check->isNodeSkippedByRector($phpRector, $onNode)) {
foreach ($this->exclusionChecks as $exclusionCheck) {
if ($exclusionCheck->isNodeSkippedByRector($phpRector, $onNode)) {
return true;
}
}
return false;
}
}

View File

@ -106,6 +106,23 @@ final class BetterNodeFinder
return $this->nodeFinder->findFirstInstanceOf($nodes, $type);
}
/**
* @param Node|Node[] $nodes
* @param string[] $types
*/
public function hasInstancesOf($nodes, array $types): bool
{
foreach ($types as $type) {
if ($this->nodeFinder->findFirstInstanceOf($nodes, $type) === null) {
continue;
}
return true;
}
return false;
}
/**
* @param Node|Node[] $nodes
*/

View File

@ -82,7 +82,7 @@ trait ComplexRemovalTrait
{
$this->removeNode($classMethod);
$classMethodCalls = $this->methodCallParsedNodesFinder->findClassMethodCalls($classMethod);
$classMethodCalls = $this->methodCallParsedNodesFinder->findByClassMethod($classMethod);
foreach ($classMethodCalls as $classMethodCall) {
if ($classMethodCall instanceof ArrayCallable) {
continue;