PHP-Parser で PHP5からPHP7で動くコードに自動修正するツールを作る夢をみた

PHP-Parser で PHP5からPHP7で動くコードに自動修正するツールを作る夢をみた

2020-12-2934 min read

目次

  1. 概要
  2. php-parser-について
  3. php-parser-の簡単なサンプル
  4. astオブジェクトの置換変更
  5. php5からphp7への変更内容を実装する
  6. 結果
  7. 所感
  8. 参考にしたサイト

概要

PHP5からPHP7への移行ツールを作るための解析・自動修正ツールを調べる の続きです。 PHP-Parser を利用して PHP5でしか動作しないコードをPHP7で動作するコードに再生成するための夢のような自動変換ツールが作れないか考えてみました。

ツールが備える機能条件

ここで、「PHP5でしか動作しないコードをPHP7で動作するコードに再生成するためのツール」が備える機能の条件を以下としています。

  • 非推奨/廃止となった関数の置換
  • 関数/変数/クラス/例外クラスの置換
  • ファイルパスの修正
  • 引数の追加・変更
  • 型の判定・キャスト (例えばPHP5のcount()の場合、count(false)だと1、count(null)だと0になるがphp7ではwarning)

PHP-Parser について

This is a PHP 5.2 to PHP 8.0 parser written in PHP. Its purpose is to simplify static code analysis and manipulation.

nikic/PHP-Parser https://github.com/nikic/PHP-Parser

PHP5.2からPHP8までに対応したパーサであり。静的解析や操作を行うためのツールです。

こんな特徴があると説明されています。

  • PHP 5、PHP 7、およびPHP 8コードを抽象構文木(AST)に解析
  • ASTを可読形式で吐き出し
  • ASTをPHPコードに再生成
  • ASTを走査および変更するための基礎的なツール
  • 名前空間名の解決
  • 定数式の評価
  • コード生成のためのAST構築を簡素化するビルダーを備える
  • ASTをJSONに変換して戻す

PHP-Parser の簡単なサンプル

まずは簡単なサンプルを紹介します。

<?php
$code = <<<EOT
<?php
if ($var === true) {
    echo "hello";
}
EOT;
$parser = (new PhpParser\ParserFactory)->create(PhpParser\ParserFactory::PREFER_PHP7);
$stmts = $parser->parse($code);
// var_dump($stmts);
$printer = new PhpParser\PrettyPrinter\Standard();
$result = $printer->prettyPrintFile($stmts);
// echo $result;

このサンプルコードはphpファイルを読み込んでASTに分解した後、再度コードを生成するソースです。

この時変数 $stmts はパースされたコードに関する情報を保持する次のようなオブジェクトです。

array(1) {
  [0]=>
  object(PhpParser\Node\Stmt\If_)#1149 (5) {
    ["cond"]=>
    object(PhpParser\Node\Expr\BinaryOp\Identical)#1146 (3) {
      ["left"]=>
      object(PhpParser\Node\Expr\Variable)#1143 (2) {
        ["name"]=>
        string(3) "var"
        ["attributes":protected]=>
        array(2) {
          ["startLine"]=>
          int(2)
          ["endLine"]=>
          int(2)
        }
      }
      ["right"]=>
      object(PhpParser\Node\Expr\ConstFetch)#1145 (2) {
        ["name"]=>
        object(PhpParser\Node\Name)#1144 (2) {
          ["parts"]=>
          array(1) {
            [0]=>
            string(4) "true"
          }
          ["attributes":protected]=>
          array(2) {
            ["startLine"]=>
            int(2)
            ["endLine"]=>
            int(2)
          }
        }
        ["attributes":protected]=>
        array(2) {
          ["startLine"]=>
          int(2)
          ["endLine"]=>
          int(2)
        }
      }
      ["attributes":protected]=>
      array(2) {
        ["startLine"]=>
        int(2)
        ["endLine"]=>
        int(2)
      }
    }
    ["stmts"]=>
    array(1) {
      [0]=>
      object(PhpParser\Node\Stmt\Echo_)#1148 (2) {
        ["exprs"]=>
        array(1) {
          [0]=>
          object(PhpParser\Node\Scalar\String_)#1147 (2) {
            ["value"]=>
            string(5) "hello"
            ["attributes":protected]=>
            array(3) {
              ["startLine"]=>
              int(3)
              ["endLine"]=>
              int(3)
              ["kind"]=>
              int(2)
            }
          }
        }
        ["attributes":protected]=>
        array(2) {
          ["startLine"]=>
          int(3)
          ["endLine"]=>
          int(3)
        }
      }
    }
    ["elseifs"]=>
    array(0) {
    }
    ["else"]=>
    NULL
    ["attributes":protected]=>
    array(2) {
      ["startLine"]=>
      int(2)
      ["endLine"]=>
      int(4)
    }
  }
}

PhpParser\PrettyPrinter\Standard::prettyPrintFile()によってパースされたオブジェクトからコードを再生成することができます。

コードの自動修正ツールは、パースされたオブジェクトに変更を加えコードを再生成を行うことでコードの自動修正が行う仕組みを利用して実装します。

ASTオブジェクトの置換・変更

先程のサンプルコードを拡張してソースコードを書き換える処理を作ります。 次のサンプルは hello という文字列が存在したら hello world に置換するコードです。

<?php
$code = <<<EOT
<?php
if ($var === true) {
    echo 'hello';
}
EOT;

class HelloWorldNodeVisitor extends NodeVisitorAbstract
{
    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Scalar\String_ && $node->value === 'hello') {
            $node->value = 'hello world';
        }
    }
}

$parser = (new PhpParser\ParserFactory)->create(PhpParser\ParserFactory::PREFER_PHP7);
$stmts = $parser->parse($code);
$traverser = new NodeTraverser;
$traverser->addVisitor(new HelloWorldNodeVisitor);
$stmts = $traverser->traverse($stmts);
$printer = new PhpParser\PrettyPrinter\Standard();
$result = $printer->prettyPrintFile($stmts);
// <?php
// if ($var === true) {
//     echo 'hello world';
// }

NodeTraverserオブジェクトにNodeVisitorインタフェースを実装したオブジェクトをセットすることでASTオブジェクトを走査する際に書き換え処理を行います。 上記のサンプルコードではNodeVisitorインタフェースを実装したNodeVisitorAbstractクラスを継承したオブジェクトを利用しています。

PHP5からPHP7への変更内容を実装する

次のようなサンプルコードを用意しました。これはphpのバージョンを5から7へ変更した際に問題となる部分が要素が含まれているコードです。

<?php
//ex1 includeするパスを path/to/foo から path/to/bar に変更する
include("path/to/foo");
include_once("path/to/foo");
require("path/to/foo");
require_once("path/to/foo");
// include("path/to/foo");
$include = "path/to/foo";

//ex2 catchする例外クラスを Exception から Throwable に変更
try {
    $callable = function (Exception $obj) {
        var_dump($obj);
        return true;
    };
} catch (Exception $err) {
    print_r($err);
}

//ex3 廃止関数の置換
{
    $str = "123";
    ereg("/[0-9]/", $str);
    call_user_method('strCount', $str);
    mysql_connect("host", "user", "password");
}

//ex4 クラス名(宣言・継承)の変更
class Int {}
class Num extends Int {}

//ex5 引数の調整
$var1 = null;
$num = count($var1);
$var2 = false;
$num = count($var2);
$var3 = array();
$num = count($var3, COUNT_RECURSIVE);

function toArrayForCount($arg): array
{
    if (is_countable($arg)) {
        return $arg;
    }
    if (is_null($arg)) {
        return [];
    }
    if ($arg === false) {
        return [1];
    }
    return [];
}

ex1 includeパスを変更する

これはphp5→7とは関係ありませんが、ソースコード中のパスを一括変換したいと言う場面はあると思うので用意しました。 次のようなクラスを用意しました。

class IncludePathVisitor extends NodeVisitorAbstract
{
    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Expr\Include_) {
            if ($node->expr->value === "path/to/foo") {
                $node->expr->value = "path/to/bar";
            }
        }
    }
}

ex2 例外クラスを Exception から Throwable に

PHP5で存在したE_*エラーがPHP7からThrowableを継承するERRORクラスとなりました。 全ての例外を拾うため catch している ExceptionThrowable に変更する次のようなクラスを用意しました。

class StmtCatchVisitor extends NodeVisitorAbstract
{
    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Stmt\Catch_ && $node->types[0]->parts[0] === 'Exception') {
            $node->types[0]->parts[0] = 'Throwable';
        }
    }
}

ex3 廃止関数の置換

PHP7からいくつか廃止された菅すが存在します。 例えば、ereg_* call_user_method mysql_* です。

class FunctionReplaceVisitor extends NodeVisitorAbstract
{
    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Expr\FuncCall && $node->name->getLast() === 'ereg') {
            $node->name->parts = ['preg_match'];
        }
        if ($node instanceof Node\Expr\FuncCall && $node->name->getLast() === 'call_user_method') {
            $node->name->parts = ['call_user_func'];
        }
        if ($node instanceof Node\Expr\FuncCall && $node->name->getLast() === 'mysql_connect') {
            $node->name->parts = ['mysqli_connect'];
        }
    }
}

ex4 クラス名(宣言・継承)の変更

クラス名などでは予約語が利用できませんが、PHP7では新たに予約語が追加されました。 例えば、int float bool などです。これに対応するのは次のクラスです。 処理としては別の固定されたクラス名に置き換えているだけなので場合によっては考慮が必要です。

class ClassReplaceVisitor extends NodeVisitorAbstract
{
    public function leaveNode(Node $node)
    {
        if (!($node instanceof Node\Stmt\Class_)) {
            return;
        }
        if ($node->name->name === 'Int') {
            $node->name->name = 'IntObj';
        }
        if ($node->extends !== NULL && $node->extends->parts[0] === 'Int') {
            $node->extends->parts[0] = 'IntObj';
        }
    }
}

ex5 引数の調整

count関数はcountableな値以外は警告を発するようになりました。なのでcountableかどうかチェックする必要が出てきました。

count関数の引数にtoArrayForCountFuncで値を加工(=countableな値に変更)してphp5のcountと同様の動きをするように変更します(←どうかと思うが)

class CountFuncArgInjectionVisitor extends NodeVisitorAbstract
{
    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Expr\FuncCall && $node->name->getLast() === 'count') {
            $tmp = $node->args[0]->value;
            $func = new PhpParser\Node\Expr\FuncCall(
                new PhpParser\Node\Name('toArrayForCount')
            );
            $func->args = [
                new PhpParser\Node\Arg($tmp),
            ];
            $node->args[0] = $func;
        }
    }
}

結果

こうなりました。

<?php
//ex1 includeするパスを path/to/foo から path/to/bar に変更する
include("path/to/bar");
include_once("path/to/bar");
require("path/to/bar");
require_once("path/to/bar");
// include("path/to/foo");
$include = "path/to/foo";

//ex2 catchする例外クラスを Exception から Throwable に変更
try {
    $callable = function (Exception $obj) {
        var_dump($obj);
        return true;
    };
} catch (Throwable $err) {
    print_r($err);
}

//ex3 廃止関数の置換
{
    $str = "123";
    preg_match("/[0-9]/", $str);
    call_user_func('strCount', $str);
    mysqli_connect("host", "user", "password");
}

//ex4 クラス名(宣言・継承)の変更
class IntObj {}
class Num extends IntObj {}

//ex5 引数の調整
$var1 = null;
$num = count(toArrayForCount($var1));
$var2 = false;
$num = count(toArrayForCount($var2));
$var3 = array();
$num = count(toArrayForCount($var3), COUNT_RECURSIVE);

function toArrayForCount($arg): array
{
    if (is_countable($arg)) {
        return $arg;
    }
    if (is_null($arg)) {
        return [];
    }
    if ($arg === false) {
        return [1];
    }
    return [];
}

所感

自動変換する機構自体はそこそこ作れそうな感じでした。 しかしながら1つのルールに対しての実装コストが大きく静的解析ツール(PHPStanなど)を利用しながら手動で修正して行くのが無難かなと思った次第です。 ソースコードの規模が大きいほど、このようなやり方は効果を発揮するでしょう。

参考にしたサイト

nikic/PHP-Parser入門 https://qiita.com/gong023/items/4c8401e03d843fd15122

PHPでPHPを実装する https://blog.freedom-man.com/phphp

nikic/PHP-Parser を使ってPHPコードをパースして出来る事とサンプル https://qiita.com/yotsak/items/858cfb6ea7a7563d8059

Recommends
PHP-Parser で PHP5からPHP7で動くコードに自動修正するツールを作る夢をみ...
2020-12-29
php
phpstan
ast
PHP5からPHP7への移行ツールを作るための解析・自動修正ツールを調べる
2020-12-28
php
phpstan
ast
PostfixでメールリレーしてMailHogで受信する開発用Dockerコンテナの構築
2021-05-19
docker
postfix
centos
php-fpmのステータスページを表示 Apache & htaccess
2021-03-24
php
php%20fpm
apache
Homebrew で php7.4 + Xdebug をインストール
2021-02-01
php
xdebug
mac
PHP-FPM(php7.4) Apache2.4 on Ubutnu20.04 Webサ...
2021-01-19
php
apache
ubuntu
PHP-FPM(php7.4) Apache2.4 でWebサーバ構築 on CentOS...
2021-01-17
php
apache
centos
CentOS8 に PHP7.4 インストール
2021-01-17
php
apache
centos
UNIXドメインソケット通信 vs INETドメインソケット通信 php-fpmで動作させ...
2021-01-10
php
apache
nginx
Amazon S3 と ローカルファイルのチェックサムの比較
2020-07-24
amazon%20aws
linux
php
WordPressのDBから記事データを抽出する
2020-04-30
wordpress
mysql
blog
PHPerkaigi 2020 資料まとめ
2020-02-11
php
phperkaigi
Ubuntu18.04にApache MariaDB PHP7.2 をセットアップ
2019-07-29
amazon%20aws
php
linux
AWS S3のオブジェクト一覧をPHPで表示させる
2019-06-19
amazon%20aws
php
amazon%20s3
PHPで簡単ページング処理を実装する サンプルコード
2019-01-26
php
%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%8D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3
%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%B3%E3%82%B0
New Posts
[CDK]SNS+SQS+DynamoDBでBounceとComplaint情報を収集する...
2022-04-11
amazon%20aws
node.js
typescript
[AmazonSES] node.js と ejs を利用してEメールを送信する
2022-04-09
javascript
node.js
amazon%20aws
GatsbyからNext.jsへのサイト移行
2022-04-04
next.js
gatsby
amazon%20aws
[AWS CDK] Lambda で S3 オブジェクトを読み書きするStackの構築
2022-03-18
aws%20cdk
amazon%20aws
typescript
[AWS CDK] S3 + CloudFrontの構築とOriginAccessIden...
2022-03-09
amazon%20aws
aws%20cdk
typescript
[AWS CDK] Bastion(踏み台)構築。SSMとEC2InstanceConne...
2022-03-06
amazon%20aws
aws%20cdk
node.js
[AWS CDK] Cognito を構築
2022-03-04
amazon%20aws
aws%20cdk
node.js
AWS CDK v2 でVPC上にAPI Gateway + Lambda + RDS +...
2022-02-28
amazon%20aws
aws%20cdk
node.js
javascriptで累積和を解く
2022-02-27
%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0
%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%83%9F%E3%83%B3%E3%82%B0
atcoder
AWS Amplify で monorepo を導入し 単一リポジトリで複数プロジェクトを...
2022-02-25
git
github
amazon%20aws
AWS CDK v2 で Lambda関数のデプロイ
2022-02-23
typescript
amazon%20aws
aws%20cdk
NextJSでDevToysのようなものを作成した
2022-02-22
javascript
typescript
vercel
JSで動的計画法を利用して部分和問題を解く
2022-02-20
javascript
typescript
%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0
NestJSアプリケーションをwebpackでBundle
2022-02-20
javascript
typescript
nestjs
[Next.js] Warning: Assign arrow function to a...
2022-02-13
javascript
typescript
next.js
Hot posts!
Proxy環境下でcurlを実行する
2019-12-07
linux
curl
OpenCVのMatのタイプ一覧表
2018-11-25
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86
opencv
Macでも利用できるDBクライアント MySQL PostgreSQL Oracle など
2019-12-21
linux
%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9
mysql
TablePlusを使ってみる。シンプルでモダンなSQLクライアントツール
2018-09-30
%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9
DBクライアントツールはDBeaverをおすすめしたい
2021-03-08
oracle
mysql
sqlite
AWS S3のアクセスキーIDとシークレットアクセスキーの取得 作業用ユーザを作成
2019-06-12
amazon%20aws
linux
amazon%20s3
AtCoderで初めて色がつくまでの話(茶色) レートが中々上がらなかった原因
2018-11-25
%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0
%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0
%E9%9B%91%E8%AB%87
CentOS8でEPELとPowerToolsリポジトリの有効化
2020-11-30
centos
red%20hat
EPEL
Macでターミナルからポートスキャンを行う方法。
2018-12-09
linux
mac
apple
Python + OpenCVのfillConvexPolyで複雑なポリゴンを描画する
2018-11-27
python
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86
opencv
Date
▶︎
2022 年 (21)
▶︎
2021 年 (40)
▶︎
2020 年 (30)
▶︎
2019 年 (90)
▶︎
2018 年 (89)
▶︎
2017 年 (1)
Tags
javascript(92)
linux(47)
amazon%20aws(39)
%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0(36)
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86(30)
node.js(30)
html5(29)
centos(24)
php(23)
python(22)
%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0(20)
typescript(20)
canvas(18)
mac(18)
opencv(17)
mysql(17)
%E9%9B%91%E8%AB%87(15)
wordpress(15)
docker(14)
atcoder(13)
apache(12)
%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92(12)
%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9(12)
red%20hat(12)
ubuntu(11)
amazon%20s3(11)
github(10)
git(10)
vue.js(10)
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86100%E6%9C%AC%E3%83%8E%E3%83%83%E3%82%AF(10)
css3(8)
%E5%8F%AF%E8%A6%96%E5%8C%96(8)
%E5%B0%8F%E3%83%8D%E3%82%BF(8)
mariadb(8)
amazon%20lightsail(7)
react(7)
%E3%83%96%E3%83%AD%E3%82%B0(6)
cms(6)
oracle(6)
perl(6)
gitlab(6)
next.js(6)
aws%20cdk(6)
iam(5)
amazon%20ec2(5)
aws%20amplify(5)
curl(4)
webassembly(4)
ssh(4)
homebrew(4)
Author
s-yoshiki
s-yoshiki
githubzenntwitterqiita
ただの備忘録です。
JavaScript/TypeScript/node.js/React/AWS/OpenCV
※このブログの内容は個人の見解であり、所属する組織等の見解ではありません。