Как я могу найти любые неиспользуемые функции в проекте PHP?
Существуют ли особенности или API - интерфейсы , встроенные в PHP , который позволит мне проанализировать свою кодовую - например , отражение , token_get_all()
?
Достаточно ли богаты эти API-интерфейсы, чтобы мне не пришлось полагаться на сторонние инструменты для выполнения такого анализа?
Спасибо Грегу и Дейву за отзыв. Это было не совсем то, что я искал, но я решил потратить немного времени на его изучение и нашел быстрое и грязное решение:
<?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]);
}
}
}
?>
Я, вероятно, потрачу на это больше времени, чтобы быстро найти файлы и номера строк с определениями функций и ссылками; эта информация собирается, просто не отображается.
call_user_func()
или call_user_func_array()
или$var()
die()
с continue
этим, чтобы не подавиться анонимными функциями. Если я правильно помню, вы можете использовать phpCallGraph для этого. Он сгенерирует хороший график (изображение) для вас со всеми задействованными методами. Если метод не связан ни с каким другим, это хороший признак того, что метод потерян.
Вот пример: classGallerySystem.png
Метод getKeywordSetOfCategories()
осиротел.
Кстати, вам не нужно снимать изображение - phpCallGraph также может генерировать текстовый файл или массив PHP и т. Д.
Поскольку функции / методы PHP могут вызываться динамически, нет никакого программного способа точно знать, будет ли функция никогда не вызываться.
Единственный верный путь - через ручной анализ.
phpxref определит, откуда вызываются функции, которые могли бы облегчить анализ - но все же требуется определенное количество ручного усилия.
афаик нет пути. Чтобы узнать, какие функции «кому принадлежат», вам необходимо выполнить систему (поиск функции позднего связывания во время выполнения).
Но инструменты рефакторинга основаны на статическом анализе кода. Мне действительно нравятся динамически типизированные языки, но, на мой взгляд, их сложно масштабировать. Отсутствие безопасных рефакторингов в больших кодовых базах и языках с динамической типизацией является основным недостатком для удобства сопровождения и обработки эволюции программного обеспечения.
Вы можете попробовать детектор мертвого кода Себастьяна Бергмана:
phpdcd
является детектором мертвого кода (DCD) для кода PHP Он сканирует проект PHP на предмет всех объявленных функций и методов и сообщает о них как о «мертвом коде», который не вызывается хотя бы один раз.
Источник: https://github.com/sebastianbergmann/phpdcd
Обратите внимание, что это статический анализатор кода, поэтому он может давать ложные срабатывания для методов, вызываемых только динамически, например, он не может обнаружить $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.
Больше инструментов:
Примечание: согласно уведомлению о хранилище, этот проект больше не поддерживается, а его хранилище хранится только в архивных целях . Так что ваш пробег может отличаться.
ИСПОЛЬЗОВАНИЕ: find_unused_functions.php <корневой_каталог>
ПРИМЕЧАНИЕ. Это быстрый и грязный подход к проблеме. Этот скрипт выполняет только лексическую передачу файлов и не учитывает ситуации, когда разные модули определяют идентично названные функции или методы. Если вы используете IDE для разработки PHP, она может предложить более комплексное решение.
Требуется PHP 5
Для сохранения копии и вставки здесь доступна прямая загрузка и любые новые версии .
#!/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 */
Этот фрагмент сценария bash может помочь:
grep -rhio ^function\ .*\( .|awk -F'[( ]' '{print "echo -n " $2 " && grep -rin " $2 " .|grep -v function|wc -l"}'|bash|grep 0
Это в основном рекурсивно ищет текущий каталог для определения функций, передает обращения к awk, который формирует команду для выполнения следующих действий:
- напечатать название функции
- рекурсивно grep для этого снова
- передать этот вывод в grep -v, чтобы отфильтровать определения функций, чтобы сохранить вызовы функций
- направляет этот вывод в wc -l, который печатает количество строк
Затем эта команда отправляется на выполнение в bash, и вывод очищается до 0, что указывает на 0 обращений к функции.
Обратите внимание, что это не решит проблему calebbrown cites выше, поэтому в выводе могут быть некоторые ложные срабатывания.
sort -u
(удалите дубликаты) оптимизацию, если вы предпочитаете функции в порядке файлов, а не в альфа. egrep -rhio 'function\ \w+\(' .|sort -u |awk -F'[( ]' '{print "echo -n " $2 " && grep -rin " $2 " .|grep -v function |wc -l"}'| bash | grep ' 0$'
Обновление 2019+
Ответ Андрея меня вдохновил , и я превратил его в стандартный сниффинг кодирования.
Обнаружение очень простое, но мощное:
- находит все методы
public function someMethod()
- затем найдите все вызовы методов
${anything}->someMethod()
- и просто сообщает те публичные функции, которые никогда не назывались
Это помогло мне удалить более 20+ методов, которые мне нужно было бы поддерживать и тестировать.
3 шага, чтобы найти их
Установите ECS:
composer require symplify/easy-coding-standard --dev
Настройте ecs.yaml
конфигурацию:
# ecs.yaml
services:
Symplify\CodingStandard\Sniffs\DeadCode\UnusedPublicMethodSniff: ~
Запустите команду:
vendor/bin/ecs check src
Просматривайте описанные методы и удаляйте те, которые вам не подходят.
tests
папку в команду, как vendor/bin/ecs check src tests
. Без этого вы можете получить больше ложных срабатываний. Спасибо Томаш Вотруба за это.
xdebug
могу вам сказать, что с помощью анализа покрытия кода xdebug.org/docs/code_coverage