Library / SDK
shipmonk-rnd/dead-code-detector avatar
shipmonk-rnd/dead-code-detector

dead-code-detector: finding unused PHP code with PHPStan

💀 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.

530 stars33 forksPHPMIT

At a glance

What is it?
This PHPStan extension looks for PHP that nothing uses, and its real work is not the analysis but the knowledge of how frameworks call your code. A Symfony service constructed by the container, a PHPUnit data provider, a Laravel route pointing at a controller method, a Twig view object passed into a template: each of those is a usage that a naive analyser never sees, and each of them is here.
Who is it for?
dead-code-detector suits a PHP team that has accumulated dead code and wants the detection to be precise rather than approximate, since a tool that reports false positives on framework-wired code gets switched off within a week. It does not suit a project where usages are hidden behind things the providers do not model, such as a custom framework magic or a code generator, and it does not suit a codebase where the removal is too risky to automate.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 11 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A PHPStan extension with framework knowledge

dead-code-detector is a PHPStan extension to find unused PHP code in your project with ease, and it is MIT licensed and written in PHP.

The summary in the README is the clearest statement of what it does: it is a PHPStan extension, it detects dead cycles, it does transitive dead member detection, it detects dead tested code, it can automatically remove unused code, it supports popular libraries, and its usage providers are customizable.

The two phrases that matter are dead cycles and transitive. Dead cycles are classes that reference each other so that nothing in the group is ever reachable from outside, a shape that a per-member analysis misses because every member does have a reference. Transitive detection is the same idea applied downwards: a method used only by a dead method is itself dead, so the report has to follow the reference graph rather than counting references.

That graph is where the framework knowledge comes in. A Symfony service constructed by the dependency injection container has no call site in your source. A PHPUnit data provider is referenced by name in an attribute. A Laravel route points at a controller method with a string. A Twig template receives a view object as a parameter. Each of those is a real usage, and a tool that only understands direct references will report all of them as dead, which is the fastest way to make a team turn the tool off.

The releases are 1.4.0 on 2026-08-28, 1.4.1 on 2026-09-10 and 1.4.2 on 2026-09-21, with the last push to the repository on 2026-10-13. The topic tags are dead, dead-code-removal, php, phpstan, phpstan-extension, phpstan-rules, static-analysis and unused-code.

Installation, and the instruction that decides whether it works

The installation is a development dependency:

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

Then either use the official extension-installer, or load the rules yourself in the PHPStan configuration:

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

And run PHPStan as usual:

bash
$ vendor/bin/phpstan

Then there is the note, and it is the one that decides whether the tool is useful or noise. Make sure you analyse the whole codebase, for example both src and tests, so that all usages are found.

That single requirement explains the dead tested code feature. A method used only by a test is genuinely dead in production, but it is not dead in your codebase, and a tool that analysed only src would report every test helper and every factory as unused. Analysing the tests makes those usages visible, and the separate detection for code used only in tests then tells you which members are reachable from tests but not from production code.

So the tool has two questions to answer: is this referenced anywhere, and is it referenced from production code. The first needs the whole codebase, and the second is what the dead tested code detection reports.

Which members are reported, and how to narrow it

All dead class member types are detected by default, and you can disable some if needed. The configuration lives under the shipmonkDeadCode parameter, and the detect section has a switch for each member kind.

neon
parameters:
    shipmonkDeadCode:
        detect:
            deadMethods: true
            deadConstants: true
            deadEnumCases: true
            deadProperties:
                neverRead: true
                neverWritten: true

The split for properties is the one worth understanding. A property that is never read and a property that is never written are different bugs. A property that is written but never read is dead weight. A property that is read but never written is worse than dead weight, because the code reading it gets a value it can only have obtained by accident, usually a default.

The constants and enum cases are the ones that surprise people. A constant or an enum case referenced from a configuration file, a YAML service definition or a database value is used, and the generic providers exist partly to cover those cases.

The same structure holds for the library support. Under usageProviders you can force a provider on or off, with the example given for phpunit. The README says all the supported libraries are autoenabled when found within your composer dependencies, so in a normal project you should not have to configure anything, and the switches exist for the case where the detection is wrong or where you want to narrow the report.

The framework providers, which are the real content

The supported library list is long, and it is the substance of the project. Each entry corresponds to a way a framework calls your code that no direct reference would reveal.

Symfony is the biggest one. Calls made by the dependency injection container over your services, including constructors, calls and factory methods, are detected if you have the phpstan-symfony extension with a containerXmlPath, or if you configure shipmonkDeadCode.usageProviders.symfony.containerXmlPaths. Beyond that the provider knows the attributes: AsEventListener, AsMessageHandler, AsController, AsCommand, the Assert Callback, Interact, Route and Required attributes on methods and properties, the scheduling attributes AsSchedule, AsCronTask and AsPeriodicTask, the Autoconfigure family including AutoconfigureTag, AutowireCallable, the default index and priority methods of the autowire and tagged locator attributes, and the workflow event listener attributes. It also covers the EventSubscriberInterface method, the kernel request and response hooks, the PHP constant and enum references in config YAML files, and the Symfony UX components with their constructor, mount, LiveProp, LiveAction and LiveListener attributes.

Doctrine covers the entity listener attributes, the event listener tags in the Symfony container XML, the ORM and Common event subscriber methods, the repositoryMethod in the UniqueEntity attribute, the lifecycle event attributes, and enums used in a Column attribute.

PHPUnit covers the data provider methods, the testXxx methods, the annotations for test, before and afterClass, and the matching attributes. PhpBench covers the benchXxx methods and the before and after methods attributes plus the param providers. Behat covers the context class constructors, the step definitions by annotation and by attribute, the scenario hooks and the transformations. PHPStan itself is covered for constructor calls of its own services, and PHPat for the architecture test methods.

Nette covers the handleXxx, renderXxx, actionXxx, injectXxx and createComponentXxx magic methods, the SmartObject magic calls for property annotations, and the constructors of classes the container instantiates when the neon paths are configured. Nette Tester covers the test methods and the setup and teardown methods.

Laravel covers route registration in all its forms, event listeners through the Event facade and the auto-discovered listeners, scheduled jobs, the gates and policies including the automatic policy class resolution, the real-time facades, console commands, jobs, service providers, middleware, notifications, form requests, mailables, broadcast events and JSON resources.

Eloquent covers the model methods including the constructor, boot, booted, casts, newFactory, query scopes, relationships and accessors, the observers with the observe call and the ObservedBy attribute, the factories and seeders, and the migrations.

And Twig covers the view objects passed as parameters to templates, including transitively referenced ones, whether they are passed to a controller render call, returned from a controller method or rendered through the environment, along with the AsTwigFilter, AsTwigFunction and AsTwigTest attributes.

The generic providers and the removal step

The generic usage providers cover the cases where the usage is invisible to the type system, and they are the ones you will extend for your own code.

Reflection covers anything accessed through a ReflectionClass: a property, enum, constant or method reached by getConstructor, getConstant, getMethods or getCases. The reasoning is that a reflective access is a usage by definition, and reporting it as dead is a false positive rather than a finding.

Vendor covers overridden methods that originate in vendor code, so that implementing an interface from a library is not reported. Builtin covers overridden methods that come from PHP core or an extension, so implementing IteratorAggregate::getIterator is not reported.

Enum covers the usages caused by BackedEnum::from, BackedEnum::tryFrom and UnitEnum::cases. StreamWrapper covers the usages caused by stream_wrapper_register. Composer covers static methods referenced as callbacks in the scripts section.

The last of these is the pattern to copy when the tool reports a false positive in your own project: a usage provider that recognises the convention your project uses.

The automatic removal is the feature that needs the most care, because it edits your source. The repository carries the pieces that support it: a coverage-guard.php script, a composer-dependency-analyser.php script, a collision-detector.json, a cache/ directory, phpcs.xml.dist for the coding standard, phpstan.neon.dist for the analysis configuration and rules.neon for the rules themselves, alongside the src/ and tests/ directories and an AGENTS.md and CLAUDE.md for automated contributors.

The removal is safe only to the extent that the detection is complete. If a usage was missed, removing the member breaks the code at run time rather than at analysis time, which is why the analysis has to cover the tests and why the framework providers exist.

Where the dead code detector is the wrong tool

There are clear cases where this kind of tool is the wrong answer, and the first one is about how the code is used rather than whether it is used.

Dynamic usages that no provider models. A framework of your own with its own magic, a code generator that emits calls, a configuration file that names a class, an event dispatcher that resolves handlers from a string. Each of those is a real usage that the tool cannot see, and the consequence is a report full of things that are not dead. The fix is to write a usage provider for the convention, which is real work, and for a codebase with several such conventions it may be more work than the dead code is worth.

The second limit is the removal. Removing a member that is only called through a mechanism the tool did not understand does not produce an error, it produces a runtime failure somewhere else. The detection is only as safe as the coverage of the providers, and the coverage is the part you have to verify for your own stack.

The third is the scale of the payoff. Dead code in a young codebase is often deliberate, a method kept for a planned feature or a public API for consumers outside the repository, and the tool cannot tell the difference between a method kept on purpose and one forgotten. Deleting a public method from a library breaks its users, and the analyser will report it as unused if the users are in another repository.

The alternative approach is runtime coverage, which answers a different question. A coverage report tells you what a particular test run did not execute, and it is excellent for finding code that no test touches. The difference in approach is that coverage is empirical and this tool is static: coverage misses code that is never executed in this run but is still referenced, and this tool finds code that is referenced by nothing at all. A project that already has good coverage will get most of the same benefit from the coverage report, and a project without it will get more from this tool.

Editorial conclusion

dead-code-detector suits a PHP team that has accumulated dead code and wants the detection to be precise rather than approximate, since a tool that reports false positives on framework-wired code gets switched off within a week. It does not suit a project where usages are hidden behind things the providers do not model, such as a custom framework magic or a code generator, and it does not suit a codebase where the removal is too risky to automate. Check three things before you turn it on in CI: that your analysis covers the tests as well as the source, because the README is explicit that you should analyse the whole codebase so all usages are found, that the framework providers are enabled for the libraries you actually use, since they are autoenabled when found in your composer dependencies, and whether the dead code detection for code used only in tests is switched on, since that is a separate setting from the rest. The last push to the repository was on 2026-10-13 and the newest release is 1.4.2.

Frequently asked questions

What does the shipmonk dead-code-detector extension do?

It is a PHPStan extension that finds unused PHP code in a project. It detects dead methods, constants, enum cases and properties, finds dead cycles and transitively dead members, reports code used only by tests, and can remove the unused code automatically.

How do I install the dead-code-detector extension?

With composer require --dev shipmonk/dead-code-detector, then use the official PHPStan extension installer or include vendor/shipmonk/dead-code-detector/rules.neon in your phpstan.neon.dist, and run vendor/bin/phpstan.

Why does the dead-code-detector need to analyse my tests?

Because a method used only by a test would otherwise look unused. The README asks you to analyse the whole codebase, for example both src and tests, so that all usages are found, and the separate dead tested code detection then reports members reachable from tests but not from production code.

Which frameworks does the dead-code-detector understand?

Symfony, Doctrine, PHPUnit, PhpBench, Behat, PHPStan, PHPat, Nette, Nette Tester, Laravel, Eloquent and Twig, each with its own usage provider recognising the attributes, tags and magic methods that framework uses. All the providers are autoenabled when the library is found in your composer dependencies, and can be forced on or off in the usageProviders configuration.

What do the generic usage providers in the dead-code-detector cover?

Reflection accesses through ReflectionClass, overridden methods that originate in vendor code or in PHP core, enum usages through BackedEnum::from, BackedEnum::tryFrom and UnitEnum::cases, stream wrapper registrations, and static methods referenced as callbacks in the composer scripts section.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. shipmonk-rnd/dead-code-detector on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/shipmonk-rnd-dead-code-detector.svg)](https://hysenlabs.com/projects/shipmonk-rnd-dead-code-detector)

Was this useful?