π PHP unused code detection via PHPStan extension. Detects dead cycles, supports libs like Laravel, Symfony, Twig, Doctrine, PHPUnit etc. Can automatically remove dead PHP code. Able to detect dead code used only in tests. Finds unused methods, constants, properties and enum cases.
PHP
529
349 commits
updated Oct 13, 2026
PHPStan extension to find unused PHP code in your project with ease!
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
$ vendor/bin/phpstan
[!NOTE] Make sure you analyse whole codebase (e.g. both
srcandtests) so that all usages are found.
Check out the recording and slides from TechMeetup Conference (2025) about this package:
All dead class member types are detected by default, you can disable some if needed:
parameters:
shipmonkDeadCode:
detect:
deadMethods: true
deadConstants: true
deadEnumCases: true
deadProperties:
neverRead: true
neverWritten: true
phpstan/phpstan-symfony with containerXmlPathshipmonkDeadCode.usageProviders.symfony.containerXmlPaths configured#[AsEventListener], #[AsMessageHandler], #[AsController], #[AsCommand]#[Assert\Callback], #[Interact], #[Route], #[Required] (methods and properties)#[AsSchedule], #[AsCronTask], #[AsPeriodicTask]#[AutoconfigureTag('doctrine.event_listener')], #[Autoconfigure(constructor:)], #[Autoconfigure(calls:)], #[AutowireCallable]defaultIndexMethod and defaultPriorityMethod in #[AutowireLocator], #[AutowireIterator], #[TaggedIterator], #[TaggedLocator]#[AsAnnounceListener], ...EventSubscriberInterface::getSubscribedEventsonKernelResponse, onKernelRequest, etc!php/const and !php/enum references in config yamls#[AsTwigComponent]/#[AsLiveComponent] (constructor, mount()), #[LiveProp], #[LiveAction], #[LiveListener], lifecycle hooks#[AsEntityListener], #[AsDoctrineListener] attributedoctrine.event_listener tags in the Symfony container XMLDoctrine\ORM\Events::* events, Doctrine\Common\EventSubscriber methodsrepositoryMethod in #[UniqueEntity] attribute#[PreFlush], #[PostLoad], ...#[Column(enumType: UserStatus::class)]testXxx methods@test, @before, @afterClass etc#[Test], #[Before], #[AfterClass] etcbenchXxx methods#[BeforeMethods], #[AfterMethods] attributes#[ParamProviders] attribute for param provider methods@Given, @When, @Then) or attributes (#[Given], #[When], #[Then])@BeforeScenario, @AfterScenario, etc.) or attributes (#[BeforeScenario], #[AfterScenario], etc.)@Transform or #[Transform]test* or #[TestRule]) of classes registered via the phpat.test service taghandleXxx, renderXxx, actionXxx, injectXxx, createComponentXxxSmartObject magic calls for @property annotationsshipmonkDeadCode.usageProviders.nette.containerNeonPaths is configured (requires nette/neon)test* methods, setUp/tearDown, @dataProvider methods in Tester\TestCase subclassesRoute::get/post/put/...(), resource(), apiResource() with callable, string (Controller@method), and invokable syntaxEvent::listen(), Event::subscribe(), auto-discovered listeners (handle*/__invoke with typed first param)Schedule::job()Gate::define(), Gate::policy(), $this->authorize() with automatic policy class resolution\Facades\... static calls mapped to the underlying classboot, booted, casts, newFactory, query scopes, relationships, attribute accessors (modern + legacy)Model::observe() + #[ObservedBy] attributedefinition, configure), seeders (run), migrations (up, down)$controller->render('my.twig', ['param' => $viewModel]),#[Template] controller methodsTwig\Environment::render() and similar#[AsTwigFilter], #[AsTwigFunction], #[AsTwigTest]new TwigFilter(..., callback), new TwigFunction(..., callback), new TwigTest(..., callback)All those libraries are autoenabled when found within your composer dependencies. If you want to force enable/disable some of them, you can:
parameters:
shipmonkDeadCode:
usageProviders:
phpunit:
enabled: true
ReflectionClass is detected as used
$reflection->getConstructor(), $reflection->getConstant('NAME'), $reflection->getMethods(), $reflection->getCases()...vendor is not reported as dead
Psr\Log\LoggerInterface::log is automatically considered usedIteratorAggregate::getIterator is automatically considered usedBackedEnum::from, BackedEnum::tryFrom and UnitEnum::casesstream_wrapper_registerscripts section of composer.json
"post-install-cmd": "MyVendor\\MyClass::postInstall"composer.json is autodetected, but you can point it elsewhere via shipmonkDeadCode.usageProviders.composer.composerJsonPathThose providers are enabled by default, but you can disable them if needed.
src is only used in teststests usage excluder:
parameters:
shipmonkDeadCode:
usageExcluders:
tests:
enabled: true
devPaths: # optional, autodetects from autoload-dev sections of composer.json when omitted; entries may contain * globs (like Composer classmap paths)
- %currentWorkingDirectory%/tests
With such setup, members used only in tests will be reported with corresponding message, e.g:
Unused AddressValidator::isValidPostalCode (all usages excluded by tests excluder)
[!TIP] We recommend enabling this excluder for all projects.
shipmonk.deadCode.memberUsageProvider and implement ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProviderservices:
-
class: App\ApiOutputUsageProvider
tags:
- shipmonk.deadCode.memberUsageProvider
ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider:
use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
class FuzzyTwigUsageProvider extends ReflectionBasedMemberUsageProvider
{
public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData
{
if ($method->getDeclaringClass()->implementsInterface(UsedInTwigMarkerInterface::class)) {
return VirtualUsageData::withNote('Probably used in twig');
}
return null;
}
}
MemberUsageProvider interface.User::__construct usage in following PHP snippet:function test(SerializerInterface $serializer): User {
return $serializer->deserialize('{"name": "John"}', User::class, 'json');
}
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
use Symfony\Component\Serializer\SerializerInterface;
class DeserializationUsageProvider implements MemberUsageProvider
{
public function __construct(
private UsageOriginDetector $originDetector,
) {}
/**
* @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 place it was called from (needed for proper transitive dead code elimination)
$usageOrigin = UsageOrigin::createRegular($node, $scope);
// record the hidden constructor call
$constructorRef = new ClassMethodRef($serializedClass->getValue(), '__construct', possibleDescendant: false);
return [new ClassMethodUsage($usageOrigin, $constructorRef)];
}
return [];
}
}
You can exclude any usage based on custom logic, just implement MemberUsageExcluder and register it with shipmonk.deadCode.memberUsageExcluder tag:
use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;
class MyUsageExcluder implements MemberUsageExcluder
{
public function shouldExclude(ClassMemberUsage $usage, Node $node, Scope $scope): bool
{
// ...
}
}
# phpstan.neon.dist
services:
-
class: App\MyUsageExcluder
tags:
- shipmonk.deadCode.memberUsageExcluder
The same interface is used for exclusion of test-only usages, see above.
[!NOTE] Excluders are called after providers.
------ ------------------------------------------------------------------------
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
------ ------------------------------------------------------------------------
phpstan.neon.dist:parameters:
shipmonkDeadCode:
reportTransitivelyDeadMethodAsSeparateError: true
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;
- }
}
editorUrl parameter) β’ Removed method UserFacade::deadMethod
! Excluded usage at tests/User/UserFacadeTest.php:241 left intact
$unknown->method()) by marking all methods named method as used
new $unknown() will mark all constructors as used$unknown::CONSTANT)phpstan.neon.dist by excluding such usages:parameters:
shipmonkDeadCode:
usageExcluders:
usageOverMixed:
enabled: true
-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
$class->$unknown()) by marking all possible methods as used$unknownClass->$unknownMethod()), we ignore such usage (as it would mark all methods in codebase as used) and show warning in debug verbosity (-vvv)ReflectionClass also emit unknown method calls:/** @var ReflectionClass<Foo> $reflection */
$methods = $reflection->getMethods(); // all Foo methods are used here
__get, __set etc) are never reported as dead
__construct, __cloneparameters:
ignoreErrors:
- '#^Unused .*?::__construct$#'
neverReaduse ReflectionProperty;
use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
class ApiOutputPropertyUsageProvider extends ReflectionBasedMemberUsageProvider
{
protected function shouldMarkPropertyAsRead(ReflectionProperty $property): ?VirtualUsageData
{
if ($property->getDeclaringClass()->implementsInterface(ApiOutput::class)) {
return VirtualUsageData::withNote('Used upon JSON serialization');
}
return null;
}
}
Controller return value, use AST-based provider
MemberUsageProvider:use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
class IgnoreDeadInterfaceUsageProvider extends ReflectionBasedMemberUsageProvider
{
public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData
{
if ($method->getDeclaringClass()->isInterface()) {
return VirtualUsageData::withNote('Interface method, kept for unification even when possibly unused');
}
return null;
}
}
parameters:
shipmonkDeadCode:
debug:
usagesOf:
- App\User\Entity\Address::__construct
Then, run PHPStan with -vvv CLI option and you will see the output like this:
App\User\Entity\Address::__construct
|
| Marked as alive by:
| entry virtual usage from ShipMonk\PHPStan\DeadCode\Provider\SymfonyUsageProvider
| 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
If you set up editorUrl parameter, you can click on the usages to open it in your IDE.
[!TIP] You can change the list of debug references without affecting result cache, so rerun is instant!
@api phpdoc, those will be considered entrypoints@api to mark all its methods as entrypointsNo error with identifier shipmonk.deadMethod is reported on line X false positives for every inline ignore (e.g. // @phpstan-ignore shipmonk.deadMethod) as those errors are no longer emittedparameters:
errorFormat: filterOutUnmatchedInlineIgnoresDuringPartialAnalysis
# optionally:
shipmonkDeadCode:
filterOutUnmatchedInlineIgnoresDuringPartialAnalysis:
wrappedErrorFormatter: table
composer checkcomposer fix:csDCD has built-in support only for core libraries:
symfony/*, illuminate/* and nette/*.For other libraries, write a custom usage provider or publish it as a PHPStan extension.
0.x β PHP 7.4 - 8.51.x β PHP 8.1+660 followers Β· starred Feb 2026
4,385 followers Β· starred Apr 2026
1,669 followers Β· starred May 2025
247 followers Β· starred Nov 2024
π PHP unused code detection via PHPStan extension. Detects dead cycles, supports libs like Laravel, Symfony, Twig, Doctrine, PHPUnit etc. Can automatically remove dead PHP code. Able to detect dead code used only in tests. Finds unused methods, constants, properties and enum cases.
PHP
529
349 commits
updated Oct 13, 2026
PHPStan extension to find unused PHP code in your project with ease!
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
$ vendor/bin/phpstan
[!NOTE] Make sure you analyse whole codebase (e.g. both
srcandtests) so that all usages are found.
Check out the recording and slides from TechMeetup Conference (2025) about this package:
All dead class member types are detected by default, you can disable some if needed:
parameters:
shipmonkDeadCode:
detect:
deadMethods: true
deadConstants: true
deadEnumCases: true
deadProperties:
neverRead: true
neverWritten: true
phpstan/phpstan-symfony with containerXmlPathshipmonkDeadCode.usageProviders.symfony.containerXmlPaths configured#[AsEventListener], #[AsMessageHandler], #[AsController], #[AsCommand]#[Assert\Callback], #[Interact], #[Route], #[Required] (methods and properties)#[AsSchedule], #[AsCronTask], #[AsPeriodicTask]#[AutoconfigureTag('doctrine.event_listener')], #[Autoconfigure(constructor:)], #[Autoconfigure(calls:)], #[AutowireCallable]defaultIndexMethod and defaultPriorityMethod in #[AutowireLocator], #[AutowireIterator], #[TaggedIterator], #[TaggedLocator]#[AsAnnounceListener], ...EventSubscriberInterface::getSubscribedEventsonKernelResponse, onKernelRequest, etc!php/const and !php/enum references in config yamls#[AsTwigComponent]/#[AsLiveComponent] (constructor, mount()), #[LiveProp], #[LiveAction], #[LiveListener], lifecycle hooks#[AsEntityListener], #[AsDoctrineListener] attributedoctrine.event_listener tags in the Symfony container XMLDoctrine\ORM\Events::* events, Doctrine\Common\EventSubscriber methodsrepositoryMethod in #[UniqueEntity] attribute#[PreFlush], #[PostLoad], ...#[Column(enumType: UserStatus::class)]testXxx methods@test, @before, @afterClass etc#[Test], #[Before], #[AfterClass] etcbenchXxx methods#[BeforeMethods], #[AfterMethods] attributes#[ParamProviders] attribute for param provider methods@Given, @When, @Then) or attributes (#[Given], #[When], #[Then])@BeforeScenario, @AfterScenario, etc.) or attributes (#[BeforeScenario], #[AfterScenario], etc.)@Transform or #[Transform]test* or #[TestRule]) of classes registered via the phpat.test service taghandleXxx, renderXxx, actionXxx, injectXxx, createComponentXxxSmartObject magic calls for @property annotationsshipmonkDeadCode.usageProviders.nette.containerNeonPaths is configured (requires nette/neon)test* methods, setUp/tearDown, @dataProvider methods in Tester\TestCase subclassesRoute::get/post/put/...(), resource(), apiResource() with callable, string (Controller@method), and invokable syntaxEvent::listen(), Event::subscribe(), auto-discovered listeners (handle*/__invoke with typed first param)Schedule::job()Gate::define(), Gate::policy(), $this->authorize() with automatic policy class resolution\Facades\... static calls mapped to the underlying classboot, booted, casts, newFactory, query scopes, relationships, attribute accessors (modern + legacy)Model::observe() + #[ObservedBy] attributedefinition, configure), seeders (run), migrations (up, down)$controller->render('my.twig', ['param' => $viewModel]),#[Template] controller methodsTwig\Environment::render() and similar#[AsTwigFilter], #[AsTwigFunction], #[AsTwigTest]new TwigFilter(..., callback), new TwigFunction(..., callback), new TwigTest(..., callback)All those libraries are autoenabled when found within your composer dependencies. If you want to force enable/disable some of them, you can:
parameters:
shipmonkDeadCode:
usageProviders:
phpunit:
enabled: true
ReflectionClass is detected as used
$reflection->getConstructor(), $reflection->getConstant('NAME'), $reflection->getMethods(), $reflection->getCases()...vendor is not reported as dead
Psr\Log\LoggerInterface::log is automatically considered usedIteratorAggregate::getIterator is automatically considered usedBackedEnum::from, BackedEnum::tryFrom and UnitEnum::casesstream_wrapper_registerscripts section of composer.json
"post-install-cmd": "MyVendor\\MyClass::postInstall"composer.json is autodetected, but you can point it elsewhere via shipmonkDeadCode.usageProviders.composer.composerJsonPathThose providers are enabled by default, but you can disable them if needed.
src is only used in teststests usage excluder:
parameters:
shipmonkDeadCode:
usageExcluders:
tests:
enabled: true
devPaths: # optional, autodetects from autoload-dev sections of composer.json when omitted; entries may contain * globs (like Composer classmap paths)
- %currentWorkingDirectory%/tests
With such setup, members used only in tests will be reported with corresponding message, e.g:
Unused AddressValidator::isValidPostalCode (all usages excluded by tests excluder)
[!TIP] We recommend enabling this excluder for all projects.
shipmonk.deadCode.memberUsageProvider and implement ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProviderservices:
-
class: App\ApiOutputUsageProvider
tags:
- shipmonk.deadCode.memberUsageProvider
ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider:
use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
class FuzzyTwigUsageProvider extends ReflectionBasedMemberUsageProvider
{
public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData
{
if ($method->getDeclaringClass()->implementsInterface(UsedInTwigMarkerInterface::class)) {
return VirtualUsageData::withNote('Probably used in twig');
}
return null;
}
}
MemberUsageProvider interface.User::__construct usage in following PHP snippet:function test(SerializerInterface $serializer): User {
return $serializer->deserialize('{"name": "John"}', User::class, 'json');
}
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
use Symfony\Component\Serializer\SerializerInterface;
class DeserializationUsageProvider implements MemberUsageProvider
{
public function __construct(
private UsageOriginDetector $originDetector,
) {}
/**
* @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 place it was called from (needed for proper transitive dead code elimination)
$usageOrigin = UsageOrigin::createRegular($node, $scope);
// record the hidden constructor call
$constructorRef = new ClassMethodRef($serializedClass->getValue(), '__construct', possibleDescendant: false);
return [new ClassMethodUsage($usageOrigin, $constructorRef)];
}
return [];
}
}
You can exclude any usage based on custom logic, just implement MemberUsageExcluder and register it with shipmonk.deadCode.memberUsageExcluder tag:
use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;
class MyUsageExcluder implements MemberUsageExcluder
{
public function shouldExclude(ClassMemberUsage $usage, Node $node, Scope $scope): bool
{
// ...
}
}
# phpstan.neon.dist
services:
-
class: App\MyUsageExcluder
tags:
- shipmonk.deadCode.memberUsageExcluder
The same interface is used for exclusion of test-only usages, see above.
[!NOTE] Excluders are called after providers.
------ ------------------------------------------------------------------------
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
------ ------------------------------------------------------------------------
phpstan.neon.dist:parameters:
shipmonkDeadCode:
reportTransitivelyDeadMethodAsSeparateError: true
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;
- }
}
editorUrl parameter) β’ Removed method UserFacade::deadMethod
! Excluded usage at tests/User/UserFacadeTest.php:241 left intact
$unknown->method()) by marking all methods named method as used
new $unknown() will mark all constructors as used$unknown::CONSTANT)phpstan.neon.dist by excluding such usages:parameters:
shipmonkDeadCode:
usageExcluders:
usageOverMixed:
enabled: true
-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
$class->$unknown()) by marking all possible methods as used$unknownClass->$unknownMethod()), we ignore such usage (as it would mark all methods in codebase as used) and show warning in debug verbosity (-vvv)ReflectionClass also emit unknown method calls:/** @var ReflectionClass<Foo> $reflection */
$methods = $reflection->getMethods(); // all Foo methods are used here
__get, __set etc) are never reported as dead
__construct, __cloneparameters:
ignoreErrors:
- '#^Unused .*?::__construct$#'
neverReaduse ReflectionProperty;
use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
class ApiOutputPropertyUsageProvider extends ReflectionBasedMemberUsageProvider
{
protected function shouldMarkPropertyAsRead(ReflectionProperty $property): ?VirtualUsageData
{
if ($property->getDeclaringClass()->implementsInterface(ApiOutput::class)) {
return VirtualUsageData::withNote('Used upon JSON serialization');
}
return null;
}
}
Controller return value, use AST-based provider
MemberUsageProvider:use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
class IgnoreDeadInterfaceUsageProvider extends ReflectionBasedMemberUsageProvider
{
public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData
{
if ($method->getDeclaringClass()->isInterface()) {
return VirtualUsageData::withNote('Interface method, kept for unification even when possibly unused');
}
return null;
}
}
parameters:
shipmonkDeadCode:
debug:
usagesOf:
- App\User\Entity\Address::__construct
Then, run PHPStan with -vvv CLI option and you will see the output like this:
App\User\Entity\Address::__construct
|
| Marked as alive by:
| entry virtual usage from ShipMonk\PHPStan\DeadCode\Provider\SymfonyUsageProvider
| 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
If you set up editorUrl parameter, you can click on the usages to open it in your IDE.
[!TIP] You can change the list of debug references without affecting result cache, so rerun is instant!
@api phpdoc, those will be considered entrypoints@api to mark all its methods as entrypointsNo error with identifier shipmonk.deadMethod is reported on line X false positives for every inline ignore (e.g. // @phpstan-ignore shipmonk.deadMethod) as those errors are no longer emittedparameters:
errorFormat: filterOutUnmatchedInlineIgnoresDuringPartialAnalysis
# optionally:
shipmonkDeadCode:
filterOutUnmatchedInlineIgnoresDuringPartialAnalysis:
wrappedErrorFormatter: table
composer checkcomposer fix:csDCD has built-in support only for core libraries:
symfony/*, illuminate/* and nette/*.For other libraries, write a custom usage provider or publish it as a PHPStan extension.
0.x β PHP 7.4 - 8.51.x β PHP 8.1+660 followers Β· starred Feb 2026
4,385 followers Β· starred Apr 2026
1,669 followers Β· starred May 2025
247 followers Β· starred Nov 2024