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

StackOverflow https://stackoverflow.com/questions/11532

  •  08-06-2019
  •  | 
  •  

Вопрос

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

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

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

Это было полезно?

Решение 2

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

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

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

Другие советы

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

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.

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


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

Этот фрагмент сценария 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 вызовов функции.

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

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

ПРИМЕЧАНИЕ:Это ‘быстрый и грязный’ подход к проблеме.Этот скрипт выполняет только лексический переход по файлам и не учитывает ситуации, когда разные модули определяют функции или методы с одинаковыми именами.Если вы используете 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 phpCallGraph чтобы сделать это.Это сгенерирует для вас хороший график (изображение) со всеми задействованными методами.Если метод не подключен ни к какому другому, это хороший признак того, что метод потерян.

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

Способ getKeywordSetOfCategories() осиротел.

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

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

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

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

Просмотрите описанные методы и удалите те, которые вы считаете бесполезными 👍


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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top