Вопрос:

How can I find unused functions in a PHP project

php

19795 просмотра

9 ответа

5091 Репутация автора

How can I find any unused functions in a PHP project?

Are there features or APIs built into PHP that will allow me to analyse my codebase - for example Reflection, token_get_all()?

Are these APIs feature rich enough for me not to have to rely on a third party tool to perform this type of analysis?

Автор: Stacey Richards Источник Размещён: 14.08.2008 07:08

Ответы (9)


24 плюса

5091 Репутация автора

Решение

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

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

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

Автор: Stacey Richards Размещён: 18.08.2008 01:47

3 плюса

20132 Репутация автора

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

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

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

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

Автор: Till Размещён: 29.03.2009 02:12

3 плюса

42385 Репутация автора

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

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

Автор: webbiedave Размещён: 01.06.2010 12:38

-1 плюса

41304 Репутация автора

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

Автор: symcbean Размещён: 01.06.2010 08:59

-1 плюса

8265 Репутация автора

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

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

Автор: manuel aldana Размещён: 01.06.2010 09:19

33 плюса

265526 Репутация автора

You can try Sebastian Bergmann's Dead Code Detector:

phpdcd is a Dead Code Detector (DCD) for PHP code. It scans a PHP project for all declared functions and methods and reports those as being "dead code" that are not called at least once.

Source: https://github.com/sebastianbergmann/phpdcd

Note that it's a static code analyzer, so it might give false positives for methods that only called dynamically, e.g. it cannot detect $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.

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


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

Автор: Gordon Размещён: 25.01.2011 09:50

7 плюса

1974 Репутация автора

ИСПОЛЬЗОВАНИЕ: 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 */
Автор: Andrey Butov Размещён: 20.08.2011 04:57

18 плюса

191 Репутация автора

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

Автор: Tim Cullen Размещён: 02.04.2012 03:43

0 плюса

9596 Репутация автора

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

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


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

Автор: Tomáš Votruba Размещён: 16.03.2019 11:55
Вопросы из категории :
32x32