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.
Build vs. buy: buy. A Laravel-aware cross-reference analyzer that understands container resolution, route registration, event discovery, policy auto-resolution, Eloquent lifecycle hooks, and real-time facades is not a weekend script — it's a continuously maintained model of framework internals that changes with every Laravel minor. This package already encodes that knowledge (routes, events, schedules, gates/policies, commands, jobs, providers, middleware, notifications, form requests, mailables, broadcast events, JSON resources, observers, factories, seeders, migrations, plus Reflection/vendor/builtin/Composer providers). Building an equivalent in-house is a multi-quarter side quest with no product differentiation. MIT license removes procurement friction.
Roadmap: feature sunsetting as a verifiable exit criterion. Most "remove feature X" tickets stall because nobody can prove the code is truly unreachable. This tool converts a judgement call into a CI signal. When a flag is retired, an experiment concludes, or a legacy integration is decommissioned, the dead-member report is the evidence that the removal is complete — including transitively dead members (a dead facade method reveals the entity setters and constants it was the only caller of), which is exactly the tail of a sunset that teams normally miss.
Roadmap: refactor and modernization scoping. Dead cycles and transitive-dead reporting expose self-sustaining islands of abandoned code — clusters kept "alive" only by referencing each other. For a Laravel upgrade, a modularization effort, or a move to a new architecture, this is a cheap way to shrink the blast radius before you start, rather than discovering the dead weight mid-migration.
Quality strategy: coverage is a bad proxy for relevance. The differentiator is "dead tested code" detection with the tests usage excluder, which flags production classes whose only consumers are tests. That's not a lint error — it's a design smell and a roadmap input: either the code should be deleted, or the tests are coupled to internals in a way that will make every future refactor expensive. Very few tools make this visible, and it directly informs decisions about test strategy and internal API boundaries.
Engineering investment: consolidating static analysis into one gate. Because this is a PHPStan extension, it rides an existing, already-funded pipeline. No new tool, no new CI service, no new credential, no new dashboard. The adoption cost is one dev dependency, one includes: line, and a parameters block — which is the sort of decision a PM can approve on a sprint boundary rather than a quarter boundary.
Hygiene program with a measurable burn-down. Enable in report-only mode, capture the count of dead members, and treat it as a tracked metric alongside other tech-debt KPIs (LOC removed, PHPStan baseline entries retired, files deleted). This gives leadership a defensible trend line rather than an anecdote about the codebase "feeling heavy."
Onboarding and maintainability cost model. Dead code inflates the surface a new hire must read, review, and grep through. Reducing it is one of the cheapest levers on time-to-first-PR, and the removeDeadCode output format makes the work mechanical and delegatable to a junior or an agent rather than requiring senior judgement per case.
Due diligence / inherited codebases. For an acquisition, an inherited team, or a contractor handoff, running this once gives a fast, objective read on how much of the codebase is load-bearing. It's a much better signal than LOC or test coverage percentage.
What I would not use it for: as an autopilot cleanup bot. Automatic removal is a real capability, but the value it creates is capped and the downside (deleting a live code path in a dynamically dispatched corner) is not. Treat it as a review-accelerator, not an unattended janitor.
Adopt when most of these are true:
src and tests). Partial-path analysis is the single biggest source of false positives, and false positives are the whole risk of this tool.MemberUsageProvider — the extension points are genuinely good, which is the main reason this is viable versus a black box.Look elsewhere, or defer, when:
call_user_func by convention, code generated at runtime, or a plugin system that resolves classes from database rows. The generic providers (Reflection, vendor, builtin, enum, Composer scripts) cover more than you'd expect, but you will be writing and maintaining providers, and every false negative is a deleted feature.composer-unused is the right tool. This operates at member granularity.--dev), and accepting that the worst case is falling back to a fork or removing it, not a production outage.Adoption sequencing I'd recommend: one sprint of report-only on a single bounded module or a legacy subsystem, not the whole monolith. Measure false-positive rate there. If it's low, expand; if it's high, you've learned the shape of the custom providers you need before you've burned credibility with a noisy repo-wide run.
One-liner: a free, MIT-licensed PHPStan extension that finds and — with review — removes unused PHP code, and that already understands Laravel's magic well enough to be trusted on our codebase.
For executives: we spend real engineering hours reading, reviewing, and grepping code that nothing calls. This tool finds that code automatically inside the static analysis we already run, and produces a diff we can review like any other PR. There is no license cost, no new vendor, no production runtime dependency — it's a dev-only package and the exit cost is one line deleted from composer.json. The measurable outcomes are lines of code removed, files deleted, and a declining count of known-unused members over time. Secondary benefit: it flags production code that only tests use, which is a leading indicator of refactoring cost.
For engineering leadership: the integration is small — one dev dependency, one includes: in phpstan.neon.dist, one parameters block. It respects our opt-outs (you can disable methods/constants/enum cases/properties individually), supports Symfony, Doctrine, PHPUnit, PhpBench, Behat, PHPStan, PHPat, Nette, Laravel, Eloquent and Twig out of the box and auto-enables them based on composer.json, and exposes two clean extension interfaces (MemberUsageProvider, MemberUsageExcluder) for our in-house magic. The tests excluder is the feature I'd push hardest: it separates "used" from "used only by our tests," which is a much sharper signal than anything we have today.
For the engineers doing the work: it plugs into the PHPStan run you already have. It distinguishes regular AST usages, vendor-overridden members, and genuinely magic calls, so the noise floor is lower than a naive grep-based tool. It reports transitively dead members as tips under the root cause rather than as hundreds of separate errors, and --error-format removeDeadCode produces an actual diff. The one rule I'll set: we adopt in report-only mode first, we triage every reported member before deleting anything, and auto-removal lands only as a reviewed PR — never as an unattended CI job. The value here is a shorter review, not a robot with delete access.
The honest caveats to state up front: it's static analysis, not runtime truth — it proves unreachability, not lack of traffic. It needs whole-codebase analysis to be accurate, which costs CI time. It will not know our bespoke dynamic dispatch on day one, so we should plan for one custom provider. And at ~500 stars with no downstream package dependents, we are early adopters — which is fine for a pinned, dev-only tool, but should be a conscious choice rather than a surprise.
Ask: one sprint, one bounded legacy module, report-only, with a single named owner. Success criteria: false-positive rate low enough to justify repo-wide rollout, and a triaged removal PR merged.
How can I help you explore Laravel packages today?