質問
PHP プロジェクトで未使用の関数を見つけるにはどうすればよいですか?
コードベースを分析できるようにする機能や API が PHP に組み込まれていますか? たとえば 反射, token_get_all()
?
これらの API は、この種の分析を実行するためにサードパーティのツールに依存しなくても済むほど豊富な機能を備えていますか?
解決 2
フィードバックをくださった 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]);
}
}
}
?>
関数定義と参照のファイルと行番号をすぐに見つけられるように、おそらくもう少し時間をかけて作業するつもりです。この情報は収集中ですが、表示されていないだけです。
他のヒント
Sebastian Bergmann の Dead Code Detector を試すことができます。
phpdcd
PHP コード用のデッド コード ディテクタ (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.
その他のツール:
注記: リポジトリの通知に従って、 このプロジェクトはもう保守されておらず、そのリポジトリはアーカイブ目的のみに保持されています。. 。したがって、走行距離は異なる場合があります。
このちょっとした 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 が上で引用した問題を解決するため、出力に誤検知が含まれる可能性があります。
使用法: find_unused_functions.php <ルートディレクトリ>
注記:これは問題に対する「即効性のある」アプローチです。このスクリプトはファイルの字句の受け渡しのみを実行し、異なるモジュールが同じ名前の関数またはメソッドを定義する状況を考慮しません。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 */
私の記憶が正しければ、使用できます phpコールグラフ それをするために。関連するすべてのメソッドを含む素晴らしいグラフ (画像) が生成されます。メソッドが他のメソッドに接続されていない場合、それはメソッドが孤立していることを示す良い兆候です。
以下に例を示します。 クラスギャラリーシステム.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
報告されたメソッドを参照し、役に立たないメソッドを削除してください 👍
詳細については、ここで読むことができます。 死んだパブリック メソッドをコードから削除する
私の知る限り方法はありません。どの関数が「誰に属している」かを知るには、システムを実行する必要があります (ランタイム遅延バインディング関数の検索)。
ただし、リファクタリング ツールは静的コード分析に基づいています。私は動的型付け言語がとても好きですが、拡張するのが難しいと私は考えています。大規模なコードベースや動的型付け言語では安全なリファクタリングが欠如しているため、保守性やソフトウェアの進化に対応する上で大きな欠点となります。
phpxref 関数がどこから呼び出されているかを特定すると分析が容易になりますが、それでもある程度の手作業が必要になります。