¿Cómo puedo encontrar funciones no utilizadas en un proyecto PHP?

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

  •  08-06-2019
  •  | 
  •  

Pregunta

¿Cómo puedo encontrar funciones no utilizadas en un proyecto PHP?

¿Existen funciones o API integradas en PHP que me permitan analizar mi base de código, por ejemplo? Reflexión, token_get_all()?

¿Estas API tienen suficientes funciones como para no tener que depender de una herramienta de terceros para realizar este tipo de análisis?

¿Fue útil?

Solución 2

Gracias Greg y Dave por los comentarios.No era exactamente lo que estaba buscando, pero decidí dedicar un poco de tiempo a investigarlo y se me ocurrió esta solución rápida y sucia:

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

Probablemente le dedicaré más tiempo para poder encontrar rápidamente los archivos y números de línea de las definiciones y referencias de funciones;Esta información se recopila, pero no se muestra.

Otros consejos

Puedes probar el Detector de códigos muertos de Sebastian Bergmann:

phpdcd es un detector de código muerto (DCD) para código PHP.Escanea un proyecto PHP en busca de todas las funciones y métodos declarados y los informa como "código muerto" que no se llama al menos una vez.

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

Tenga en cuenta que es un analizador de código estático, por lo que podría dar falsos positivos para métodos que solo llamaron dinámicamente, por ejemplo.no puede detectar $foo = 'fn'; $foo();

Puedes instalarlo a través de PEAR:

pear install phpunit/phpdcd-beta

Después de eso puedes usar con las siguientes opciones:

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.

Más herramientas:


Nota: según el aviso del repositorio, este proyecto ya no se mantiene y su repositorio solo se conserva con fines de archivo.Así que su millaje puede variar.

Este fragmento de scripting bash podría ayudar:

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

Básicamente, esto busca recursivamente el directorio actual para las definiciones de funciones, pasa los resultados a awk, que forma un comando para hacer lo siguiente:

  • imprimir el nombre de la función
  • búsquelo recursivamente de nuevo
  • canalizar esa salida a grep -v para filtrar las definiciones de funciones y así retener las llamadas a la función
  • canaliza esta salida a wc -l que imprime el recuento de líneas

Luego, este comando se envía para su ejecución a bash y la salida se registra en 0, lo que indicaría 0 llamadas a la función.

Tenga en cuenta que esto no resuelva el problema que calebbrown cita anteriormente, por lo que puede haber algunos falsos positivos en el resultado.

USO: find_unused_functions.php <directorio_raíz>

NOTA:Éste es un enfoque “rápido y sucio” del problema.Este script solo realiza un paso léxico sobre los archivos y no respeta situaciones en las que diferentes módulos definen funciones o métodos con nombres idénticos.Si utiliza un IDE para su desarrollo PHP, puede ofrecer una solución más completa.

Requiere PHP 5

Para guardarle una copia y pegado, una descarga directa y cualquier versión nueva, son disponible aquí.

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

Si no recuerdo mal puedes usar phpCallGraph Para hacer eso.Generará un bonito gráfico (imagen) con todos los métodos involucrados.Si un método no está conectado a ningún otro, es una buena señal de que el método está huérfano.

He aquí un ejemplo: claseGallerySystem.png

El método getKeywordSetOfCategories() queda huérfano.

Por cierto, no es necesario tomar una imagen: phpCallGraph también puede generar un archivo de texto, o una matriz PHP, etc.

Debido a que las funciones/métodos PHP se pueden invocar dinámicamente, no existe una forma programática de saber con certeza si nunca se llamará una función.

La única forma segura es mediante el análisis manual.

Actualización 2019+

Me inspiré en la respuesta de andrey y convertí esto en un olfateo estándar de codificación.

La detección es muy simple pero poderosa:

  • encuentra todos los métodos public function someMethod()
  • luego busque todas las llamadas a métodos ${anything}->someMethod()
  • y simplemente informa aquellas funciones públicas que nunca fueron convocadas

Me ayudó a eliminar encima Más de 20 métodos Tendría que mantener y probar.


3 pasos para encontrarlos

Instalar ECS:

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

Configuración ecs.yaml configuración:

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

Ejecute el comando:

vendor/bin/ecs check src

Vea los métodos informados y elimine aquellos que no considere útiles 👍


Puedes leer más sobre esto aquí: Elimine los métodos públicos muertos de su código

afaik no hay manera.Para saber qué funciones "pertenecen a quién", necesitaría ejecutar el sistema (búsqueda de función de enlace tardío en tiempo de ejecución).

Pero las herramientas de refactorización se basan en el análisis de código estático.Me gustan mucho los lenguajes de escritura dinámica, pero en mi opinión son difíciles de escalar.La falta de refactorizaciones seguras en grandes bases de código y lenguajes de tipo dinámico es un inconveniente importante para la mantenibilidad y el manejo de la evolución del software.

phpxref identificará desde dónde se llaman las funciones que facilitarían el análisis, pero todavía hay una cierta cantidad de esfuerzo manual involucrado.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top