Как найти неиспользуемые функции в PHP-проекте

Как найти неиспользуемые функции в проекте PHP?

Есть ли функции или API, встроенные в PHP, которые позволят мне анализировать мою кодовую базу – например Reflection , token_get_all() ?

Являются ли эти API достаточно богатыми, чтобы мне не приходилось полагаться на сторонний инструмент для проведения такого анализа?

Вы можете попробовать детектор мертвых кодов Себастьяна Бергмана:

phpdcd – это детектор мертвых кодов (DCD) для PHP-кода. Он сканирует проект PHP для всех объявленных функций и методов и сообщает, что это «мертвый код», который не вызывается хотя бы один раз.

Источник: https://github.com/sebastianbergmann/phpdcd

Обратите внимание, что это статический анализатор кода, поэтому он может давать ложные срабатывания для методов, которые вызываются только динамически, например, он не может обнаружить $foo = 'fn'; $foo(); $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. 

Дополнительные инструменты:


Примечание: в соответствии с уведомлением репозитория этот проект больше не поддерживается, а его хранилище хранится только в архивных целях . Таким образом, ваш пробег может отличаться.

Спасибо Грегу и Дейву за отзывы. Не совсем то, что я искал, но я решил потратить немного времени на изучение этого и придумал это быстрое и грязное решение:

 <?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 может помочь:

 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, а выход – grepped для 0, что указывает на 0 вызовов функции.

Обратите внимание, что это не решит проблему calebbrown cites выше, поэтому в результате могут возникнуть некоторые ложные срабатывания.

Если я правильно помню, вы можете использовать phpCallGraph для этого. Он создаст хороший граф (изображение) для вас со всеми задействованными способами. Если метод не связан с каким-либо другим, это хороший признак того, что метод осиротет.

Вот пример: classGallerySystem.png

Метод getKeywordSetOfCategories() является сиротой.

Кстати, вам не нужно брать изображение – phpCallGraph также может генерировать текстовый файл или массив PHP и т. Д.

ИСПОЛЬЗОВАНИЕ: find_unused_functions.php <root_directory>

ПРИМЕЧАНИЕ. Это – «быстрый n-грязный» подход к проблеме. Этот скрипт выполняет только лексический проход по файлам и не учитывает ситуации, когда разные модули определяют одинаково названные функции или методы. Если вы используете среду 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 */ не #!/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 */ 

Поскольку функции / методы PHP можно динамически вызывать, нет никакого программного способа узнать с уверенностью, если функция никогда не будет вызвана.

Единственный определенный способ – ручной анализ.

afaik нет никакого способа. Чтобы узнать, какие функции «принадлежат кому», вам нужно будет выполнить систему (поиск функции поздней привязки во время выполнения).

Но инструменты рефакторинга основаны на анализе статического кода. Мне очень нравятся динамически типизированные языки, но, на мой взгляд, их сложно масштабировать. Отсутствие безопасных рефакторингов в больших кодовых версиях и динамически типизированных языках является серьезным недостатком для поддержки и разработки программного обеспечения.

phpxref будет определять, откуда будут вызываться функции, из которых будет облегчен анализ, – но все же есть определенное количество ручных усилий.