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

# 背景・概要

[20万行超のコードベースをテストせずにリファクタリングリリースした話](https://tech-blog.monotaro.com/entry/2018/09/26/142451) を読んで、面白いなあ、これPHPだとどうなってるんだろう・・・と思って調べてみました。

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

[PHP AST 徹底解説](https://www.slideshare.net/do_aki/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 ;
;;;;;;;;;;;;;;;;;;;
```

## とりあえず試す

```php: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 のクラスをやってみよう。

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

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

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

```php:
<?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を追加
- 引数を追加

```php: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以外のコメント、動作に変更のないような修正は、元の記事のように同一とみなして良いといえそうです。

# まとめ

```php: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ハッシュ値のチェックができるようになりました。