Comment puis-je trouver des fonctions inutilisées dans un projet PHP

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

  •  08-06-2019
  •  | 
  •  

Question

Comment puis-je trouver des fonctions inutilisées dans un projet PHP ?

Existe-t-il des fonctionnalités ou des API intégrées à PHP qui me permettront d'analyser ma base de code - par exemple Réflexion, token_get_all()?

Ces API sont-elles suffisamment riches en fonctionnalités pour que je n'aie pas besoin de recourir à un outil tiers pour effectuer ce type d'analyse ?

Était-ce utile?

La solution 2

Merci Greg et Dave pour les commentaires.Ce n'était pas tout à fait ce que je cherchais, mais j'ai décidé de consacrer un peu de temps à mes recherches et j'ai trouvé cette solution rapide et simple :

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

Je vais probablement y consacrer un peu plus de temps pour pouvoir trouver rapidement les fichiers et les numéros de ligne des définitions et références des fonctions ;ces informations sont collectées, mais ne sont pas affichées.

Autres conseils

Vous pouvez essayer le détecteur de code mort de Sebastian Bergmann :

phpdcd est un détecteur de code mort (DCD) pour le code PHP.Il analyse un projet PHP pour toutes les fonctions et méthodes déclarées et signale celles qui sont du « code mort » qui ne sont pas appelées au moins une fois.

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

Notez qu'il s'agit d'un analyseur de code statique, il peut donc donner des faux positifs pour les méthodes qui n'appellent que dynamiquement, par ex.il ne peut pas détecter $foo = 'fn'; $foo();

Vous pouvez l'installer via PEAR :

pear install phpunit/phpdcd-beta

Après cela, vous pouvez utiliser les options suivantes :

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.

Plus d'outils :


Note: conformément à l'avis du référentiel, ce projet n'est plus maintenu et son référentiel n'est conservé qu'à des fins d'archivage.Votre kilométrage peut donc varier.

Ce petit script bash pourrait aider :

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

Cela récupère de manière récursive le répertoire actuel pour les définitions de fonctions, transmet les hits à awk, qui forme une commande pour effectuer les opérations suivantes :

  • imprimer le nom de la fonction
  • récursivement, récupérez-le à nouveau
  • canaliser cette sortie vers grep -v pour filtrer les définitions de fonction afin de conserver les appels à la fonction
  • dirige cette sortie vers wc -l qui imprime le nombre de lignes

Cette commande est ensuite envoyée pour exécution à bash et la sortie est récupérée pour 0, ce qui indiquerait 0 appel à la fonction.

Notez que ce sera pas résolvez le problème cité par Calebbrown ci-dessus, il peut donc y avoir des faux positifs dans la sortie.

USAGE: find_unused_functions.php <root_directory>

NOTE:Il s’agit d’une approche « rapide et sale » du problème.Ce script effectue uniquement une passe lexicale sur les fichiers, et ne respecte pas les situations où différents modules définissent des fonctions ou des méthodes portant le même nom.Si vous utilisez un IDE pour votre développement PHP, il peut offrir une solution plus complète.

Nécessite PHP5

Pour vous enregistrer un copier-coller, un téléchargement direct et toute nouvelle version, sont disponible ici.

#!/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 je me souviens bien, vous pouvez utiliser phpCallGraph pour faire ça.Il générera pour vous un joli graphique (image) avec toutes les méthodes impliquées.Si une méthode n’est connectée à aucune autre, c’est un bon signe qu’elle est orpheline.

Voici un exemple : classGallerySystem.png

La méthode getKeywordSetOfCategories() est orphelin.

En passant, vous n'êtes pas obligé de prendre une image -- phpCallGraph peut également générer un fichier texte, ou un tableau PHP, etc.

Étant donné que les fonctions/méthodes PHP peuvent être invoquées dynamiquement, il n'existe aucun moyen programmatique de savoir avec certitude si une fonction ne sera jamais appelée.

Le seul moyen sûr est l'analyse manuelle.

2019+ Mise à jour

J'ai été espionné par La réponse d'Andrey et l'a transformé en un sniff standard de codage.

La détection est très simple mais puissante :

  • trouve toutes les méthodes public function someMethod()
  • puis trouvez tous les appels de méthode ${anything}->someMethod()
  • et simplement rapporte ces fonctions publiques qui n'ont jamais été convoquées

Cela m'a aidé à supprimer sur 20+ méthodes Il faudrait que je maintienne et teste.


3 étapes pour les trouver

Installez ECS :

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

Installation ecs.yaml configuration :

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

Exécutez la commande :

vendor/bin/ecs check src

Consultez les méthodes signalées et supprimez celles qui ne vous semblent pas utiles 👍


Vous pouvez en savoir plus ici : Supprimez les méthodes publiques mortes de votre code

autant que je sache, il n'y a aucun moyen.Pour savoir quelles fonctions "appartiennent à qui", vous devez exécuter le système (recherche de fonction de liaison tardive à l'exécution).

Mais les outils de refactoring sont basés sur une analyse de code statique.J'aime beaucoup les langages typés dynamiques, mais à mon avis, ils sont difficiles à mettre à l'échelle.Le manque de refactorisations sécurisées dans les grandes bases de code et les langages typés dynamiques est un inconvénient majeur pour la maintenabilité et la gestion de l'évolution des logiciels.

phpxréf identifiera d'où les fonctions sont appelées, ce qui faciliterait l'analyse - mais il y a encore un certain effort manuel impliqué.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top