shipmonk/dead-code-detector
PHPStan extension that detects and helps remove unused PHP code. Finds dead methods/properties/constants/enum cases, dead cycles and transitive dead members, even dead tested code. Supports popular frameworks like Symfony and is configurable via usage providers.
## Getting Started
### Minimal Setup
1. **Install the package** in your Laravel project:
```bash
composer require --dev shipmonk/dead-code-detector
phpstan.neon:
includes:
- vendor/shipmonk/dead-code-detector/rules.neon
tests):
vendor/bin/phpstan analyse --level 8
Run a one-time cleanup to identify dead code in your Laravel application:
vendor/bin/phpstan analyse --error-format removeDeadCode
This will:
array_column() string keys (now correctly treated as property reads)Pre-commit Check Add a Git hook or CI step to run:
vendor/bin/phpstan analyse --error-format=json > dead-code-report.json
json output for programmatic checks (e.g., fail builds if dead code exists).Laravel-Specific Patterns
routes/web.php (e.g., unused Route::get('/old', [Controller::class, 'deadMethod'])).App\Http\Controllers\*.App\Services\* called only via dependency injection.#[AsEventListener] classes (properly detected).Test Integration Enable test usage exclusion to avoid false positives:
parameters:
shipmonkDeadCode:
usageExcluders:
tests:
enabled: true
devPaths:
- %currentWorkingDirectory%/tests
Automated Cleanup Schedule a weekly cleanup in CI:
vendor/bin/phpstan analyse --error-format=removeDeadCode --generate-report=dead-code.md
dead-code.md before merging.Laravel Service Providers:
Dead code in register()/boot() methods (e.g., unused bindings or listeners).
// Example: Unused binding
$this->app->bind('unused.service', UnusedService::class); // Flagged if never resolved
Eloquent Observers:
Dead observed() methods or unused event handlers.
// Example: Unused observer
User::observe(UnusedObserver::class); // Flagged if `handle*` methods are dead
Console Commands:
Unused methods in handle() or dead command classes.
php artisan list // Cross-check with dead-code report
Middleware:
Dead handle() methods in App\Http\Middleware\*.
Policies/Gates:
Unused before()/after() methods or dead policy classes.
Symfony Event Listeners:
Dead constructors in classes annotated with #[AsEventListener] are now properly detected as used.
array_column() Fix:
No longer flags array_column($array, 'dynamicKey') as dead property access when 'dynamicKey' is a string literal. This resolves false positives in collection processing.
False Positives in Dynamic Code
call_user_func() or ReflectionMethod may still be flagged as dead.ReflectionBasedMemberUsageProvider to whitelist dynamic calls:
class DynamicCallProvider extends ReflectionBasedMemberUsageProvider {
public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData {
if ($method->getName() === 'dynamicMethod') {
return VirtualUsageData::withNote('Called dynamically');
}
return null;
}
}
Test Exclusion Overkill
tests excluder may hide intentional test-only usages (e.g., private methods tested via reflection).MemberUsageExcluder to fine-tune exclusions:
class ReflectionExcluder implements MemberUsageExcluder {
public function shouldExclude(ClassMemberUsage $usage, Node $node, Scope $scope): bool {
return $scope->getFile()->getRelativeFilePath()->contains('tests/ReflectionTest.php');
}
}
Transitive Dead Code Overload
parameters:
shipmonkDeadCode:
reportTransitivelyDeadMethodAsSeparateError: false
Laravel Facades
Route::get()) may not be detected if called via dynamic strings.phpstan/phpstan-symfony is installed and configured:
includes:
- vendor/phpstan/phpstan-symfony/extension.neon
Enum Cases
tryFrom() may miss usages.parameters:
shipmonkDeadCode:
detect:
deadEnumCases: true
array_column() String Keys
array_column($array, 'key') as dead property access. This resolves false positives in collection processing (e.g., array_column($users, 'email')).Inspect False Positives
Use --debug to see why a method was marked dead:
vendor/bin/phpstan analyse --debug --error-format=removeDeadCode
Isolate Analysis Run PHPStan on a single file to debug:
vendor/bin/phpstan analyse src/Http/Controllers/UserController.php
Check Usage Providers
Verify enabled providers in phpstan.neon:
parameters:
shipmonkDeadCode:
usageProviders:
laravel:
enabled: true
symfony:
enabled: true
IDE Integration
Configure editorUrl in phpstan.neon for clickable links:
output:
editorUrl: 'vscode://file/{file}:{line}'
Custom Usage Providers
Add support for Laravel-specific magic (e.g., app()->make() calls):
class LaravelContainerProvider implements MemberUsageProvider {
public function getUsages(Node $node, Scope $scope): array {
if ($node instanceof MethodCall && $node->var->var->name === 'app') {
$service = $scope->getType($node->getArgs()[0]->value)->getClassName();
return [new ClassMethodUsage(..., new ClassMethodRef($service, '__construct'))];
}
return [];
}
}
Excluders for Legacy Code
Whitelist legacy methods called via call_user_func_array():
class LegacyCallExcluder implements MemberUsageExcluder {
public function shouldExclude(ClassMemberUsage $usage, Node $node, Scope $scope): bool {
return $node instanceof MethodCall &&
$node->name->toString() === 'call_user_func_array' &&
$usage->getMemberRef()->getClassName() === 'LegacyService';
}
}
Post-Processing Scripts
Use the removeDeadCode output to generate a PR template:
vendor/bin/phpstan analyse --error-format=removeDeadCode | grep "• Removed" > DEAD_CODE_REMOVALS.md
Route Cache
php artisan route:clear
Service Container
AppServiceProvider may not be detected if resolved via app()->bind() without a class.app()->singleton() or explicit bind() with a class.Artisan Commands
php artisan list until cached.composer dump-autoload after cleanupHow can I help you explore Laravel packages today?