Como posso encontrar funções não utilizadas em um projeto PHP

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

  •  08-06-2019
  •  | 
  •  

Pergunta

Como posso encontrar funções não utilizadas em um projeto PHP?

Existem recursos ou APIs integrados ao PHP que me permitirão analisar minha base de código - por exemplo Reflexão, token_get_all()?

Esses recursos de APIs são ricos o suficiente para que eu não precise depender de uma ferramenta de terceiros para realizar esse tipo de análise?

Foi útil?

Solução 2

Obrigado Greg e Dave pelo feedback.Não era bem o que eu procurava, mas decidi dedicar um pouco de tempo pesquisando e descobri esta solução rápida e suja:

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

Provavelmente gastarei mais tempo nisso para poder encontrar rapidamente os arquivos e números de linha das definições e referências das funções;essas informações estão sendo coletadas, mas não exibidas.

Outras dicas

Você pode experimentar o Detector de Código Morto de Sebastian Bergmann:

phpdcd é um Dead Code Detector (DCD) para código PHP.Ele verifica um projeto PHP em busca de todas as funções e métodos declarados e relata aqueles como sendo "código morto" que não são chamados pelo menos uma vez.

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

Observe que é um analisador de código estático, portanto pode fornecer falsos positivos para métodos que são chamados apenas dinamicamente, por exemplo.não consegue detectar $foo = 'fn'; $foo();

Você pode instalá-lo via PEAR:

pear install phpunit/phpdcd-beta

Depois disso você pode usar as seguintes opções:

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.

Mais ferramentas:


Observação: de acordo com o aviso do repositório, este projeto não é mais mantido e seu repositório é mantido apenas para fins de arquivamento.Portanto, sua milhagem pode variar.

Este script bash pode ajudar:

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

Isso basicamente busca recursivamente o diretório atual para definições de função, passa os hits para awk, que forma um comando para fazer o seguinte:

  • imprima o nome da função
  • grep recursivamente para isso novamente
  • canalizando essa saída para grep -v para filtrar as definições de função, de modo a reter chamadas para a função
  • canaliza esta saída para wc -l que imprime a contagem de linhas

Este comando é então enviado para execução no bash e a saída é recebida como 0, o que indicaria 0 chamadas para a função.

Observe que isso irá não resolva o problema que Calebbrown cita acima, então pode haver alguns falsos positivos na saída.

USO: find_unused_functions.php <diretório_raiz>

OBSERVAÇÃO:Esta é uma abordagem “rápida e suja” para o problema.Este script executa apenas uma passagem lexical sobre os arquivos e não respeita situações em que módulos diferentes definem funções ou métodos com nomes idênticos.Se você usar um IDE para o desenvolvimento de PHP, ele poderá oferecer uma solução mais abrangente.

Requer PHP 5

Para economizar, copiar e colar, um download direto e quaisquer novas versões são disponivel aqui.

#!/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 */

Se bem me lembro você pode usar phpCallGraph fazer isso.Ele irá gerar um belo gráfico (imagem) para você com todos os métodos envolvidos.Se um método não estiver conectado a nenhum outro, é um bom sinal de que o método é órfão.

Aqui está um exemplo: classGallerySystem.png

O método getKeywordSetOfCategories() é órfão.

A propósito, você não precisa tirar uma imagem - o phpCallGraph também pode gerar um arquivo de texto ou um array PHP, etc.

Como as funções/métodos PHP podem ser invocados dinamicamente, não há maneira programática de saber com certeza se uma função nunca será chamada.

A única maneira certa é através da análise manual.

Atualização 2019+

Eu me inspirei em A resposta de Andrei e transformei isso em uma detecção padrão de codificação.

A detecção é muito simples, mas poderosa:

  • encontra todos os métodos public function someMethod()
  • então encontre todas as chamadas de método ${anything}->someMethod()
  • e simplesmente relata aquelas funções públicas que nunca foram convocadas

Isso me ajudou a remover sobre Mais de 20 métodos Eu teria que manter e testar.


3 etapas para encontrá-los

Instale o ECS:

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

Configurar ecs.yaml configuração:

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

Execute o comando:

vendor/bin/ecs check src

Veja os métodos relatados e remova aqueles que você não considera úteis 👍


Você pode ler mais sobre isso aqui: Remova métodos públicos mortos do seu código

afaik não tem como.Para saber quais funções "pertencem a quem" você precisaria executar o sistema (pesquisa de função de ligação tardia em tempo de execução).

Mas as ferramentas de refatoração são baseadas na análise estática de código.Gosto muito de linguagens de tipo dinâmico, mas ao meu ver são difíceis de escalar.A falta de refatorações seguras em grandes bases de código e linguagens de tipo dinâmico é uma grande desvantagem para a manutenção e o tratamento da evolução do software.

phpref identificará de onde as funções são chamadas, o que facilitaria a análise - mas ainda há um certo esforço manual envolvido.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top