Как найти неиспользуемые функции в проекте PHP?
Существуют ли функции или API, встроенные в PHP, которые позволят мне проанализировать мою кодовую базу - например, Reflection, token_get_all()
?
Достаточно ли функциональны эти API, чтобы мне не пришлось прибегать к помощи сторонних инструментов для проведения такого рода анализа?
Вы можете попробовать Себастьян Бергманн'ы мертвый код детектора:
&ГТ; phpdcd
- это мертвый детектор код (ДКД) для PHP-кода. Она сканирует проекта PHP для всех заявленных функций и методов и сообщает, что как-то "мертвый код", что не позвонил хотя бы раз.
Источник: https://github.com/sebastianbergmann/phpdcd
Обратите внимание, что это'ы статический анализатор кода, поэтому он может давать ложные срабатывания для методов, которые только называются динамически, например, он не может обнаружить не$Foo = 'ФН'; $и Foo();
Вы можете установить его с помощью груши:
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]);
}
}
}
?>
Возможно, я потрачу еще немного времени на это, чтобы быстро найти файлы и номера строк определений функций и ссылок; эта информация собирается, просто не отображается.
Этот бит Баш скриптов может помочь:
grep -rhio ^function\ .*\( .|awk -F'[( ]' '{print "echo -n " $2 " && grep -rin " $2 " .|grep -v function|wc -l"}'|bash|grep 0
В основном это рекурсивно Грэпс текущего каталога для определения функций, передает просмотров в awk, который формирует команду выполнить следующие действия:
Эта команда отправляется на исполнение в bash и выход grepped на 0, что будет указывать на 0 вызовы функции.
Обратите внимание, что это не решить проблему выше calebbrown СИТЕС, поэтому там могут быть некоторые ложные срабатывания на выходе.
Использование: find_unused_functions.php &амп;ЛТ;корень&амп;ГТ;
Примечание: это подход быстрый-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 */
Если я правильно помню, для этого можно использовать phpCallGraph. Он сгенерирует для вас красивый график (изображение) со всеми задействованными методами. Если метод не связан ни с одним другим, это хороший признак того, что метод осиротел.
Вот пример: classGallerySystem.png
Метод getKeywordSetOfCategories()
осиротел.
Кстати, необязательно брать изображение - phpCallGraph может также генерировать текстовый файл, или PHP массив, и т.д..
Поскольку функции/методы PHP могут вызываться динамически, не существует программного способа узнать с уверенностью, что функция никогда не будет вызвана.
Единственный надежный способ - это ручной анализ.
Я inspied по Андрей'ы answer и превратили это в стандарт кодирования нюхать.
Обнаружение очень простой, но мощный:
${ничего}-&ГТ;метода someMethod()
Он помог мне снять за 20+ методы я бы для поддержания и тест.
&ЛТ;БР&ГТ;
Установите ФОВ:
composer require symplify/easy-coding-standard --dev
Настройка ФОВ.в YAML
конфигурации:
# ecs.yaml
services:
Symplify\CodingStandard\Sniffs\DeadCode\UnusedPublicMethodSniff: ~
Выполните команду:
vendor/bin/ecs check src
Увидеть сообщили методов и удалить те, которые вы не'т хорошо полезные 👍
Вы можете прочитать больше об этом здесь: &ЛТ;а href="https://www.tomasvotruba.cz/blog/2019/03/14/remove-dead-public-methdos-from-your-code/">удалить мертвые публичных методов из кода&ЛТ;/а&ГТ;
насколько мне известно, нет никакой возможности. Знать, какие функции "не относятся к кому" Вы должны выполнить систему (выполнения позднего связывания подстановки функция).
Но инструменты рефакторинга на основе статического анализа кода. Мне очень нравится динамически типизированные языки, но на мой взгляд их трудно масштабировать. Отсутствие безопасного рефакторинга в больших кодовых баз и динамически типизированных языков, является основным препятствием для ремонтопригодность и управляемость эволюции программного обеспечения.
phpxref будет определить, где функции вызываются из которых облегчило бы анализ - но там's по-прежнему определенное количество ручной работы.