shipmonk/dead-code-detector
PHPStan extension that finds unused PHP code: dead methods, constants, enum cases, properties, dead cycles, and transitively dead members. Detects dead tested code and can automatically remove unused code. Supports Symfony and other libraries with customizable usage providers.
Architecture fit
shipmonk/dead-code-detector is not a runtime library — it is a PHPStan rule extension that adds a whole-program reachability analysis over your AST. Architecturally, that means it slots into the layer you already own for static analysis rather than into the application itself. For a Laravel codebase this is a genuinely good fit because Laravel is the archetypal "dead code looks dead but isn't" framework: controllers, jobs, listeners, commands, policies, middleware, form requests, mailables, notifications, and Eloquent accessors/scopes are almost all invoked by the framework via reflection, container resolution, or string references rather than by direct call sites. A naive grep-based dead-code pass on Laravel is worthless. This package's differentiating value is that it ships framework-aware usage providers — the README explicitly enumerates Laravel (Route::get/post/resource/apiResource, Event::listen/subscribe, auto-discovered listeners, Schedule::job, Gate::define/policy, real-time facades, console commands, jobs, providers, middleware, notifications, form requests, mailables, broadcast events, JSON resources) and Eloquent (model constructor, boot/booted, casts, scopes, relationships, accessors, observers, factories, seeders, migrations). That list is the product. Everything else — transitive dead cycles, tested-code detection, auto-removal — is mechanism.
The second architectural fit is customizability. The MemberUsageProvider / MemberUsageExcluder interfaces let you teach the analyser about in-house magic (custom discovery attributes, dynamic container bindings, domain-specific serializers). That converts the tool from "buy or don't buy" into "adopt plus a bounded extension surface," which is the shape a TPM wants.
The counterweight: this is a static-analysis tool whose correctness is defined by how completely it enumerates framework magic. Its accuracy on your Laravel app is a function of which Laravel features and which third-party packages you use. The README's Laravel coverage list is impressive but not exhaustive — it does not claim Route::controller(), Route::inertia(), Route::livewire(), Filament/Nova resource discovery, Livewire components, Inertia props, Horizon/Spark internals, or package-registered macros. Any of those in your stack is a false-positive generator until you write a provider.
Integration feasibility
Low friction to try, moderate effort to trust.
composer require --dev shipmonk/dead-code-detector plus a single includes: entry in phpstan.neon.dist. No runtime dependency, no service provider, no config beyond opt-in toggles.tests usage excluder is the single most important configuration decision and the README recommends it explicitly. Without it, a class defined in src/ that is only exercised by tests is reported as used, which silently defeats the purpose of the tool. With it, you get the high-value report: "this production method exists solely because a test touches it."--error-format removeDeadCode) is a real capability but is a process risk, not a technical one. It rewrites source files. That must never run unattended.Technical risk
| Risk | Severity | Notes |
|---|---|---|
| False positives → deletion of live code | High | The only irreversible failure mode. Magic-heavy Laravel features or third-party packages outside the provider list will be misreported. Auto-removal turns a warning into a data-loss event. |
| Provider drift on framework/package upgrades | Medium–High | New Laravel features and new packages introduce new invisible call paths. This is a recurring cost, not a one-time one. |
| Custom provider authoring complexity | Medium | AST-based providers require PhpParser node handling, Scope type resolution, and correct UsageOrigin construction. This is specialist work, not a junior task. |
| CI cost | Medium | Whole-program reachability adds a full graph pass. On a large Laravel monolith this can be minutes, and memory pressure on PHPStan is a known operational tax. |
| Supply-chain / sustainability | Low–Medium | 534 stars but 0 dependents in the data provided, single-vendor origin (ShipMonk R&D), MIT, no commercial support. Blast radius is CI, not production, and it is a pinned dev dependency — but there is no vendor SLA. |
| PHPStan version coupling | Medium | As a PHPStan extension it is tightly bound to PHPStan internals. A major PHPStan upgrade can block your PHPStan upgrade. |
Key questions
Route/container patterns.Stack fit
Strong fit for a PHP 8.1+ Laravel application already running PHPStan. The package auto-enables its Laravel, Eloquent, and (where relevant) PHPUnit, PhpBench, Behat, Twig, Doctrine, Symfony, Nette, PHPStan, and PHPat providers based on composer.json contents — so a standard Laravel app gets Laravel + Eloquent + PHPUnit coverage with zero provider configuration. That is the fastest path to a first signal.
Where it fits less well:
@livewire-style references are likely uncovered.Migration path
There is no migration — it is net-new static analysis. The correct framing to stakeholders is "we are adding a linting capability," not "we are changing architecture." The only real migration is cultural: teams must learn to treat dead-code reports as actionable rather than baseline-able.
Compatibility
src and tests paths, as the README insists. A partial scan produces mass false positives.phpstan/extension-installer is present, or wire rules.neon manually.tests excluder's devPaths — it autodetects from autoload-dev, but monorepos and non-standard layouts need explicit paths.Route::controller(), grouped controllers, Route::resource with only/except, invokable controllers, Event::listen with string class names, queued listener discovery, and any Gate::before/Gate::after superadmin bypass.Sequencing
detect for methods, constants, enum cases, and properties. Enable the tests usage excluder. Generate a baseline so CI stays green while you triage.MemberUsageProvider, (c) intentional/edge → MemberUsageExcluder with a documented reason. Each custom provider needs a test, per the project's own contributor guidance.Do not reorder steps 1–4. The temptation is to go straight to repo-wide enforcement; that is how static-analysis rollouts get reverted.
Maintenance
The steady-state maintenance surface is threefold:
Support
MIT, community-maintained, no vendor SLA, single-organisation origin. The README is unusually good (worked examples, provider interfaces, slides/recording from a 2025 conference talk), which materially reduces ramp cost. But there is no escalation path: a false positive caused by a new Laravel release is fixed by you or awaited upstream. For a dev-only tool with CI-only blast radius, that is an acceptable posture — provided the decision is made explicitly rather than discovered during an incident.
Scaling
phpstan.neon and its own exclusion set; findings can cross app boundaries and produce noise.Failure modes
Ramp-up
rules.neon, run report-only. Any PHP-fluent engineer can do this.detect.*, usageProviders.*, usageExcluders.tests), interpret the transitive-dead tip output, learn to distinguish "dead" from "invisibly called."Scope, and the package's ClassMethodRef/UsageOrigin graph model. This is a senior-engineer task, ~1–2 weeks to first working provider, and it is the capability that determines long-term value.TPM recommendation: treat this as a high-opportunity, medium-confidence bet — consistent with the 65.84 opportunity score against a 39.99 overall score and thin adoption signal (0 dependents). Fund a time-boxed, one-sprint spike on a bounded module with a hard gate on measured false-positive rate. If false positives land under roughly 10% after the tests excluder is enabled, proceed to repo-wide report-only plus a named owner for custom providers. If they land higher, the gap is almost certainly unmodelled framework magic, and the honest question is whether you have the senior capacity to close it — because without that capacity, the tool will be baselined and ignored, which is worse than not adopting it.
How can I help you explore Laravel packages today?