PHP

モノタロウさんがすごいなと思ったのでphp-astを試してみた

背景・概要

20万行超のコードベースをテストせずにリファクタリングリリースした話 を読んで、面白いなあ、これPHPだとどうなってるんだろう・・・と思って調べてみました。

php-ast というのがあるらしい

PHP AST 徹底解説 のような発表があったので、とりあえずそれを使ってみることにした。

インストール

$ git clone https://github.com/nikic/php-ast
$ cd php-ast
$ phpize
Configuring for:
PHP Api Version:         20170718
Zend Module Api No:      20170718
Zend Extension Api No:   320170718

ここで自分の環境(phpbrewでインストールしたての7.2.11, CentOS7.5)では下記の設定がされておらずエラーになったのでしておいた。

export LANGUAGE="ja_JP:ja"
export LC_ALL="ja_JP.UTF-8"
export LANG="ja_JP.UTF-8"

あとはいつもどおりに...(phpbrewでのインストールなのでインストール先は一旦.phpbrew以下に)

$ ./configure
(省略)
$ make
(省略)
$ sudo make install
Installing shared extensions:     /home/vagrant/.phpbrew/php/php-7.2.11/lib/php/extensions/no-debug-non-zts-20170718/

php.ini ファイルを探して

$ php --ini
Configuration File (php.ini) Path: /home/vagrant/.phpbrew/php/php-7.2.11/etc
Loaded Configuration File:         /home/vagrant/.phpbrew/php/php-7.2.11/etc/php.ini
Scan for additional .ini files in: /home/vagrant/.phpbrew/php/php-7.2.11/var/db
Additional .ini files parsed:      (none)

ast.so を追加。

php.ini
;extension=soap
;extension=sockets
;extension=sqlite3
;extension=tidy
;extension=xmlrpc
;extension=xsl
extension=ast.so

;;;;;;;;;;;;;;;;;;;
; Module Settings ;
;;;;;;;;;;;;;;;;;;;

とりあえず試す

test.php
<?php

$ast = ast\parse_code('<?php echo "ok"; ?>', $version=50);
var_dump($ast);
object(ast\Node)#1 (4) {
  ["kind"]=>
  int(132)
  ["flags"]=>
  int(0)
  ["lineno"]=>
  int(1)
  ["children"]=>
  array(1) {
    [0]=>
    object(ast\Node)#2 (4) {
      ["kind"]=>
      int(282)
      ["flags"]=>
      int(0)
      ["lineno"]=>
      int(1)
      ["children"]=>
      array(1) {
        ["expr"]=>
        string(2) "ok"
      }
    }
  }
}

クラスファイルに対して適用してみる+ast_dumpを試す

ast_dump を試す。

まずはサンプルにあるように

<?php
// clone したときのフォルダがあればそれを使う
require_once 'php-ast/util.php';

$ast = ast\parse_code('<?php echo "ok"; ?>', $version=50);
echo ast_dump($ast) . PHP_EOL;

こんな結果に。この結果をmd5とかしておけばいいのですね。

AST_STMT_LIST
    0: AST_ECHO
        expr: "ok"

クラスファイルに対して適用

http://php.net/manual/ja/language.oop5.basic.php のクラスをやってみよう。

SimpleClass.php
<?php
class SimpleClass
{
    // プロパティの宣言
    public $var = 'a default value';

    // メソッドの宣言
    public function displayVar() {
        echo $this->var;
    }
}
?>

こちらのプログラムに流してみる

<?php
require_once 'php-ast/util.php';

$file = $argv[1];
$ast = ast\parse_file($file, $version=50);
echo ast_dump($ast) . PHP_EOL;
AST_STMT_LIST
    0: AST_CLASS
        flags: 0
        name: "SimpleClass"
        docComment: null
        extends: null
        implements: null
        stmts: AST_STMT_LIST
            0: AST_PROP_DECL
                flags: MODIFIER_PUBLIC (256)
                0: AST_PROP_ELEM
                    name: "var"
                    default: "a default value"
                    docComment: null
            1: AST_METHOD
                flags: MODIFIER_PUBLIC (256)
                name: "displayVar"
                docComment: null
                params: AST_PARAM_LIST
                uses: null
                stmts: AST_STMT_LIST
                    0: AST_ECHO
                        expr: AST_PROP
                            expr: AST_VAR
                                name: "this"
                            prop: "var"
                returnType: null
                __declId: 0
        __declId: 1

クラスに変更を加えて確認してみる

  • クラスにPHPDocのパーツを追加
  • 行コメントの削除
  • {} の位置を変更
  • returnを追加
  • 引数を追加
SimpleClass.php
<?php
/**
 * SimpleClass
 */
class SimpleClass {
    public $var = 'a default value';

    public function displayVar(int $n) {
        echo $this->var;
        return;
    }
}

結果はこのように。

AST_STMT_LIST
    0: AST_CLASS
        flags: 0
        name: "SimpleClass"
        docComment: "/**
         * SimpleClass
         */"
        extends: null
        implements: null
        stmts: AST_STMT_LIST
            0: AST_PROP_DECL
                flags: MODIFIER_PUBLIC (256)
                0: AST_PROP_ELEM
                    name: "var"
                    default: "a default value"
                    docComment: null
            1: AST_METHOD
                flags: MODIFIER_PUBLIC (256)
                name: "displayVar"
                docComment: null
                params: AST_PARAM_LIST
                uses: null
                stmts: AST_STMT_LIST
                    0: AST_ECHO
                        expr: AST_PROP
                            expr: AST_VAR
                                name: "this"
                            prop: "var"
                    1: AST_RETURN
                        expr: null
                returnType: null
                __declId: 0
        __declId: 1
  • doCcomment が null だったのが内容が追加された
  • AST_PARAM, AST_RETURN のあたりが変更になった

ことがわかります。

docComment以外のコメント、動作に変更のないような修正は、元の記事のように同一とみなして良いといえそうです。

まとめ

ast_check.php
<?php
require_once 'php-ast/util.php';

$file = $argv[1];
$tmp = array_shift($argv);

$files = $argv;
foreach ($files as $file) {
    $ast = ast\parse_file($file, $version=50);
    printf("%s: %s\n", md5(ast_dump($ast)), $file);
}

このようなプログラムを使って、

$ php ast_check.php A.php B.php C.php
d436cc6a6ab4ad36a6150d6f11e4bbcd: A.php
d436cc6a6ab4ad36a6150d6f11e4bbcd: B.php
d6d0e62c871c073620aa13a739fa2638: C.php

md5ハッシュ値のチェックができるようになりました。