Can PHPStan find Dead Public Methods?

Found a typo? Edit me

This bold question has been around PHP Internet for many years... at least since 2008. In 2017 I added dead public method sniff to Symplify Coding Standard.

It runs on bare PHP tokens without any type or AST, so the capability was limited. Yet it was able to detect a few unused methods. Now, 5 years later, we maybe have a better solution.

The sniff rule looked for unused public method with very basic algorithm:

1. Collect all Public Methods

public function speedUp()
{
}

public function slowDown()
{
}


2. Collect all Method Calls

$someObject->speedUp();


3. Subtract First List from Second

There we have a list of unused public methods:


That's it!


This sniff helped to detect many dead methods, but without types, it misses a few essential details:

class Car
{
    public function speedUp()
    {
    }
}
class Bus
{
    public function speedUp()
    {
    }
}

Like the same-name methods in 2 different classes. Even if only one is used, both are reported as used. Later I've removed the rule, because the token architecture is rather crappy, as you can see.


A week ago PHPStan 1.8 introduced similar feature, called collectors. The principle is simple:


I got excited and wanted to try this feature as soon as I had some free time.

Last week I made the first rule that detects unused public constants:

I'm roughly playing with new collectors from PHPStan 1.8 😇

This is how "unused public constant" is found ↓https://t.co/SkTCITcLh4 pic.twitter.com/zt4yyq0Hmh

— Tomas Votruba 🇺🇦 (@VotrubaT) June 30, 2022


Detecting constant call is relatively easy because there is always one exact class and constant name:

SomeClass::SOME_CONSTANT


We've added rules to a few projects, and the results are fantastic.

How about trying the same approach to public methods?


PHPStan Collector for Unused Method

With method calls, this is a bit complex, as caller type could be anything:

$value->speedUp(); // $value is mixed type

function run(?Car $value) {
    $value->speedUp(); // $value is null|Car
}

/** @var Car $value;
$value->speedUp(); // $value is probably Car


I wanted to see how many false positives this rule will catch and try out collectors. Take this as a joyful learning experience.

To keep the rule simple and more reliable, I've narrowed the scope further down:

#[Required]
public function autowire(...)
{
    // ...
}

#[Inject]
public function inject(....)
{
    // ...
}


Are you maintaining an open-source project? Do you have a method that is never called but is designed for external use? Mark it with @api to make clear the method is for devs to use. The rule will skip it then.

Use at Your Own Risk

I've prototyped the UnusedPublicClassMethodRule rule. In PR, you'll find both collectors for public methods, their calls, and the rule that compares them both. The test included, of course. Do you want to build a collector of your own? I recommend checking it out and mimicking behavior.


Are you willing to try this rule on your code base? Even if there will be false positives?

You've been warned. There you go...

composer require symplify/phpstan-rules --dev


Register rule with both collectors to phpstan.neon:

rules:
    - Symplify\PHPStanRules\DeadCode\UnusedPublicClassMethodRule

services:
    -
        class: Symplify\PHPStanRules\Collector\ClassMethod\PublicClassMethodCollector
        tags:
            - phpstan.collector

    -
        class: Symplify\PHPStanRules\Collector\ClassMethod\MethodCallCollector
        tags:
            - phpstan.collector

Run PHPStan and see the results.

Do you have a tip to improve this rule or architecture? It's my 2nd collector rule, and it has flaws in the design, so don't hesitate and share it in the comments so that I can improve it. Thank you, good luck, and have fun!


Happy coding!


Have you find this post useful? Do you want more?

Follow me on Twitter, RSS or support me on GitHub Sponsors.