如何在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. |
更多工具:
注意:根据存储库通知,该项目不再维护,其存储库仅用于存档目的。因此,您的里程可能会有所不同。
感谢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将确定从何处调用函数以方便进行分析-但仍然需要一定量的人工。