🔲

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