How can I find unused functions in a PHP project

Viewed 31833

How can I find any unused functions in a PHP project?

Are there features or APIs built into PHP that will allow me to analyse my codebase - for example Reflection, token_get_all()?

Are these APIs feature rich enough for me not to have to rely on a third party tool to perform this type of analysis?

10 Answers

You can try Sebastian Bergmann's Dead Code Detector:

phpdcd is a Dead Code Detector (DCD) for PHP code. It scans a PHP project for all declared functions and methods and reports those as being "dead code" that are not called at least once.

Source: https://github.com/sebastianbergmann/phpdcd

Note that it's a static code analyzer, so it might give false positives for methods that only called dynamically, e.g. it cannot detect $foo = 'fn'; $foo();

You can install it via PEAR:

pear install phpunit/phpdcd-beta

After that you can use with the following options:

Usage: phpdcd [switches] <directory|file> ...

--recursive Report code as dead if it is only called by dead code.

--exclude <dir> Exclude <dir> from code analysis.
--suffixes <suffix> A comma-separated list of file suffixes to check.

--help Prints this usage information.
--version Prints the version and exits.

--verbose Print progress bar.

More tools:


Note: as per the repository notice, this project is no longer maintained and its repository is only kept for archival purposes. So your mileage may vary.

Thanks Greg and Dave for the feedback. Wasn't quite what I was looking for, but I decided to put a bit of time into researching it and came up with this quick and dirty solution:

<?php
    $functions = array();
    $path = "/path/to/my/php/project";
    define_dir($path, $functions);
    reference_dir($path, $functions);
    echo
        "<table>" .
            "<tr>" .
                "<th>Name</th>" .
                "<th>Defined</th>" .
                "<th>Referenced</th>" .
            "</tr>";
    foreach ($functions as $name => $value) {
        echo
            "<tr>" . 
                "<td>" . htmlentities($name) . "</td>" .
                "<td>" . (isset($value[0]) ? count($value[0]) : "-") . "</td>" .
                "<td>" . (isset($value[1]) ? count($value[1]) : "-") . "</td>" .
            "</tr>";
    }
    echo "</table>";
    function define_dir($path, &$functions) {
        if ($dir = opendir($path)) {
            while (($file = readdir($dir)) !== false) {
                if (substr($file, 0, 1) == ".") continue;
                if (is_dir($path . "/" . $file)) {
                    define_dir($path . "/" . $file, $functions);
                } else {
                    if (substr($file, - 4, 4) != ".php") continue;
                    define_file($path . "/" . $file, $functions);
                }
            }
        }       
    }
    function define_file($path, &$functions) {
        $tokens = token_get_all(file_get_contents($path));
        for ($i = 0; $i < count($tokens); $i++) {
            $token = $tokens[$i];
            if (is_array($token)) {
                if ($token[0] != T_FUNCTION) continue;
                $i++;
                $token = $tokens[$i];
                if ($token[0] != T_WHITESPACE) die("T_WHITESPACE");
                $i++;
                $token = $tokens[$i];
                if ($token[0] != T_STRING) die("T_STRING");
                $functions[$token[1]][0][] = array($path, $token[2]);
            }
        }
    }
    function reference_dir($path, &$functions) {
        if ($dir = opendir($path)) {
            while (($file = readdir($dir)) !== false) {
                if (substr($file, 0, 1) == ".") continue;
                if (is_dir($path . "/" . $file)) {
                    reference_dir($path . "/" . $file, $functions);
                } else {
                    if (substr($file, - 4, 4) != ".php") continue;
                    reference_file($path . "/" . $file, $functions);
                }
            }
        }       
    }
    function reference_file($path, &$functions) {
        $tokens = token_get_all(file_get_contents($path));
        for ($i = 0; $i < count($tokens); $i++) {
            $token = $tokens[$i];
            if (is_array($token)) {
                if ($token[0] != T_STRING) continue;
                if ($tokens[$i + 1] != "(") continue;
                $functions[$token[1]][1][] = array($path, $token[2]);
            }
        }
    }
?>

I'll probably spend some more time on it so I can quickly find the files and line numbers of the function definitions and references; this information is being gathered, just not displayed.

This bit of bash scripting might help:

grep -rhio ^function\ .*\(  .|awk -F'[( ]'  '{print "echo -n " $2 " && grep -rin " $2 " .|grep -v function|wc -l"}'|bash|grep 0

This basically recursively greps the current directory for function definitions, passes the hits to awk, which forms a command to do the following:

  • print the function name
  • recursively grep for it again
  • piping that output to grep -v to filter out function definitions so as to retain calls to the function
  • pipes this output to wc -l which prints the line count

This command is then sent for execution to bash and the output is grepped for 0, which would indicate 0 calls to the function.

Note that this will not solve the problem calebbrown cites above, so there might be some false positives in the output.

2020 Update

I have used the other methods outlined above, even the 2019 update answer here is outdated.

Tomáš Votruba's answer led me to find Phan as the ECS route has now been deprecated. Symplify have removed the dead public method checker.

Phan is a static analyzer for PHP

We can utilise Phan to search for dead code. Here are the steps to take using composer to install. These steps are also found on the git repo for phan. These instructions assume you're at the root of your project.

Step 1 - Install Phan w/ composer

composer require phan/phan

Step 2 - Install php-ast

PHP-AST is a requirement for Phan As I'm using WSL, I've been able to use PECL to install, however, other install methods for php-ast can be found in a git repo

pecl install ast

Step 3 - Locate and edit php.ini to use php-ast

Locate current php.ini

php -i | grep 'php.ini'

Now take that file location and nano (or whichever of your choice to edit this doc). Locate the area of all extensions and ADD the following line:

extension=ast.so

Step 4 - create a config file for Phan

Steps on config file can be found in Phan's documentation on how to create a config file You'll want to use their sample one as it's a good starting point. Edit the following arrays to add your own paths on both directory_list & exclude_analysis_directory_list. Please note that exclude_analysis_directory_list will still be parsed but not validated eg. adding Wordpress directory here would mean, false positives for called wordpress functions in your theme would not appear as it found the function in wordpress but at the same time it'll not validate functions in wordpress' folder. Mine looked like this

......

'directory_list' => [
  'public_html'
],

......

'exclude_analysis_directory_list' => [
    'vendor/',
    'public_html/app/plugins',
    'public_html/app/mu-plugins',
    'public_html/admin'
],
......

Step 5 - Run Phan with dead code detection

Now that we've installed phan and ast, configured the folders we wish to parse, it's time to run Phan. We'll be passing an argument to phan --dead-code-detection which is self explanatory.

./vendor/bin/phan --dead-code-detection

This output will need verifying with a fine tooth comb but it's certainly the best place to start

The output will look like this in console

the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()
the/path/to/php/file.php:324 PhanUnreferencedPublicMethod Possibly zero references to public method\the\path\to\function::the_funciton()

Please feel free to add to this answer or correct my mistakes :)

If I remember correctly you can use phpCallGraph to do that. It'll generate a nice graph (image) for you with all the methods involved. If a method is not connected to any other, that's a good sign that the method is orphaned.

Here's an example: classGallerySystem.png

The method getKeywordSetOfCategories() is orphaned.

Just by the way, you don't have to take an image -- phpCallGraph can also generate a text file, or a PHP array, etc..

2019+ Update

I got inspied by Andrey's answer and turned this into a coding standard sniff.

The detection is very simple yet powerful:

  • finds all methods public function someMethod()
  • then find all method calls ${anything}->someMethod()
  • and simply reports those public functions that were never called

It helped me to remove over 20+ methods I would have to maintain and test.


3 Steps to Find them

Install ECS:

composer require symplify/easy-coding-standard --dev

Set up ecs.yaml config:

# ecs.yaml
services:
    Symplify\CodingStandard\Sniffs\DeadCode\UnusedPublicMethodSniff: ~

Run the command:

vendor/bin/ecs check src

See reported methods and remove those you don't fine useful


You can read more about it here: Remove Dead Public Methods from Your Code

Related