{"id":28391421,"url":"https://github.com/shipmonk-rnd/dead-code-detector","last_synced_at":"2025-06-25T21:31:22.301Z","repository":{"id":213683573,"uuid":"734674518","full_name":"shipmonk-rnd/dead-code-detector","owner":"shipmonk-rnd","description":"💀 PHP unused code detection via PHPStan extension. Detects dead cycles, supports libs like Symfony, Doctrine, PHPUnit etc. Can automatically remove dead PHP code. Able to detect dead code used only in tests.","archived":false,"fork":false,"pushed_at":"2025-06-11T13:33:42.000Z","size":638,"stargazers_count":262,"open_issues_count":10,"forks_count":11,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-06-11T14:46:51.296Z","etag":null,"topics":["dead","dead-code-removal","php","phpstan","phpstan-extension","phpstan-rules","static-analysis","unused-code"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shipmonk-rnd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-12-22T09:56:33.000Z","updated_at":"2025-06-11T14:13:17.000Z","dependencies_parsed_at":"2023-12-22T12:08:22.994Z","dependency_job_id":"0cb01451-de8c-475d-adf2-162a9e71221f","html_url":"https://github.com/shipmonk-rnd/dead-code-detector","commit_stats":null,"previous_names":["shipmonk-rnd/dead-code-detector"],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/shipmonk-rnd/dead-code-detector","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shipmonk-rnd%2Fdead-code-detector","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shipmonk-rnd%2Fdead-code-detector/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shipmonk-rnd%2Fdead-code-detector/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shipmonk-rnd%2Fdead-code-detector/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shipmonk-rnd","download_url":"https://codeload.github.com/shipmonk-rnd/dead-code-detector/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shipmonk-rnd%2Fdead-code-detector/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261955964,"owners_count":23235986,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dead","dead-code-removal","php","phpstan","phpstan-extension","phpstan-rules","static-analysis","unused-code"],"created_at":"2025-05-31T09:12:50.719Z","updated_at":"2025-06-25T21:31:22.287Z","avatar_url":"https://github.com/shipmonk-rnd.png","language":"PHP","funding_links":[],"categories":["Table of Contents","PHP"],"sub_categories":["Static Analysis"],"readme":"# Dead code detector for PHP\n\n[PHPStan](https://phpstan.org/) extension to find unused PHP code in your project with ease!\n\n## Summary:\n\n- ✅ **PHPStan** extension\n- ♻️ **Dead cycles** detection\n- 🔗 **Transitive dead** member detection\n- 🧪 **Dead tested code** detection\n- 🧹 **Automatic removal** of unused code\n- 📚 **Popular libraries** support\n- ✨ **Customizable** usage providers\n\n## Installation:\n\n```sh\ncomposer require --dev shipmonk/dead-code-detector\n```\n\nUse [official extension-installer](https://phpstan.org/user-guide/extension-library#installing-extensions) or just load the rules:\n\n```neon\n# phpstan.neon.dist\nincludes:\n    - vendor/shipmonk/dead-code-detector/rules.neon\n```\n\n## Usage:\n\n```sh\n$ vendor/bin/phpstan\n```\n\n\u003e [!NOTE]\n\u003e Make sure you analyse whole codebase (e.g. both `src` and `tests`) so that all usages are found.\n\n## Supported libraries:\n\n#### Symfony:\n- **Calls made by DIC over your services!**\n   - constructors, calls, factory methods\n   - [`phpstan/phpstan-symfony`](https://github.com/phpstan/phpstan-symfony) with `containerXmlPath` must be used\n- `#[AsEventListener]` attribute\n- `#[AsController]` attribute\n- `#[AsCommand]` attribute\n- `#[Required]` attribute\n- `#[Route]` attributes\n- `#[Assert\\Callback]` attributes\n- `EventSubscriberInterface::getSubscribedEvents`\n- `onKernelResponse`, `onKernelRequest`, etc\n- `!php const` references in `config` yamls\n- `defaultIndexMethod` in `#[AutowireLocator]` and `#[AutowireIterator]`\n\n#### Doctrine:\n- `#[AsEntityListener]` attribute\n- `Doctrine\\ORM\\Events::*` events\n- `Doctrine\\Common\\EventSubscriber` methods\n- `repositoryMethod` in `#[UniqueEntity]` attribute\n- lifecycle event attributes `#[PreFlush]`, `#[PostLoad]`, ...\n\n#### PHPUnit:\n- **data provider methods**\n- `testXxx` methods\n- annotations like `@test`, `@before`, `@afterClass` etc\n- attributes like `#[Test]`, `#[Before]`, `#[AfterClass]` etc\n\n#### PHPStan:\n- constructor calls for DIC services (rules, extensions, ...)\n\n#### Nette:\n- `handleXxx`, `renderXxx`, `actionXxx`, `injectXxx`, `createComponentXxx`\n- `SmartObject` magic calls for `@property` annotations\n\n#### Twig:\n- `#[AsTwigFilter]`, `#[AsTwigFunction]`, `#[AsTwigTest]`\n- `new TwigFilter(..., callback)`, `new TwigFunction(..., callback)`, `new TwigTest(..., callback)`\n\nAll those libraries are autoenabled when found within your composer dependencies.\nIf you want to force enable/disable some of them, you can:\n\n```neon\nparameters:\n    shipmonkDeadCode:\n        usageProviders:\n            phpunit:\n                enabled: true\n```\n\n## Generic usage providers:\n\n#### Reflection:\n- Any constant or method accessed via `ReflectionClass` is detected as used\n  - e.g. `$reflection-\u003egetConstructor()`, `$reflection-\u003egetConstant('NAME')`, `$reflection-\u003egetMethods()`, ...\n\n#### Vendor:\n- Any overridden method that originates in `vendor` is not reported as dead\n  - e.g. implementing `Psr\\Log\\LoggerInterface::log` is automatically considered used\n\nThose providers are enabled by default, but you can disable them if needed.\n\n## Excluding usages in tests:\n- By default, all usages within scanned paths can mark members as used\n- But that might not be desirable if class declared in `src` is **only used in `tests`**\n- You can exclude those usages by enabling `tests` usage excluder:\n  - This **will not disable analysis for tests** as only usages of src-defined classes will be excluded\n\n```neon\nparameters:\n    shipmonkDeadCode:\n        usageExcluders:\n            tests:\n                enabled: true\n                devPaths: # optional, autodetects from autoload-dev sections of composer.json when omitted\n                    - %currentWorkingDirectory%/tests\n```\n\nWith such setup, members used only in tests will be reported with corresponding message, e.g:\n\n```\nUnused AddressValidator::isValidPostalCode (all usages excluded by tests excluder)\n```\n\n## Customization:\n- If your application does some magic calls unknown to this library, you can implement your own usage provider.\n- Just tag it with `shipmonk.deadCode.memberUsageProvider` and implement `ShipMonk\\PHPStan\\DeadCode\\Provider\\MemberUsageProvider`\n\n```neon\nservices:\n    -\n        class: App\\ApiOutputUsageProvider\n        tags:\n            - shipmonk.deadCode.memberUsageProvider\n```\n\n\u003e [!IMPORTANT]\n\u003e _The interface \u0026 tag changed in [0.7](../../releases/tag/0.7.0). If you are using PHPStan 1.x, those were [used differently](../../blob/0.5.0/README.md#customization)._\n\n### Reflection-based customization:\n- For simple reflection usecases, you can just extend `ShipMonk\\PHPStan\\DeadCode\\Provider\\ReflectionBasedMemberUsageProvider`:\n\n```php\n\nuse ReflectionMethod;\nuse ShipMonk\\PHPStan\\DeadCode\\Provider\\VirtualUsageData;\nuse ShipMonk\\PHPStan\\DeadCode\\Provider\\ReflectionBasedMemberUsageProvider;\n\nclass FuzzyTwigUsageProvider extends ReflectionBasedMemberUsageProvider\n{\n\n    public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData\n    {\n        if ($method-\u003egetDeclaringClass()-\u003eimplementsInterface(UsedInTwigMarkerInterface::class)) {\n            return VirtualUsageData::withNote('Probably used in twig');\n        }\n        return null;\n    }\n\n}\n```\n\n### AST-based customization:\n- For more complex usecases that are deducible only from AST, you just stick with raw `MemberUsageProvider` interface.\n- Here is simplified example how to emit `User::__construct` usage in following PHP snippet:\n\n```php\nfunction test(SerializerInterface $serializer): User {\n    return $serializer-\u003edeserialize('{\"name\": \"John\"}', User::class, 'json');\n}\n```\n\n```php\nuse PhpParser\\Node;\nuse PhpParser\\Node\\Expr\\MethodCall;\nuse PHPStan\\Analyser\\Scope;\nuse ReflectionMethod;\nuse ShipMonk\\PHPStan\\DeadCode\\Graph\\ClassMethodRef;\nuse ShipMonk\\PHPStan\\DeadCode\\Graph\\ClassMethodUsage;\nuse ShipMonk\\PHPStan\\DeadCode\\Graph\\UsageOrigin;\nuse ShipMonk\\PHPStan\\DeadCode\\Provider\\MemberUsageProvider;\nuse Symfony\\Component\\Serializer\\SerializerInterface;\n\nclass DeserializationUsageProvider implements MemberUsageProvider\n{\n\n    public function __construct(\n        private UsageOriginDetector $originDetector,\n    ) {}\n\n    /**\n     * @return list\u003cClassMemberUsage\u003e\n     */\n    public function getUsages(Node $node, Scope $scope): array\n    {\n        if (!$node instanceof MethodCall) {\n            return [];\n        }\n\n        if (\n            // our deserialization calls constructor\n            $scope-\u003egetType($node-\u003evar)-\u003egetObjectClassNames() === [SerializerInterface::class] \u0026\u0026\n            $node-\u003ename-\u003etoString() === 'deserialize'\n        ) {\n            $secondArgument = $node-\u003egetArgs()[1]-\u003evalue;\n            $serializedClass = $scope-\u003egetType($secondArgument)-\u003egetConstantStrings()[0];\n\n            // record the place it was called from (needed for proper transitive dead code elimination)\n            $usageOrigin = UsageOrigin::createRegular($node, $scope);\n\n            // record the hidden constructor call\n            $constructorRef = new ClassMethodRef($serializedClass-\u003egetValue(), '__construct', false);\n\n            return [new ClassMethodUsage($usageOrigin, $constructorRef)];\n        }\n\n        return [];\n    }\n\n}\n```\n\n### Excluding usages:\n\nYou can exclude any usage based on custom logic, just implement `MemberUsageExcluder` and register it with `shipmonk.deadCode.memberUsageExcluder` tag:\n\n```php\n\nuse ShipMonk\\PHPStan\\DeadCode\\Excluder\\MemberUsageExcluder;\n\nclass MyUsageExcluder implements MemberUsageExcluder\n{\n\n    public function shouldExclude(ClassMemberUsage $usage, Node $node, Scope $scope): bool\n    {\n        // ...\n    }\n\n}\n```\n\n```neon\n# phpstan.neon.dist\nservices:\n    -\n        class: App\\MyUsageExcluder\n        tags:\n            - shipmonk.deadCode.memberUsageExcluder\n```\n\nThe same interface is used for exclusion of test-only usages, see above.\n\n\u003e [!NOTE]\n\u003e Excluders are called **after** providers.\n\n## Dead cycles \u0026 transitively dead methods\n- This library automatically detects dead cycles and transitively dead methods (methods that are only called from dead methods)\n- By default, it reports only the first dead method in the subtree and the rest as a tip:\n\n```\n ------ ------------------------------------------------------------------------\n  Line   src/App/Facade/UserFacade.php\n ------ ------------------------------------------------------------------------\n  26     Unused App\\Facade\\UserFacade::updateUserAddress\n         🪪  shipmonk.deadMethod\n         💡 Thus App\\Entity\\User::updateAddress is transitively also unused\n         💡 Thus App\\Entity\\Address::setPostalCode is transitively also unused\n         💡 Thus App\\Entity\\Address::setCountry is transitively also unused\n         💡 Thus App\\Entity\\Address::setStreet is transitively also unused\n         💡 Thus App\\Entity\\Address::MAX_STREET_CHARS is transitively also unused\n ------ ------------------------------------------------------------------------\n```\n\n- If you want to report all dead methods individually, you can enable it in your `phpstan.neon.dist`:\n\n```neon\nparameters:\n    shipmonkDeadCode:\n        reportTransitivelyDeadMethodAsSeparateError: true\n```\n\n## Automatic removal of dead code\n- If you are sure that the reported methods are dead, you can automatically remove them by running PHPStan with `removeDeadCode` error format:\n\n```bash\nvendor/bin/phpstan analyse --error-format removeDeadCode\n```\n\n```diff\nclass UserFacade\n{\n-    public const TRANSITIVELY_DEAD = 1;\n-\n-    public function deadMethod(): void\n-    {\n-        echo self::TRANSITIVELY_DEAD;\n-    }\n}\n```\n\n- If you are excluding tests usages (see above), this will not cause the related tests to be removed alongside.\n  - But you will see all those kept usages in output (with links to your IDE if you set up `editorUrl` [parameter](https://phpstan.org/user-guide/output-format#opening-file-in-an-editor))\n\n```txt\n • Removed method UserFacade::deadMethod\n   ! Excluded usage at tests/User/UserFacadeTest.php:241 left intact\n```\n\n\n## Calls over unknown types\n- In order to prevent false positives, we support even calls over unknown types (e.g. `$unknown-\u003emethod()`) by marking all methods named `method` as used\n  - Such behaviour might not be desired for strictly typed codebases, because e.g. single `new $unknown()` will mark all constructors as used\n  - The same applies to constant fetches over unknown types (e.g. `$unknown::CONSTANT`)\n  - Thus, you can disable this feature in your `phpstan.neon.dist` by excluding such usages:\n\n```neon\nparameters:\n    shipmonkDeadCode:\n        usageExcluders:\n            usageOverMixed:\n                enabled: true\n```\n\n- If you want to check how many of those cases are present in your codebase, you can run PHPStan analysis with `-vvv` and you will see some diagnostics:\n\n```\nFound 2 usages over unknown type:\n • setCountry method, for example in App\\Entity\\User::updateAddress\n • setStreet method, for example in App\\Entity\\User::updateAddress\n```\n\n## Access of unknown member\n- In order to prevent false positives, we support even calls of unknown methods (e.g. `$class-\u003e$unknown()`) by marking all possible methods as used\n- If we find unknown call over unknown type (e.g. `$unknownClass-\u003e$unknownMethod()`), we ignore such usage (as it would mark all methods in codebase as used) and show warning in debug verbosity (`-vvv`)\n- Note that some calls over `ReflectionClass` also emit unknown method calls:\n\n```php\n/** @var ReflectionClass\u003cFoo\u003e $reflection */\n$methods = $reflection-\u003egetMethods(); // all Foo methods are used here\n```\n\n- All that applies even to constant fetches (e.g. `Foo::{$unknown}`)\n\n## Comparison with tomasvotruba/unused-public\n- You can see [detailed comparison PR](https://github.com/shipmonk-rnd/dead-code-detector/pull/53)\n- Basically, their analysis is less precise and less flexible. Mainly:\n  - It cannot detect dead constructors\n  - It does not properly detect calls within inheritance hierarchy\n  - It does not offer any custom adjustments of used methods\n  - It has almost no built-in library extensions\n  - It ignores trait methods\n  - Is lacks many minor features like class-string calls, dynamic method calls, array callbacks, nullsafe call chains etc\n  - It cannot detect dead cycles nor transitively dead methods\n  - It has no built-in dead code removal\n\n## Limitations:\n- Methods of anonymous classes are never reported as dead ([PHPStan limitation](https://github.com/phpstan/phpstan/issues/8410))\n- Abstract trait methods are never reported as dead\n- Most magic methods (e.g. `__get`, `__set` etc) are never reported as dead\n    - Only supported are: `__construct`, `__clone`\n\n### Other problematic cases:\n\n#### Constructors:\n- For symfony apps \u0026 PHPStan extensions, we simplify the detection by assuming all DIC classes have used constructor.\n- For other apps, you may get false-positives if services are created magically.\n  - To avoid those, you can easily disable constructor analysis with single ignore:\n\n```neon\nparameters:\n    ignoreErrors:\n        - '#^Unused .*?::__construct$#'\n```\n\n#### Private constructors:\n- Those are never reported as dead as those are often used to deny class instantiation\n  - This applies only to constructors without any parameters\n\n#### Interface methods:\n- If you never call interface method over the interface, but only over its implementors, it gets reported as dead\n- But you may want to keep the interface method to force some unification across implementors\n  - The easiest way to ignore it is via custom `MemberUsageProvider`:\n\n```php\nuse ShipMonk\\PHPStan\\DeadCode\\Provider\\VirtualUsageData;\nuse ShipMonk\\PHPStan\\DeadCode\\Provider\\ReflectionBasedMemberUsageProvider;\n\nclass IgnoreDeadInterfaceUsageProvider extends ReflectionBasedMemberUsageProvider\n{\n    public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData\n    {\n        if ($method-\u003egetDeclaringClass()-\u003eisInterface()) {\n            return VirtualUsageData::withNote('Interface method, kept for unification even when possibly unused');\n        }\n\n        return null;\n    }\n}\n```\n\n## Debugging:\n- If you want to see how dead code detector evaluated usages of certain method, you do the following:\n\n```neon\nparameters:\n    shipmonkDeadCode:\n        debug:\n            usagesOf:\n                - App\\User\\Entity\\Address::__construct\n```\n\nThen, run PHPStan with `-vvv` CLI option and you will see the output like this:\n\n```txt\nApp\\User\\Entity\\Address::__construct\n|\n| Marked as alive by:\n| entry virtual usage from ShipMonk\\PHPStan\\DeadCode\\Provider\\SymfonyUsageProvider\n|   calls App\\User\\RegisterUserController::__invoke:36\n|     calls App\\User\\UserFacade::registerUser:142\n|       calls App\\User\\Entity\\Address::__construct\n|\n| Found 2 usages:\n|  • src/User/UserFacade.php:142\n|  • tests/User/Entity/AddressTest.php:64 - excluded by tests excluder\n```\n\nIf you set up `editorUrl` [parameter](https://phpstan.org/user-guide/output-format#opening-file-in-an-editor), you can click on the usages to open it in your IDE.\n\n\u003e [!TIP]\n\u003e You can change the list of debug references without affecting result cache, so rerun is instant!\n\n## Usage in libraries:\n- Libraries typically contain public api, that is unused\n  - If you mark such methods with `@api` phpdoc, those will be considered entrypoints\n  - You can also mark whole class or interface with `@api` to mark all its methods as entrypoints\n\n## Future scope:\n- Dead class property detection\n- Dead class detection\n\n## Contributing\n- Check your code by `composer check`\n- Autofix coding-style by `composer fix:cs`\n- All functionality must be tested\n\n## Supported PHP versions\n- PHP 7.4 - 8.4\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshipmonk-rnd%2Fdead-code-detector","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshipmonk-rnd%2Fdead-code-detector","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshipmonk-rnd%2Fdead-code-detector/lists"}