🔲
PHPのtry-catchの握りつぶしを探すときにAIとASTを使ってみた
PHPのコードを見ているときに、たまたまtry-catchの握りつぶしを見かけたので、撲滅しようと思い対象を検索。
コード量が多くAIに調査依頼しても漏れがあったのでどうしようか?と調べたのが発端。
grep検索だと量が多いと大変だし、静的解析ツールで出せるはずなのだがパッとは出てこず。。。
空チェックくらいならASTライブラリを使えばいけるのでは?とやってみた話。
AST(抽象構文木)の説明
ソースコードの文法構造を木構造として表現したもの。
ライブラリを使うと簡単にパースした結果を返してくれる。
下準備
composerと下記のライブラリが必要。
composer require --dev nikic/php-parser
生成されたコード
<?php
require_once 'vendor/autoload.php';
use PhpParser\Error;
use PhpParser\Node;
use PhpParser\Node\Stmt\TryCatch;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;
use PhpParser\Lexer;
class EmptyCatchBlockDetector extends NodeVisitorAbstract
{
private array $violations = [];
private string $currentFile;
public function __construct(string $filePath)
{
$this->currentFile = $filePath;
}
public function enterNode(Node $node): void
{
if ($node instanceof TryCatch) {
foreach ($node->catches as $catch) {
// catchブロック内の実行可能なステートメントがないか確認
$executableStmts = array_filter($catch->stmts, function ($stmt) {
// コメントやnopステートメントは除外
return !($stmt instanceof Node\Stmt\Nop);
});
if (empty($executableStmts)) {
$this->violations[] = [
'file' => $this->currentFile,
'line' => $catch->getLine(),
'type' => $catch->types[0]->toString(),
];
}
}
}
}
public function getViolations(): array
{
return $this->violations;
}
}
function detectEmptyCatchBlocks(string $directory): array
{
$parserFactory = new ParserFactory();
$parser = $parserFactory->createForNewestSupportedVersion();
$violations = [];
// ディレクトリを再帰的に走査
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$filePath = $file->getRealPath();
$code = file_get_contents($filePath);
try {
$ast = $parser->parse($code);
if ($ast !== null) {
$detector = new EmptyCatchBlockDetector($filePath);
$traverser = new NodeTraverser();
$traverser->addVisitor($detector);
$traverser->traverse($ast);
$violations = array_merge($violations, $detector->getViolations());
}
} catch (Error $error) {
echo "Parse error in {$filePath}: {$error->getMessage()}\n";
}
}
}
return $violations;
}
// コマンドライン実行
if (php_sapi_name() === 'cli') {
if ($argc < 2) {
echo "Usage: php detect-empty-catch.php <directory>\n";
exit(1);
}
$targetDir = $argv[1];
if (!is_dir($targetDir)) {
echo "Error: Directory not found: {$targetDir}\n";
exit(1);
}
$violations = detectEmptyCatchBlocks($targetDir);
if (empty($violations)) {
echo "No empty catch blocks found.\n";
exit(0);
}
echo "Empty catch blocks detected:\n";
foreach ($violations as $violation) {
echo " {$violation['file']}:{$violation['line']} - Empty catch block for {$violation['type']}\n";
}
exit(1);
}
実行
php -d memory_limit=9G hoge.php /fuga/your-directory
コード量が多いとメモリ量上げないとエラー出るので注意!!
確認方法
実際に自分でも空のtry-catch入れてみてヒットするか?確認。
感想
最初はAIに検索をお願いしたがヒットしないものがあった。
(プロンプトが悪いとか、使ったLLMが悪いなどの要因もあると思う)
その場合は、抽出するためのコードを作成依頼した方がうまくいくこともあるという事例だと思う。
(他にも処理するテキストが多すぎる場合とかにも有効だと思う)
今回のようなものであれば、ASTを使っても割と短文で出来上がるのでおすすめです^^
あと、今回の用途だとRust言語で書いてとAIにお願いしたほうが、爆速になるのでいいのでは?と思いました。
(今回はPHPコードが対象だったので合わせてみました)
Discussion