Skip to content

πŸ’€ PHP unused code detection via PHPStan extension. Detects dead cycles, supports libs like Symfony, Doctrine, PHPUnit etc. Can automatically remove dead PHP code.

Notifications You must be signed in to change notification settings

shipmonk-rnd/dead-code-detector

Repository files navigation

Dead code detector for PHP

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

Summary:

  • βœ… PHPStan extension
  • ♻️ Dead cycles detection
  • πŸ”— Transitive dead method detection
  • 🧹 Automatic removal of unused code
  • πŸ“š Popular libraries support
  • ✨ Customizable entrypoints

Installation:

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

Use official extension-installer or just load the rules:

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

Supported libraries

  • Any overridden method that originates in vendor is not reported as dead
  • We also support many magic calls in following libraries:

Symfony:

  • constructor calls for DIC services!
  • #[AsEventListener] attribute
  • #[AsController] attribute
  • #[AsCommand] attribute
  • #[Required] attribute
  • #[Route] attributes
  • onKernelResponse, onKernelRequest, etc

Doctrine:

  • #[AsEntityListener] attribute
  • Doctrine\ORM\Events::* events
  • Doctrine\Common\EventSubscriber methods
  • lifecycle event attributes #[PreFlush], #[PostLoad], ...

PHPUnit:

  • data provider methods
  • testXxx methods
  • annotations like @test, @before, @afterClass etc
  • attributes like #[Test], #[Before], #[AfterClass] etc

PHPStan:

  • constructor calls for DIC services (rules, extensions, ...)

Nette:

  • handleXxx, renderXxx, actionXxx, injectXxx, createComponentXxx
  • SmartObject magic calls for @property annotations

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:
        entrypoints:
            phpunit:
                enabled: true

Customization:

  • If your application does some magic calls unknown to this library, you can implement your own entrypoint provider.
  • Just tag it with shipmonk.deadCode.entrypointProvider and implement ShipMonk\PHPStan\DeadCode\Provider\MethodEntrypointProvider
  • You can simplify your implementation by extending ShipMonk\PHPStan\DeadCode\Provider\SimpleMethodEntrypointProvider
# phpstan.neon.dist
services:
    -
        class: App\ApiOutputEntrypointProvider
        tags:
            - shipmonk.deadCode.entrypointProvider
use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Provider\SimpleMethodEntrypointProvider;

class ApiOutputEntrypointProvider extends SimpleMethodEntrypointProvider
{

    public function isEntrypointMethod(ReflectionMethod $method): bool
    {
        return $method->getDeclaringClass()->implementsInterface(ApiOutput::class));
    }
}

Dead cycles & transitively dead methods

  • This library automatically detects dead cycles and transitively dead methods (methods that are only called from dead methods)
  • By default, it reports only the first dead method in the subtree and the rest as a tip:
 ------ ------------------------------------------------------------------------
  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::setZip is transitively also unused
 ------ ------------------------------------------------------------------------
  • If you want to report all dead methods individually, you can enable it in your phpstan.neon.dist:
parameters:
    shipmonkDeadCode:
        reportTransitivelyDeadMethodAsSeparateError: true

Automatic removal of dead code

  • If you are sure that the reported methods are dead, you can automatically remove them by running PHPStan with removeDeadCode error format:
vendor/bin/phpstan analyse --error-format removeDeadCode
class UserFacade
{
-    public function deadMethod(): void
-    {
-    }
}

Calls over unknown types

  • In order to prevent false positives, we support even calls over unknown types (e.g. $unknown->method()) by marking all methods named method as used
    • Such behaviour might not be desired for strictly typed codebases, because e.g. single new $unknown() will mark all constructors as used
    • Thus, you can disable this feature in your phpstan.neon.dist:
parameters:
    shipmonkDeadCode:
        trackCallsOnMixed: false
  • If you want to check how many of those cases are present in your codebase, you can run PHPStan analysis with -vvv and you will see some diagnostics:
Found 2 methods called over unknown type:
 β€’ setCountry, for example in App\Entity\User::updateAddress
 β€’ setStreet, for example in App\Entity\User::updateAddress

Comparison with tomasvotruba/unused-public

  • You can see detailed comparison PR
  • Basically, their analysis is less precise and less flexible. Mainly:
    • It cannot detect dead constructors
    • It does not properly detect calls within inheritance hierarchy
    • It does not offer any custom adjustments of used methods
    • It has almost no built-it library extensions
    • It ignores trait methods
    • Is lacks many minor features like class-string calls, dynamic method calls, array callbacks, nullsafe call chains etc
    • It cannot detect dead cycles nor transitively dead methods
    • It has no built-in dead code removal

Limitations:

  • Methods of anonymous classes are never reported as dead (PHPStan limitation)
  • Abstract trait methods are never reported as dead
  • Most magic methods (e.g. __get, __set etc) are never reported as dead
    • Only supported are: __construct, __clone

Other problematic cases:

Constructors:

  • For symfony apps & PHPStan extensions, we simplify the detection by assuming all DIC classes have used constructor.
  • For other apps, you may get false-positives if services are created magically.
    • To avoid those, you can easily disable consructor analysis with single ignore:
parameters:
    ignoreErrors:
        - '#^Unused .*?::__construct$#'

Private constructors:

  • Those are never reported as dead as those are often used to deny class instantiation

Interface methods:

  • If you never call interface method over the interface, but only over its implementors, it gets reported as dead
  • But you may want to keep the interface method to force some unification across implementors
    • The easiest way to ignore it is via custom MethodEntrypointProvider:
class IgnoreDeadInterfaceMethodsProvider extends SimpleMethodEntrypointProvider
{
    public function isEntrypointMethod(ReflectionMethod $method): bool
    {
        return $method->getDeclaringClass()->isInterface();
    }
}

Future scope:

  • Dead class constant detection
  • Dead class property detection
  • Dead class detection

Contributing

  • Check your code by composer check
  • Autofix coding-style by composer fix:cs
  • All functionality must be tested

Supported PHP versions

  • PHP 7.4 - 8.4

About

πŸ’€ PHP unused code detection via PHPStan extension. Detects dead cycles, supports libs like Symfony, Doctrine, PHPUnit etc. Can automatically remove dead PHP code.

Topics

Resources

Stars

Watchers

Forks

Languages