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


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

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

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

8 53
php

8 ответов:

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

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

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

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

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

этот бит сценариев bash может помочь:

grep -rhio ^function\ .*\(  .|awk -F'[( ]'  '{print "echo -n "  " && grep -rin "  " .|grep -v function|wc -l"}'|bash|grep 0

это в основном рекурсивно greps текущий каталог для определения функций, передает хиты awk, который формирует команду, чтобы сделать следующее:

  • печатать имя функции
  • рекурсивно grep для него снова
  • трубопровод, который выводится в grep-v для фильтрации определений функций, чтобы сохранить вызовы функции
  • передает этот вывод в wc-l, который печатает количество строк

эта команда затем отправляется для выполнения в bash, и выводится для 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 также может создать a текстовый файл или массив PHP и т. д..

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

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

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

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

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