如何在PHP项目中找到未使用的函数

如何在PHP项目中找到未使用的函数,php,Php,如何在PHP项目中找到任何未使用的函数 PHP中是否有内置的特性或API可以让我分析代码库——例如 这些API功能是否足够丰富,我不必依赖第三方工具来执行此类分析?感谢Greg和Dave的反馈。这并不是我想要的,但我决定花一点时间来研究它,并提出了这个快速而肮脏的解决方案: <?php $functions = array(); $path = "/path/to/my/php/project"; define_dir($path, $functions);

如何在PHP项目中找到任何未使用的函数

PHP中是否有内置的特性或API可以让我分析代码库——例如


这些API功能是否足够丰富,我不必依赖第三方工具来执行此类分析?

感谢Greg和Dave的反馈。这并不是我想要的,但我决定花一点时间来研究它,并提出了这个快速而肮脏的解决方案:

<?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]);
            }
        }
    }
?>


我可能会花更多的时间在上面,这样我可以快速找到函数定义和引用的文件和行号;此信息正在收集中,只是没有显示。

如果我没记错,您可以使用。它将生成一个很好的图形(图像)为您与所有涉及的方法。如果一个方法未连接到任何其他方法,则表明该方法是孤立的

下面是一个例子:

方法
getKeywordSetOfCategories()
是孤立的


顺便说一句,您不必拍摄图像——phpCallGraph还可以生成文本文件或PHP数组等。

由于PHP函数/方法可以动态调用,因此无法通过编程方式确定函数是否永远不会被调用


唯一确定的方法是通过手动分析。

将确定从何处调用函数,这将有助于分析-但仍然需要一定量的手动工作。

恐怕没有办法。要知道哪些函数“属于谁”,您需要执行系统(运行时后期绑定函数查找)


但是重构工具是基于静态代码分析的。我非常喜欢动态类型语言,但在我看来,它们很难扩展。大型代码库和动态类型语言中缺乏安全重构是可维护性和处理软件演化的一个主要缺点。

您可以尝试Sebastian Bergmann的死代码检测器:

phpdcd
是PHP代码的死代码检测器(DCD)。它扫描PHP项目中所有已声明的函数和方法,并将这些函数和方法报告为“死代码”,至少不会调用一次

资料来源:

请注意,它是一个静态代码分析器,因此可能会对仅动态调用的方法给出误报,例如,它无法检测
$foo='fn'$foo()

您可以通过PEAR进行安装:

pear install phpunit/phpdcd-beta
之后,您可以使用以下选项:

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.
用法:phpdcd[开关]。。。
--如果仅由死代码调用,则递归报告代码为死代码。
--从代码分析中排除。
--后缀要检查的文件后缀的逗号分隔列表。
--帮助打印此使用信息。
--版本打印版本并退出。
--详细打印进度条。
更多工具:



注意:根据存储库通知,此项目不再维护,其存储库仅用于存档目的。因此,您的里程数可能会有所不同。

用法:查找未使用的函数。php

注意:这是一种解决问题的快速方法。此脚本仅对文件执行词法传递,不考虑不同模块定义同名函数或方法的情况。如果您在PHP开发中使用IDE,它可能会提供更全面的解决方案

需要PHP5

要保存拷贝和粘贴,请使用直接下载和任何新版本

#!/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 */
#/usr/bin/php-f

这一点bash脚本可能会有所帮助:

grep -rhio ^function\ .*\(  .|awk -F'[( ]'  '{print "echo -n " $2 " && grep -rin " $2 " .|grep -v function|wc -l"}'|bash|grep 0
这基本上是递归地将当前目录中的函数定义变灰,并将点击传递给awk,awk形成一个命令来执行以下操作:

  • 打印函数名
  • 递归地再次对它进行grep
  • 输出到grep-v以过滤掉函数定义以便保留对函数的调用的管道
  • 将此输出传输到wc-l,wc-l打印行计数
然后将此命令发送到bash执行,并将输出灰显为0,这表示对函数的调用为0

请注意,这不会解决calebbrown引用的上述问题,因此输出中可能存在一些误报。

2019+更新 我被发现了,并把它变成了一个编码标准嗅探

检测非常简单,但功能强大:

  • 查找所有方法
    公共函数someMethod()
  • 然后查找所有方法调用
    ${anything}->somethod()
  • 只需报告那些从未调用过的公共功能
它帮助我移除了我必须维护和测试的设备


找到它们的3个步骤 安装云服务器:

composer require symplify/easy-coding-standard --dev
设置
ecs.yaml
config:

# ecs.yaml
services:
    Symplify\CodingStandard\Sniffs\DeadCode\UnusedPublicMethodSniff: ~
运行以下命令:

vendor/bin/ecs check src
查看报告的方法并删除那些您不知道的方法 我使用了上面列出的其他方法,甚至这里的2019更新答案也过时了

#!/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 */
TomášVotruba的回答让我找到了潘,因为ECS路线现在已经被弃用了。Symplify已删除无效的公共方法检查器

Phan是PHP的静态分析器 我们可以利用PAN搜索死代码。以下是使用composer安装的步骤。这些步骤也可以在上找到。这些说明假设您是项目的根

步骤1-安装带合成器的Phan 步骤2-安装php ast
PHP-AST
是Phan的一项要求 当我使用WSL时,我已经能够使用PECL进行安装,但是,在git repo中可以找到其他的
php ast
安装方法

pecl install ast
步骤3-找到并编辑php.ini以使用php ast 找到当前
php.ini

php -i | grep 'php.ini'
现在获取该文件位置和nano(或您选择的任何一个来编辑此文档)。找到所有扩展的区域并添加以下行:

extension=ast.so
步骤4-创建
......

'directory_list' => [
  'public_html'
],

......

'exclude_analysis_directory_list' => [
    'vendor/',
    'public_html/app/plugins',
    'public_html/app/mu-plugins',
    'public_html/admin'
],
......
./vendor/bin/phan --dead-code-detection
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()