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

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

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

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

Ответы [ 9 ]

33 голосов
/ 25 января 2011

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

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.

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


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

24 голосов
/ 18 августа 2008

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

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

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

18 голосов
/ 02 апреля 2012

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

7 голосов
/ 20 августа 2011

ИСПОЛЬЗОВАНИЕ: 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 */
3 голосов
/ 01 июня 2010

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

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

3 голосов
/ 29 марта 2009

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

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

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

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

1 голос
/ 16 марта 2019

2019 + обновление

Я был вдохновлен ответом Андрея и превратил это в стандартный сниффинг кодирования.

Обнаружение очень простое, но мощное:

  • находит все методы public function someMethod()
  • затем найдите все вызовы методов ${anything}->someMethod()
  • и просто сообщает о тех открытых функциях, которые никогда не вызывались

Это помогло мне удалить более 20 + методов Я должен был бы поддерживать и тестировать.


3 шага, чтобы найти их

Установить ECS:

composer require symplify/easy-coding-standard --dev

Настройка ecs.yaml config:

# ecs.yaml
services:
    Symplify\CodingStandard\Sniffs\DeadCode\UnusedPublicMethodSniff: ~

Запустите команду:

vendor/bin/ecs check src

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


Подробнее об этом можно прочитать здесь: Удалить из вашего кода мертвые общедоступные методы

0 голосов
/ 02 июня 2010

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

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

0 голосов
/ 01 июня 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...