shipmonk/dead-code-detector
PHPStan extension that detects unused PHP code: dead methods, properties, constants, and enum cases. Finds dead cycles and transitive dead members, can flag dead tested code, supports popular frameworks (e.g., Symfony), and offers customizable usage providers with optional auto-removal.
Route::get(), post(), put(), patch(), delete(), any(), match(), resource(), apiResource()
[Controller::class, 'method']'Controller@method'Controller::classEvent::listen(), Event::subscribe()handle* methods whose first parameter has a class typeSchedule::job()Gate::define() (callable + invokable), Gate::policy()$this->authorize() calls resolve policy class via Laravel's namespace convention (Models\ → Policies\)__construct, handle on Illuminate\Console\Command subclasses__construct, handle, failed, middleware, retryUntil, uniqueId, tags, backoff, uniqueVia, displayName on ShouldQueue / Dispatchable classes__construct, register, boothandle, terminate, __construct (detected by first parameter type Illuminate\Http\Request)via, toMail, toArray, toDatabase, toBroadcast, toVonage, toSlackauthorize, rules, messages, attributes, prepareForValidation, passedValidation, failedValidation, failedAuthorizationbefore, viewAny, view, create, update, delete, restore, forceDelete (heuristic via \Policies\ namespace + Policy suffix)build, content, envelope, attachments, headersbroadcastWith, broadcastAs, broadcastWhenpaginationInformationrouteNotificationFor* methodsboot, booted, casts, newFactoryscope* methodsIlluminate\Database\Eloquent\Relations\*Illuminate\Database\Eloquent\Casts\Attributeget*Attribute() / set*Attribute()Model::observe() calls + #[ObservedBy] attribute (all observer event methods)definition, configurerunup, down#[Autoconfigure(constructor:, calls:)], #[AutowireCallable], #[TaggedIterator], #[TaggedLocator], #[Required] on properties, #[AsSchedule], #[AsCronTask], #[AsPeriodicTask], !php/enum references in config yamls (#317)containerXmlPaths configuration - no need for phpstan/phpstan-doctrine for DIC calls analysis (#325)#[AsTwigComponent]/#[AsLiveComponent] (constructor, mount()), #[LiveProp], #[LiveAction], #[LiveListener], lifecycle hooks (#329)new ReflectionMethod(Class, 'method') (#313)PHP ^8.1 (was ^7.4) (#296)phpstan/phpstan ^2.1.41, (was ^2.1.23) (#297)$controller->render('my.twig', ['param' => $viewModel]),#[Template] controller methodsTwig\Environment::render() and similartest* methods, setUp/tearDown, [@dataProvider](https://github.com/dataProvider) (#299)ApiPhpDocUsageProvider: only count phpdocs within analysed files (#284)parameters:
shipmonkDeadCode:
detect:
deadEnumCases: false
deadProperties:
neverRead: false
neverWritten: false
stream_wrapper_register magic (#266)usageProviders.symfony.configDirusageExcluders.tests.devPathsphpstan/phpstan now requires ^2.1.23 (was ^2.1.12) (#254)#[AsDoctrineListener] #[AutoconfigureTag('doctrine.event_listener')] #[AsTransitionListenerWorkflow] and other Event listener attributes (#248, by @S1ructure)#[AsMessageHandler] attribute (#247, by @S1ructure)[@api](https://github.com/api) over enums and enum cases (#240)ReflectionClass<object>::getMethods() etc. (#241)parameters:
shipmonkDeadCode:
detect:
deadEnumCases: true
[@final](https://github.com/final) class is inherited (#233)ClassMemberRef is now generic, you may need to update phpdocs of thoseClassConstantRef constructor now requires TrinaryLogic $isEnumCase argumentphpstan/phpstan now requires ^2.1.12 (was ^2.1.9)usageProviders.symfony.configDir wiring (#216)#[DataProviderExternal] (#212)[@api](https://github.com/api) phpdoc for marking public API of libraries or tools (#200)
phpstan/phpstan-src itself :tada:devPaths config for tests excluder (#202)composer.json for tests excluder (#203)$foo->$unknown() (#189)
$foo as used (including possible descendants, ancestors, traits, ...)ReflectionClass->getMethods() now mark alive also methods of descendants (#189)ReflectionClass<object> (#193)
$anyObjectReflection->getMethod('foo') marks all foo methods of all types as aliveusageOverMixed excluder#[AsTwigFilter], #[AsTwigFunction], #[AsTwigTest] and new TwigFilter(..., callback) counterparts (#194, @zacharylund)- #[Deprecated] // [@phpstan-ignore](https://github.com/phpstan-ignore) shipmonk.deadMethod
- public function someMethod(): void
+ #[Deprecated]
+ public function someMethod(): void // [@phpstan-ignore](https://github.com/phpstan-ignore) shipmonk.deadMethod
#[Assert\Callback] (#180, @zacharylund)repositoryMethod in #[UniqueEntity] (#184) • Removed method UserFacade::deadMethod
! Excluded usage at tests/User/UserFacadeTest.php:241 left intact
phpstan/phpstan now requires ^2.1.9 (was ^2.1.7) (#169)parameters:
shipmonkDeadCode:
debug:
usagesOf:
- App\User\Entity\Address::__construct
with -vvv outputs e.g.:
App\User\Entity\Address::__construct
|
| Marked as alive by:
| entry virtual usage from ShipMonk\PHPStan\DeadCode\Provider\SymfonyUsageProvider (Route method via #[Route] attribute)
| calls App\User\RegisterUserController::__invoke:36
| calls App\User\UserFacade::registerUser:142
| calls App\User\Entity\Address::__construct
|
| Found 2 usages:
| • src/User/UserFacade.php:142
| • tests/User/Entity/AddressTest.php:64 - excluded by tests excluder
new Foo->method() does not mark method as used on Foo children anymoredefaultIndexMethod on AutowireLocator/AutowireIterator attribute (#161, @ruudk)phpstan/phpstan ^2.1.7 (was ^2.0.0) (#151)ClassMemberUsage now requires UsageOrigin, not optionally ClassMethodRef (#165)UsageOriginDetector was removed (#165)
UsageOrigin::createRegular or UsageOrigin::createVirtualReflectionBasedMemberUsageProvider now require note (#171)
true => VirtualUsageData::withNote('why considered used')false => nulltrackMixedAccess was replaced with excluder (#167)parameters:
shipmonkDeadCode:
- trackMixedAccess: false
+ usageExcluders:
+ usageOverMixed:
+ enabled: true
parameters:
shipmonkDeadCode:
usageExcluders:
tests:
enabled: true # off by default
MemberUsageExcluder interface!php const Foo\Bar::BAZ (#131)factory: [Acme\SerializerFactory, create] (#128)$unknown::CONST)--error-format removeDeadCode)PHP 8.2)constant('PDO::ATTR_ERRMODE'))!php/const Foo::BAR usages from your yamls)MemberUsageProvider$reflectionClass->getMethod('foo') marks foo method as usedEventSubscriberInterface::getSubscribedEvents (#122)Doctrine\Common\EventSubscriber::getSubscribedEvents (#127)# classes & methods
- `ShipMonk\PHPStan\DeadCode\Provider\MethodEntrypointProvider::getEntrypoints(): list<ReflectionMethod>`
+ `ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider::getUsages(): list<ClassMemberUsage>`
- `ShipMonk\PHPStan\DeadCode\Provider\SimpleMethodEntrypointProvider::isEntrypointMethod(): bool`
+ `ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider::shouldMarkMethodAsUsed(): bool`
# neon config params
- `parameters.shipmonkDeadCode.entrypoints`
+ `parameters.shipmonkDeadCode.usageProviders`
- `parameters.shipmonkDeadCode.trackCallsOnMixed`
+ `parameters.shipmonkDeadCode.trackMixedAccess`
# neon service tag
- `shipmonk.deadCode.entrypointProvider`
+ `shipmonk.deadCode.memberUsageProvider`
phpstan/phpstan 2.0trackMixedCalls: false (#109)vendor/bin/phpstan analyse --error-format removeDeadCode$unknownClass->method()phpstan/phpstan 1.11.7 (was 1.11.0) (#107) ------ ------------------------------------------------------------------------
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
#[AsCommand], #[AsController], Bundle children (#89)EntrypointProvider interface changed (#74)
SimpleMethodEntrypointProvider for easy transition__clone methods (#80)nette provider in config (#81)How can I help you explore Laravel packages today?