shipmonk / dead-code-detector
Dead code detector to find unused PHP code via PHPStan extension.
Installs: 20 320
Dependents: 5
Suggesters: 0
Security: 0
Stars: 148
Watchers: 4
Forks: 8
Open Issues: 7
Type:phpstan-extension
Requires
- php: ^7.4 || ^8.0
- phpstan/phpstan: ^2.0
Requires (Dev)
- doctrine/orm: ^2.19 || ^3.0
- editorconfig-checker/editorconfig-checker: ^10.3.0
- ergebnis/composer-normalize: ^2.28
- nette/application: ^3.1
- nette/component-model: ^3.0
- nette/utils: ^3.0 || ^4.0
- nikic/php-parser: ^5.3.1
- phpstan/phpstan-phpunit: ^2.0.0
- phpstan/phpstan-strict-rules: ^2.0.0
- phpstan/phpstan-symfony: ^2.0.0
- phpunit/phpunit: ^9.6.21
- shipmonk/composer-dependency-analyser: ^1.6
- shipmonk/name-collision-detector: ^2.0.0
- shipmonk/phpstan-rules: ^4.0.0
- slevomat/coding-standard: ^8.15.0
- symfony/contracts: ^2.5 || ^3.0
- symfony/event-dispatcher: ^5.4 || ^6.0 || ^7.0
- symfony/http-kernel: ^5.4 || ^6.0 || ^7.0
- symfony/routing: ^5.4 || ^6.0 || ^7.0
This package is auto-updated.
Last update: 2024-12-22 09:08:08 UTC
README
PHPStan extension to find unused PHP code in your project with ease!
Summary:
- โ PHPStan extension
- โป๏ธ Dead cycles detection
- ๐ Transitive dead member detection
- ๐งน Automatic removal of unused code
- ๐ Popular libraries support
- โจ Customizable usage providers
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:
- constructor calls for DIC services!
phpstan/phpstan-symfony
withcontainerXmlPath
must be used
#[AsEventListener]
attribute#[AsController]
attribute#[AsCommand]
attribute#[Required]
attribute#[Route]
attributesEventSubscriberInterface::getSubscribedEvents
onKernelResponse
,onKernelRequest
, etc
Doctrine:
#[AsEntityListener]
attributeDoctrine\ORM\Events::*
eventsDoctrine\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: usageProviders: phpunit: enabled: true
Generic usage providers:
Reflection:
- Any constant or method accessed via
ReflectionClass
is detected as used- e.g.
$reflection->getConstructor()
,$reflection->getConstant('NAME')
,$reflection->getMethods()
, ...
- e.g.
Vendor:
- Any overridden method that originates in
vendor
is not reported as dead- e.g. implementing
Psr\Log\LoggerInterface::log
is automatically considered used
- e.g. implementing
Those providers are enabled by default, but you can disable them if needed.
Customization:
- If your application does some magic calls unknown to this library, you can implement your own usage provider.
- Just tag it with
shipmonk.deadCode.memberUsageProvider
and implementShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider
# 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:
- For simple reflection usecases, you can just extend
ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider
:
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:
- For more complex usecases that are deducible only from AST (e.g. serialization calls), you just stick with raw
MemberUsageProvider
interface:
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
- 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::MAX_STREET_CHARS 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 const TRANSITIVELY_DEAD = 1; - - public function deadMethod(): void - { - echo self::TRANSITIVELY_DEAD; - } }
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 namedmethod
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
:
- Such behaviour might not be desired for strictly typed codebases, because e.g. single
- The same applies to constant fetches over unknown types (e.g.
$unknown::CONSTANT
)
parameters: shipmonkDeadCode: trackMixedAccess: 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 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
- 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-in 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
- Only supported are:
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 constructor 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
MemberUsageProvider
:
- The easiest way to ignore it is via custom
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider; class IgnoreDeadInterfaceUsageProvider extends ReflectionBasedMemberUsageProvider { public function shouldMarkMethodAsUsed(ReflectionMethod $method): bool { return $method->getDeclaringClass()->isInterface(); } }
Future scope:
- 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