Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/set/dead-code/dead-classes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
services:
Rector\DeadCode\Rector\Class_\RemoveUnusedClassesRector: ~
28 changes: 27 additions & 1 deletion docs/AllRectorsOverview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# All 418 Rectors Overview
# All 419 Rectors Overview

- [Projects](#projects)
- [General](#general)
Expand Down Expand Up @@ -2277,6 +2277,32 @@ Remove unreachable statements

<br>

### `RemoveUnusedClassesRector`

- class: `Rector\DeadCode\Rector\Class_\RemoveUnusedClassesRector`

Remove unused classes without interface

```diff
interface SomeInterface
{
}

class SomeClass implements SomeInterface
{
public function run($items)
{
return null;
}
-}
-
-class NowhereUsedClass
-{
}
```

<br>

### `RemoveUnusedDoctrineEntityMethodAndPropertyRector`

- class: `Rector\DeadCode\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector`
Expand Down
113 changes: 113 additions & 0 deletions packages/DeadCode/src/Rector/Class_/RemoveUnusedClassesRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace Rector\DeadCode\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use Rector\DeadCode\UnusedNodeResolver\UnusedClassResolver;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;

/**
* @see \Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedClassesRector\RemoveUnusedClassesRectorTest
*/
final class RemoveUnusedClassesRector extends AbstractRector
{
/**
* @var UnusedClassResolver
*/
private $unusedClassResolver;

public function __construct(UnusedClassResolver $unusedClassResolver)
{
$this->unusedClassResolver = $unusedClassResolver;
}

public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Remove unused classes without interface', [
new CodeSample(
<<<'PHP'
interface SomeInterface
{
}

class SomeClass implements SomeInterface
{
public function run($items)
{
return null;
}
}

class NowhereUsedClass
{
}
PHP
,
<<<'PHP'
interface SomeInterface
{
}

class SomeClass implements SomeInterface
{
public function run($items)
{
return null;
}
}
PHP

),
]);
}

/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if ($this->shouldSkip($node)) {
return null;
}

if ($this->unusedClassResolver->isClassUsed($node)) {
return null;
}

$this->removeNode($node);

return null;
}

private function shouldSkip(Class_ $class): bool
{
if (! $this->unusedClassResolver->isClassWithoutInterfaceAndNotController($class)) {
return true;
}

if ($this->isDoctrineEntityClass($class)) {
return true;
}

// most of factories can be only registered in config and create services there
// skip them for now; but in the future, detect types they create in public methods and only keep them, if they're used
if ($this->isName($class, '*Factory')) {
return true;
}

return $class->isAbstract();
}
}
193 changes: 193 additions & 0 deletions packages/DeadCode/src/UnusedNodeResolver/UnusedClassResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<?php

declare(strict_types=1);

namespace Rector\DeadCode\UnusedNodeResolver;

use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Class_;
use Rector\Exception\NotImplementedException;
use Rector\NodeContainer\ParsedNodesByType;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\Testing\PHPUnit\PHPUnitEnvironment;

final class UnusedClassResolver
{
/**
* @var NameResolver
*/
private $nameResolver;

/**
* @var ParsedNodesByType
*/
private $parsedNodesByType;

/**
* @var string[]
*/
private $cachedUsedClassNames = [];

public function __construct(NameResolver $nameResolver, ParsedNodesByType $parsedNodesByType)
{
$this->nameResolver = $nameResolver;
$this->parsedNodesByType = $parsedNodesByType;
}

/**
* @return string[]
*/
public function getUsedClassNames(): array
{
if (! PHPUnitEnvironment::isPHPUnitRun()) {
if ($this->cachedUsedClassNames !== []) {
return $this->cachedUsedClassNames;
}
}

$cachedUsedClassNames = array_merge(
$this->getParamNodesClassNames(),
$this->getNewNodesClassNames(),
$this->getStaticCallClassNames(),
$this->getClassConstantFetchNames()
);

$cachedUsedClassNames = $this->sortAndUniqueArray($cachedUsedClassNames);

return $this->cachedUsedClassNames = $cachedUsedClassNames;
}

public function isClassWithoutInterfaceAndNotController(Class_ $class): bool
{
if ($class->implements !== []) {
return false;
}

if ($class->extends !== null) {
return false;
}

if ($this->nameResolver->isNames($class, ['*Controller', '*Presenter'])) {
return false;
}

if ($this->nameResolver->isName($class, '*Test')) {
return false;
}

return true;
}

public function isClassUsed(Class_ $class): bool
{
return $this->nameResolver->isNames($class, $this->getUsedClassNames());
}

/**
* @return string[]
*/
private function getParamNodesClassNames(): array
{
$classNames = [];

/** @var Param[] $paramNodes */
$paramNodes = $this->parsedNodesByType->getNodesByType(Param::class);
foreach ($paramNodes as $paramNode) {
if ($paramNode->type === null) {
continue;
}

if ($paramNode->type instanceof NullableType) {
$paramNode = $paramNode->type;
}

if ($paramNode->type instanceof Identifier) {
continue;
}

if ($paramNode->type instanceof Name) {
/** @var string $paramTypeName */
$paramTypeName = $this->nameResolver->getName($paramNode->type);
$classNames[] = $paramTypeName;
} else {
throw new NotImplementedException();
}
}

return $classNames;
}

/**
* @return string[]
*/
private function getNewNodesClassNames(): array
{
$classNames = [];

/** @var New_[] $newNodes */
$newNodes = $this->parsedNodesByType->getNodesByType(New_::class);
foreach ($newNodes as $newNode) {
$newNodeClassName = $this->nameResolver->getName($newNode->class);
if (! is_string($newNodeClassName)) {
continue;
}

$classNames[] = $newNodeClassName;
}

return $classNames;
}

/**
* @return string[]
*/
private function getStaticCallClassNames(): array
{
$classNames = [];

/** @var StaticCall[] $staticCallNodes */
$staticCallNodes = $this->parsedNodesByType->getNodesByType(StaticCall::class);
foreach ($staticCallNodes as $staticCallNode) {
$staticClassName = $this->nameResolver->getName($staticCallNode->class);
if (! is_string($staticClassName)) {
continue;
}

$classNames[] = $staticClassName;
}
return $classNames;
}

/**
* @return string[]
*/
private function getClassConstantFetchNames(): array
{
/** @var ClassConstFetch[] $classConstFetches */
$classConstFetches = $this->parsedNodesByType->getNodesByType(ClassConstFetch::class);

$classNames = [];
foreach ($classConstFetches as $classConstFetch) {
$className = $this->nameResolver->getName($classConstFetch->class);
if (! is_string($className)) {
continue;
}

$classNames[] = $className;
}

return $classNames;
}

private function sortAndUniqueArray(array $items): array
{
sort($items);
return array_unique($items);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedClassesRector\Fixture;

use Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedClassesRector\Source\SomeInterface;

class SomeClass implements SomeInterface
{
public function run($items)
{
return null;
}
}

class NowhereUsedClass
{
}

?>
-----
<?php

namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedClassesRector\Fixture;

use Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedClassesRector\Source\SomeInterface;

class SomeClass implements SomeInterface
{
public function run($items)
{
return null;
}
}

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

namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedClassesRector\Fixture;

abstract class SkipAbstractClass
{
}
Loading