shipmonk/dead-code-detector

Dead code detector to find unused PHP code via PHPStan extension.

Installs: 14 197

Dependents: 5

Suggesters: 0

Security: 0

Stars: 130

Watchers: 4

Forks: 6

Open Issues: 9

Type:phpstan-extension

0.6.0 2024-11-11 15:02 UTC

README

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