2019-09-15   fd4dc76f

【PHPMD 対応】husky & lint-staged で CI を実行する

みなさんこんにちは、今回は husky と lint-staged を使って CI 環境をローカルに構築する方法を紹介したいと思います。

    継続的インテグレーション(CI)とは

    WikiPedia
    継続的インテグレーション(CI)とは、ソフトウェア開発においてソースコードの品質や保守性を保ち、持続可能な開発を続けていくための方法です。 つまり、ほかの人が見ても理解しやすく、また自分が書いた古いコードでも処理内容が理解できるようにプログラミングしていくということですね。

    個人の趣味のスクリプトであれば CI を導入する必要はないかもしれませんが、お仕事などでのチームでの開発では重要になると思います。 また GitHub で公開するものに対しても、CI を導入するとメリットが大きいと思います。

    ソースコードの属人性(その人にしか分からないような書き方)を解消するために、開発ルールを定めることがあります。 たとえば、コーディングスタイルを定義したり(Google C++ Style Guide など)、コードレビューをするなどです。 これらは整形ツールやコーディングチェック(lint)を使って自動化できます。

    サーバレスで CI を導入

    CI を導入する方法として、CI サーバを立てる方法があります。 たとえば、GitHub では Travis CI を導入することで、git commit 時に自動で CI を実行してくれます。 さらに、CI の実行結果を GitHub のバッジ として公言でき、リポジトリの質を保証する 1 つの指標にできます。

    しかし、CI サービスは有料プランのものもあり(Public リポジトリは無料も)、特に社内開発ではコストになる場合があります。 そこで、CI をローカル環境に導入し、git commit 時に自動でコーディングチェックと自動整形をしたいと思います。

    huskey と lint-staged を使うメリット

    コミット時に CI スクリプトを実行するには pre-commit を自前で記述することも可能です。 しかし、ほかの人と共同開発する場合では手動で pre-commit および自動整形ツール(prettier や eslint)を導入する必要がありました。

    そこで、husky を使って pre-commit をフックし、lint-staged でコーディングチェックと自動整形を実行させたいと思います。 リポジトリ直下に npm で huskylint-stagedprettiereslint などをインストールすれば、以降は npm install だけで CI 環境が整います。

    CI 環境の導入

    npm で husky と lint-staged、prettier、eslint をインストールします。

    $ npm install --save-dev husky lint-staged prettier eslint @prettier/plugin-php
    

    .huskyrc.json を用意し、pre-commit のフックに lint-staged を指定する設定を書きます。

    {
      "hooks": {
        "pre-commit": "lint-staged"
      }
    }
    

    今度は .lintstagedrc.json を用意し、ファイルごとに実行する CI を定義します。

    {
      "linters": {
        "*.php": [
          "prettier --write",
          "git add"
        ],
        "*.{js,jsx,ts,tsx}": [
          "eslint --fix",
          "git add"
        ],
        "*.{css,scss}": [
          "prettier --write",
          "git add"
        ],
        "*.{yaml,yml,md}": [
          "prettier --write",
          "git add"
        ]
      }
    }
    

    最後に、prettier や eslint の設定ファイルを設置すれば完了です。 .eslintrc.jseslint --init で作るのもよいと思います。

    • .prettierrc
    {
      printWidth: 120,
      tabWidth: 2
    }
    
    $ node_modules/.bin/eslint --init
    

    CI を試してみる

    試しに test.php を作ってみます。

    <?php
      $test=1;
    while(1){
              test();
    }
    

    こんなやる気ナッシングなコードでも、git add してコミットすると、

    $ git add .
    $ git commit -m "PHP の prettier テスト。"
    husky > pre-commit (node v8.9.1)
      ↓ Stashing changes... [skipped]
        → No partially staged files found...
      ✔ Running linters...
    [master 78d95f6] PHP の prettier テスト。
     1 file changed, 4 insertions(+), 4 deletions(-)
    
    • test.php
    <?php
    $test = 1;
    while (1) {
      test();
    }
    

    きれいになりました。 JavaScript だと、

    const test = 0;
    test = "This is const. ";
    test;
    
    $ git add .
    $ git commit -m "JavaaScript の eslint テスト。"
    husky > pre-commit (node v8.9.1)
      ↓ Stashing changes... [skipped]
        → No partially staged files found...
      ❯ Running linters...
        ↓ Running tasks for *.php [skipped]
          → No staged files match *.php
        ❯ Running tasks for *.{js,jsx,ts,tsx}
          ✖ eslint --fix
            git add
        ↓ Running tasks for *.{css,scss} [skipped]
          → No staged files match *.{css,scss}
        ↓ Running tasks for *.{yaml,yml,md} [skipped]
          → No staged files match *.{yaml,yml,md}
    
    
    
    ✖ eslint --fix found some errors. Please fix them and try committing again.
    
    /home/hato/repo/test.js
    2:1  error  'test' is constant  no-const-assign
    
    ✖ 1 problem (1 error, 0 warnings)
    
    husky > pre-commit hook failed (add --no-verify to bypass)
    

    このように const のエラーでコミットをやめてくれます。

    PHPMD を使うには?

    PHP の静的解析ツールとして PHPMD が有名です。 スタイルチェックだけでなくプログラムの意味解析を行い、保守性を確保するためのアドバイスをしてくれます。 PHPMD のインストールには Composer を利用します。

    $ composer require phpmd/phpmd
    

    肝心の phpmd を lint-staged で使う方法ですが……実は相性が悪いです。 まず、phpmd のコマンド書式より、次のようなコマンドで phpmd を実行できます。

    $ phpmd test.php,test2.php text cleancode,codesize,controversial,design,naming,unusedcode
    

    このように、チェックする php ファイルを第 1 引数にカンマ区切りで指定します。 それに対し、lint-staged では次のようにコマンドが展開されます。

    $ phpmd test.php test2.php
    

    つまり、php ファイルをコマンド末尾にスペース区切りで指定してしまうのです。 そこで、phpmd と lint-staged をうまくつなぎ合わせるシェルスクリプトを書きます。

    • .lintstaged-phpmd
    #!/bin/bash
    targets=`echo "$@" | sed "s/ /,/g"`
    vendor/bin/phpmd ${targets} text cleancode,codesize,controversial,design,naming,unusedcode
    
    • .lintstagedrc.json
    {
      "linters": {
        "*.php": [
          "prettier --write",
          "./.lintstaged-phpmd",
          "git add"
        ]
      }
    }
    

    エラーが出る php ファイルを 2 つ用意して、

    • test.php
    <?php
    $test = 1;
    while (1) {
      test();
    }
    class not_camel_case
    {
    }
    
    • test2.php
    <?php
    class not_camel_case
    {
    }
    

    コミットしてみます。

    $ git add .
    $ chmod +x .lintstaged-phpmd	# 実行権限の付与を忘れずに!
    $ git commit -m "PHP の PHPMD テスト。"
    husky > pre-commit (node v8.9.1)
      ↓ Stashing changes... [skipped]
        → No partially staged files found...
      ❯ Running linters...
        ❯ Running tasks for *.php
          ✔ prettier --write
          ✖ ./.lintstaged-phpmd
            git add
        ↓ Running tasks for *.{js,jsx,ts,tsx} [skipped]
          → No staged files match *.{js,jsx,ts,tsx}
        ↓ Running tasks for *.{css,scss} [skipped]
          → No staged files match *.{css,scss}
        ↓ Running tasks for *.{yaml,yml,md} [skipped]
          → No staged files match *.{yaml,yml,md}
    
    
    
    ✖ ./.lintstaged-phpmd found some errors. Please fix them and try committing again.
    /home/hato/repo/test.php:6	The class not_camel_case is not named in CamelCase.
    /home/hato/repo/test2.php:2	The class not_camel_case is not named in CamelCase.
    husky > pre-commit hook failed (add --no-verify to bypass)
    

    これで、フロントエンドとバックエンドの両方で CI が実行できますね。 もっと良い lint-staged と PHPMD の連携方法はないでしょうかね?

    この記事の執筆者

    hato6502

    Hata  

    中学生の頃に 6502 という CPU からプログラミングの世界に入りました。 C/C++ で1から作ることも好きですが、最近は Web 系にシフトしつつあります。