Сведения о вопросе

MAT

20:51, 4th August, 2020

Теги

php    

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

Просмотров: 484   Ответов: 9

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

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

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



  Сведения об ответе

dumai

19:20, 24th August, 2020

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

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

Источник: 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.

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


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


  Сведения об ответе

COOL

02:24, 3rd August, 2020

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

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

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


  Сведения об ответе

lesha

07:53, 5th August, 2020

Этот бит скриптов 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 вызовов функции.

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


  Сведения об ответе

davran

08:17, 1st August, 2020

Использование: find_unused_functions.php <root_directory>

NOTE: это "30-й" подход к проблеме. Этот сценарий выполняет только лексический проход над файлами и не учитывает ситуации, когда различные модули определяют идентично именованные функции или методы. Если вы используете 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 */


  Сведения об ответе

ITSME

22:29, 22nd August, 2020

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

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


  Сведения об ответе

ASSembler

20:40, 2nd August, 2020

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

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

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

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


  Сведения об ответе

Chhiki

06:28, 26th August, 2020

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

Смотрите отчетные методы и удаляйте те, которые вам не нравятся.


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


  Сведения об ответе

ASER

18:59, 29th August, 2020

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


  Сведения об ответе

piter

08:32, 22nd August, 2020

насколько мне известно, нет никакой возможности. Чтобы узнать, какие функции "are belonging to whom" вам понадобятся для выполнения системы (runtime late binding function lookup).

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


Ответить на вопрос

Чтобы ответить на вопрос вам нужно войти в систему или зарегистрироваться