开发者

How can I find unused functions in a PHP project

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 开发者_Python百科me not to have to rely on a third party tool to perform this type of analysis?


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:

  • https://phpqa.io/

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.


USAGE: find_unused_functions.php <root_directory>

NOTE: This is a ‘quick-n-dirty’ approach to the problem. This script only performs a lexical pass over the files, and does not respect situations where different modules define identically named functions or methods. If you use an IDE for your PHP development, it may offer a more comprehensive solution.

Requires PHP 5

To save you a copy and paste, a direct download, and any new versions, are available here.

#!/usr/bin/php -f
 
<?php
 
// ============================================================================
//
// find_unused_functions.php
//
// Find unused functions in a set of PHP files.
// version 1.3
//
// ============================================================================
//
// Copyright (c) 2011, Andrey Butov. All Rights Reserved.
// This script is provided as is, without warranty of any kind.
//
// http://www.andreybutov.com
//
// ============================================================================
 
// This may take a bit of memory...
ini_set('memory_limit', '2048M');
 
if ( !isset($argv[1]) ) 
{
    usage();
}
 
$root_dir = $argv[1];
 
if ( !is_dir($root_dir) || !is_readable($root_dir) )
{
    echo "ERROR: '$root_dir' is not a readable directory.\n";
    usage();
}
 
$files = php_files($root_dir);
$tokenized = array();
 
if ( count($files) == 0 )
{
    echo "No PHP files found.\n";
    exit;
}
 
$defined_functions = array();
 
foreach ( $files as $file )
{
    $tokens = tokenize($file);
 
    if ( $tokens )
    {
        // We retain the tokenized versions of each file,
        // because we'll be using the tokens later to search
        // for function 'uses', and we don't want to 
        // re-tokenize the same files again.
 
        $tokenized[$file] = $tokens;
 
        for ( $i = 0 ; $i < count($tokens) ; ++$i )
        {
            $current_token = $tokens[$i];
            $next_token = safe_arr($tokens, $i + 2, false);
 
            if ( is_array($current_token) && $next_token && is_array($next_token) )
            {
                if ( safe_arr($current_token, 0) == T_FUNCTION )
                {
                    // Find the 'function' token, then try to grab the 
                    // token that is the name of the function being defined.
                    // 
                    // For every defined function, retain the file and line
                    // location where that function is defined. Since different
                    // modules can define a functions with the same name,
                    // we retain multiple definition locations for each function name.
 
                    $function_name = safe_arr($next_token, 1, false);
                    $line = safe_arr($next_token, 2, false);
 
                    if ( $function_name && $line )
                    {
                        $function_name = trim($function_name);
                        if ( $function_name != "" )
                        {
                            $defined_functions[$function_name][] = array('file' => $file, 'line' => $line);
                        }
                    }
                }
            }
        }
    }
}
 
// We now have a collection of defined functions and
// their definition locations. Go through the tokens again, 
// and find 'uses' of the function names. 
 
foreach ( $tokenized as $file => $tokens )
{
    foreach ( $tokens as $token )
    {
        if ( is_array($token) && safe_arr($token, 0) == T_STRING )
        {
            $function_name = safe_arr($token, 1, false);
            $function_line = safe_arr($token, 2, false);;
 
            if ( $function_name && $function_line )
            {
                $locations_of_defined_function = safe_arr($defined_functions, $function_name, false);
 
                if ( $locations_of_defined_function )
                {
                    $found_function_definition = false;
 
                    foreach ( $locations_of_defined_function as $location_of_defined_function )
                    {
                        $function_defined_in_file = $location_of_defined_function['file'];
                        $function_defined_on_line = $location_of_defined_function['line'];
 
                        if ( $function_defined_in_file == $file && 
                             $function_defined_on_line == $function_line )
                        {
                            $found_function_definition = true;
                            break;
                        }
                    }
 
                    if ( !$found_function_definition )
                    {
                        // We found usage of the function name in a context
                        // that is not the definition of that function. 
                        // Consider the function as 'used'.
 
                        unset($defined_functions[$function_name]);
                    }
                }
            }
        }
    }
}
 
 
print_report($defined_functions);   
exit;
 
 
// ============================================================================
 
function php_files($path) 
{
    // Get a listing of all the .php files contained within the $path
    // directory and its subdirectories.
 
    $matches = array();
    $folders = array(rtrim($path, DIRECTORY_SEPARATOR));
 
    while( $folder = array_shift($folders) ) 
    {
        $matches = array_merge($matches, glob($folder.DIRECTORY_SEPARATOR."*.php", 0));
        $moreFolders = glob($folder.DIRECTORY_SEPARATOR.'*', GLOB_ONLYDIR);
        $folders = array_merge($folders, $moreFolders);
    }
 
    return $matches;
}
 
// ============================================================================
 
function safe_arr($arr, $i, $default = "")
{
    return isset($arr[$i]) ? $arr[$i] : $default;
}
 
// ============================================================================
 
function tokenize($file)
{
    $file_contents = file_get_contents($file);
 
    if ( !$file_contents )
    {
        return false;
    }
 
    $tokens = token_get_all($file_contents);
    return ($tokens && count($tokens) > 0) ? $tokens : false;
}
 
// ============================================================================
 
function usage()
{
    global $argv;
    $file = (isset($argv[0])) ? basename($argv[0]) : "find_unused_functions.php";
    die("USAGE: $file <root_directory>\n\n");
}
 
// ============================================================================
 
function print_report($unused_functions)
{
    if ( count($unused_functions) == 0 )
    {
        echo "No unused functions found.\n";
    }
 
    $count = 0;
    foreach ( $unused_functions as $function => $locations )
    {
        foreach ( $locations as $location )
        {
            echo "'$function' in {$location['file']} on line {$location['line']}\n";
            $count++;
        }
    }
 
    echo "=======================================\n";
    echo "Found $count unused function" . (($count == 1) ? '' : 's') . ".\n\n";
}
 
// ============================================================================
 
/* EOF */


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..


Because PHP functions/methods can be dynamically invoked, there is no programmatic way to know with certainty if a function will never be called.

The only certain way is through manual analysis.


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

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜