67

PHP プロジェクトで未使用の関数を見つけるにはどうすればよいですか?

自分のコードベースを分析できる機能や API がPHPに組み込まれていますtoken_get_all()か?

これらの API の機能は十分に豊富で、この種の分析を実行するためにサード パーティのツールに依存する必要はありませんか?

4

10 に答える 10

35

Sebastian Bergmann の Dead Code Detector を試すことができます。

phpdcdPHP コード用の Dead Code Detector (DCD) です。宣言されたすべての関数とメソッドについて PHP プロジェクトをスキャンし、少なくとも 1 回も呼び出されていない「デッド コード」として報告します。

ソース: 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.

その他のツール:


注:リポジトリの通知によると、このプロジェクトはもはや維持されておらず、そのリポジトリはアーカイブ目的でのみ保持されています。したがって、走行距離は異なる場合があります。

于 2011-01-25T09:50:51.017 に答える
25

フィードバックをくれたGregとDaveに感謝します。私が探していたものではありませんでしたが、少し時間をかけて調査することにし、この迅速で汚い解決策を思いつきました。

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

関数の定義と参照のファイルと行番号をすばやく見つけることができるように、おそらくもう少し時間を費やします。この情報は収集されており、表示されていません。

于 2008-08-18T13:47:41.860 に答える
21

この少しの bash スクリプトが役立つ場合があります。

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

これは基本的に、関数定義の現在のディレクトリを再帰的に grep し、ヒットを awk に渡します。これにより、次のことを行うコマンドが形成されます。

  • 関数名を出力する
  • 再帰的に再度 grep します
  • その出力を grep -v にパイプして、関数の呼び出しを保持するために関数定義を除外します
  • この出力を wc -l にパイプして、行数を出力します

次に、このコマンドは実行のために bash に送信され、出力は 0 に対して grep されます。これは、関数への呼び出しが 0 であることを示します。

これはcalebbrown が上で引用した問題を解決しないことに注意してください。

于 2012-04-02T15:43:35.823 に答える
9

使用法: find_unused_functions.php <root_directory>

注: これは、問題に対する「簡単な」アプローチです。このスクリプトは、ファイルに対してレキシカル パスのみを実行し、異なるモジュールが同じ名前の関数またはメソッドを定義する状況を考慮しません。PHP 開発に IDE を使用すると、より包括的なソリューションが提供される場合があります。

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 */
于 2011-08-20T16:57:40.640 に答える
3

私の記憶が正しければ、 phpCallGraphを使用してそれを行うことができます。関連するすべてのメソッドを使用して、適切なグラフ (画像) を生成します。メソッドが他のメソッドに接続されていない場合、それはそのメソッドが孤立していることを示す良い兆候です。

以下に例を示します: classGallerySystem.png

メソッドgetKeywordSetOfCategories()が孤立しています。

ところで、画像を取得する必要はありません。phpCallGraph は、テキスト ファイルや PHP 配列などを生成することもできます。

于 2009-03-29T02:12:51.910 に答える
3

PHP 関数/メソッドは動的に呼び出される可能性があるため、関数が呼び出されないかどうかをプログラムで確実に知る方法はありません。

唯一確実な方法は、手作業による分析です。

于 2010-06-01T00:38:22.250 に答える
2

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

報告されたメソッドを確認し、役に立たないメソッドを削除します


詳細については、コードからデッド パブリック メソッドを削除してください:

于 2019-03-16T11:55:43.177 に答える
0

phpxrefは、分析を容易にする関数がどこから呼び出されているかを特定しますが、それでもある程度の手作業が必要です。

于 2010-06-01T08:59:49.243 に答える
-1

仕方がありません。どの関数が「誰に属しているか」を知るには、システムを実行する必要があります (ランタイム レイト バインディング関数ルックアップ)。

しかし、リファクタリング ツールは静的コード分析に基づいています。私は動的型付け言語が本当に好きですが、私の見解では、それらは拡張が困難です。大規模なコードベースと動的型付き言語で安全なリファクタリングが欠如していることは、保守性とソフトウェアの進化への対応にとって大きな欠点です。

于 2010-06-01T21:19:14.670 に答える