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

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

Существуют ли особенности или API - интерфейсы , встроенные в PHP , который позволит мне проанализировать свою кодовую - например , отражение , token_get_all()?

Достаточно ли богаты эти API-интерфейсы, чтобы мне не пришлось полагаться на сторонние инструменты для выполнения такого анализа?

59 php
14.08.2008 19:08:21
Greg Hurlman 14.08.2008 19:11:08
Xdebug также может обеспечить покрытие кода . Вы можете использовать его в сочетании с директивами ini auto_prepend_file и auto_append_file для регистрации покрытия кода вашего приложения во время общего использования.
Dave Marshall 14.08.2008 20:17:09
xdebugмогу вам сказать, что с помощью анализа покрытия кода xdebug.org/docs/code_coverage
Mchl 25.01.2011 09:42:17
Я нахожу, что простой поиск файлов может часто уже делать - если вы не используете имена переменных функций и тому подобное.
Pekka 25.01.2011 09:43:20
9 ОТВЕТОВ
РЕШЕНИЕ

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

<?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]);
            }
        }
    }
?>

Я, вероятно, потрачу на это больше времени, чтобы быстро найти файлы и номера строк с определениями функций и ссылками; эта информация собирается, просто не отображается.

24
6.01.2009 15:33:20
Это решение хорошо, если вы никогда не используете call_user_func()или call_user_func_array()или$var()
calebbrown 26.11.2008 07:17:03
Я должен был заменить два die()с continueэтим, чтобы не подавиться анонимными функциями.
jlh 4.10.2018 07:28:21

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

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

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

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

3
29.03.2009 02:12:51

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

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

3
1.06.2010 00:38:22

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

-1
19.10.2015 12:12:27

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

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

-1
1.06.2010 21:19:14

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

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.

Больше инструментов:


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

32
9.10.2018 23:43:33
Со страницы github: «Этот проект больше не поддерживается, а его хранилище хранится только в архивных целях».
Burhan Ali 4.12.2015 11:57:32
@BurhanAli больше не поддерживается, не обязательно означает, что он больше не работает. Не стесняйтесь брать на себя обслуживание.
Gordon 4.12.2015 12:04:08
@Gordon Существует причина, по которой он больше не поддерживается, большой спектр вызовов методов, которые он не обнаруживает: «Class :: method ()» и конструкторы, как сообщается, не используются, например. Подход рефлексии не идеален для динамического языка, и поэтому улучшение инструмента в действительности означало бы переписать новый, используя другой подход для обнаружения используемых методов.
Dereckson 8.01.2016 14:48:19
@ Дерексон: да, я согласен. но пока кто - то , наконец, делает это, это является еще лучше , чем ничего.
Gordon 8.01.2016 14:54:03
Спасибо за ссылки "больше инструментов", которые также ссылаются на "PHP Mess Detector" ( phpmd.org )
BurninLeo 12.01.2016 11:28:49

ИСПОЛЬЗОВАНИЕ: 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 */
7
20.08.2011 16:57:40
Просто здорово, спасибо Андрей! Я превращаю это в стандартную программу проверки кода с большой помощью: github.com/Symplify/Symplify/pull/466
Tomáš Votruba 21.11.2017 11:52:01

Этот фрагмент сценария 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 выше, поэтому в выводе могут быть некоторые ложные срабатывания.

20
2.04.2012 15:43:35
Любить это! Быстро и грязно и практично. Вот обновленная версия для современного PHP. Удалите 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$'
Gregory Cosmo Haun 15.04.2020 23:47:47

Обновление 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

Просматривайте описанные методы и удаляйте те, которые вам не подходят.

2
16.03.2019 11:55:43
Оно работает. Подтвердил его вывод простым поиском упомянутых неиспользуемых функций. Некоторые замечания: Вы должны добавить свою testsпапку в команду, как vendor/bin/ecs check src tests. Без этого вы можете получить больше ложных срабатываний. Спасибо Томаш Вотруба за это.
k00ni 25.03.2019 11:17:16
Спасибо за отзыв, я рад, что он работает хорошо для вас! Что касается тестов, если вы включите их, вы можете получить ложные срабатывания, потому что публичные методы будут вызываться только в тестах . Это на самом деле исправлено в более новой версии сниффа.
Tomáš Votruba 25.03.2019 16:25:24