Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

Вы можете попробовать Sebastian Bergmann Dead Code Detector:

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.

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


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

Ответ 3

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

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

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

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

Затем эта команда отправляется для выполнения в bash, а вывод grepped для 0, что указывает на 0 вызовов функции.

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

Ответ 4

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

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

Метод getKeywordSetOfCategories() потерян.

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

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