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

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

How can I find unused functions in a PHP project

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

PHP中是否有功能或API可以让我分析我的代码库-例如Reflection,token_get_all()

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


您可以尝试塞巴斯蒂安·伯格曼的死代码检测器:

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.

资料来源:https://github.com/sebastianbergmann/phpdcd

请注意,这是一个静态代码分析器,因此对于仅动态调用的方法(例如它无法检测到$foo = 'fn'; $foo();

您可以通过PEAR安装它:

1
pear install phpunit/phpdcd-beta

之后,您可以使用以下选项:

1
2
3
4
5
6
7
8
9
10
11
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.

更多工具:

  • https://phpqa.io/

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?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]);
            }
        }
    }
?>

我可能会花更多时间,以便可以快速找到函数定义和引用的文件和行号。该信息正在收集,只是不显示。


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

1
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,该行显示行数

然后将该命令发送给bash执行,并将输出grepped为0,这表示对该函数的调用为0。

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


用法:find_unused_functions.php <根目录>

注意:这是解决问题的"快速解决"方法。该脚本仅对文件执行词汇传递,并且不考虑不同模块定义相同名称的函数或方法的情况。如果将IDE用于PHP开发,则它可能会提供更全面的解决方案。

需要PHP 5

要保存您的副本并粘贴,可以在此处直接下载以及任何新版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
#!/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.
"
;
    usage();
}

$files = php_files($root_dir);
$tokenized = array();

if ( count($files) == 0 )
{
    echo"No PHP files found.
"
;
    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>

"
);
}

// ============================================================================

function print_report($unused_functions)
{
    if ( count($unused_functions) == 0 )
    {
        echo"No unused functions found.
"
;
    }

    $count = 0;
    foreach ( $unused_functions as $function => $locations )
    {
        foreach ( $locations as $location )
        {
            echo"'$function' in {$location['file']} on line {$location['line']}
"
;
            $count++;
        }
    }

    echo"=======================================
"
;
    echo"Found $count unused function" . (($count == 1) ? '' : 's') .".

"
;
}

// ============================================================================

/* EOF */

因为可以动态调用PHP函数/方法,所以没有确定性的方法可以永远不会调用函数。

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


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

这是一个示例:classGallerySystem.webp

方法getKeywordSetOfCategories()被孤立。

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


2019+更新

我对Andrey的回答感到怀疑,并将其转变为编码标准嗅探。

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

  • 查找所有方法public function someMethod()
  • 然后找到所有方法调用${anything}->someMethod()
  • 并报告那些从未被调用的公共职能

它帮助我删除了必须维护和测试的20多种方法。

找到它们的3个步骤

安装ECS:

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

设置ecs.yaml配置:

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

运行命令:

1
vendor/bin/ecs check src

查看报告的方法,并删除您认为不有用的方法?

您可以在此处了解更多信息:从代码中删除无效的公共方法


afaik没有办法。要知道哪些功能"属于谁",您需要执行系统(运行时后期绑定功能查找)。

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


phpxref将确定从何处调用函数以方便进行分析-但仍然需要一定量的人工。


推荐阅读

    excel怎么用乘法函数

    excel怎么用乘法函数,乘法,函数,哪个,excel乘法函数怎么用?1、首先用鼠标选中要计算的单元格。2、然后选中单元格后点击左上方工具栏的fx公

    excel中乘法函数是什么?

    excel中乘法函数是什么?,乘法,函数,什么,打开表格,在C1单元格中输入“=A1*B1”乘法公式。以此类推到多个单元。1、A1*B1=C1的Excel乘法公式

    标准差excel用什么函数?

    标准差excel用什么函数?,函数,标准,什么,在数据单元格的下方输入l标准差公式函数公式“=STDEVPA(C2:C6)”。按下回车,求出标准公差值。详细

    excel常用函数都有哪些?

    excel常用函数都有哪些?,函数,哪些,常用,1、SUM函数:SUM函数的作用是求和。函数公式为=sum()例如:统计一个单元格区域:=sum(A1:A10)  统计多个

    vue项目一些常见问题

    vue项目一些常见问题,组件,样式,**样式污染问题**同样的样式不需要在每个组件都复制组件内单独的样式加外层class包裹。加scope。否则只是

    01-Vue项目实战-网易云音乐-准备工作

    01-Vue项目实战-网易云音乐-准备工作,网易,项目,前言在接下来的一段时间,我会仿照网易云音乐,利用Vue开发一个移动端的网易云音乐项目。在做