Home

Awesome

Dead code detector for PHP

PHPStan extension to find unused PHP code in your project with ease!

Summary:

Installation:

composer require --dev shipmonk/dead-code-detector

Use official extension-installer or just load the rules:

# phpstan.neon.dist
includes:
    - vendor/shipmonk/dead-code-detector/rules.neon

Supported libraries:

Symfony:

Doctrine:

PHPUnit:

PHPStan:

Nette:

All those libraries are autoenabled when found within your composer dependencies. If you want to force enable/disable some of them, you can:

# phpstan.neon.dist
parameters:
    shipmonkDeadCode:
        usageProviders:
            phpunit:
                enabled: true

Generic usage providers:

Reflection:

Vendor:

Those providers are enabled by default, but you can disable them if needed.

Customization:

# phpstan.neon.dist
services:
    -
        class: App\ApiOutputUsageProvider
        tags:
            - shipmonk.deadCode.memberUsageProvider

[!IMPORTANT] The interface & tag changed in 0.7. If you are using PHPStan 1.x, those were used differently.

Reflection-based customization:


use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;

class ApiOutputUsageProvider extends ReflectionBasedMemberUsageProvider
{

    public function shouldMarkMethodAsUsed(ReflectionMethod $method): bool
    {
        // all methods from our ApiOutput interface are called automatically (e.g. during serialization)
        return $method->getDeclaringClass()->implementsInterface(ApiOutput::class);
    }

}

AST-based customization:


use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
use Symfony\Component\Serializer\SerializerInterface;

class DeserializationUsageProvider implements MemberUsageProvider
{

    /**
     * @return list<ClassMemberUsage>
     */
    public function getUsages(Node $node, Scope $scope): array
    {
        if (!$node instanceof MethodCall) {
            return [];
        }

        if (
            // our deserialization calls constructor
            $scope->getType($node->var)->getObjectClassNames() === [SerializerInterface::class] &&
            $node->name->toString() === 'deserialize'
        ) {
            $secondArgument = $node->getArgs()[1]->value;
            $serializedClass = $scope->getType($secondArgument)->getConstantStrings()[0];

            // record the method it was called from (needed for proper transitive dead code elimination)
            $originRef = $this->getOriginMethodRef($scope);

            // record the hidden constructor call
            $constructorRef = new ClassMethodRef($serializedClass->getValue(), '__construct', false);

            return [new ClassMethodUsage($originRef, $constructorRef)];
        }

        return [];
    }

    private function getOriginMethodRef(Scope $scope): ?ClassMethodRef
    {
        return new ClassMethodRef(
            $scope->getClassReflection()->getName(),
            $scope->getFunction()->getName(),
            false,
        );
    }

}

Dead cycles & transitively dead methods

 ------ ------------------------------------------------------------------------
  Line   src/App/Facade/UserFacade.php
 ------ ------------------------------------------------------------------------
  26     Unused App\Facade\UserFacade::updateUserAddress
         ๐Ÿชช  shipmonk.deadMethod
         ๐Ÿ’ก Thus App\Entity\User::updateAddress is transitively also unused
         ๐Ÿ’ก Thus App\Entity\Address::setPostalCode is transitively also unused
         ๐Ÿ’ก Thus App\Entity\Address::setCountry is transitively also unused
         ๐Ÿ’ก Thus App\Entity\Address::setStreet is transitively also unused
         ๐Ÿ’ก Thus App\Entity\Address::MAX_STREET_CHARS is transitively also unused
 ------ ------------------------------------------------------------------------
parameters:
    shipmonkDeadCode:
        reportTransitivelyDeadMethodAsSeparateError: true

Automatic removal of dead code

vendor/bin/phpstan analyse --error-format removeDeadCode
class UserFacade
{
-    public const TRANSITIVELY_DEAD = 1;
-
-    public function deadMethod(): void
-    {
-        echo self::TRANSITIVELY_DEAD;
-    }
}

Calls over unknown types

parameters:
    shipmonkDeadCode:
        trackMixedAccess: false
Found 2 usages over unknown type:
 โ€ข setCountry method, for example in App\Entity\User::updateAddress
 โ€ข setStreet method, for example in App\Entity\User::updateAddress

Comparison with tomasvotruba/unused-public

Limitations:

Other problematic cases:

Constructors:

parameters:
    ignoreErrors:
        - '#^Unused .*?::__construct$#'

Private constructors:

Interface methods:

use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;

class IgnoreDeadInterfaceUsageProvider extends ReflectionBasedMemberUsageProvider
{
    public function shouldMarkMethodAsUsed(ReflectionMethod $method): bool
    {
        return $method->getDeclaringClass()->isInterface();
    }
}

Future scope:

Contributing

Supported PHP versions