【Laravel向け】git hookで自動的にLintと静的解析を行う

# TL;DR

リポジトリ内に`.git/hooks/pre-commit`を作成し、内容を以下の通りとします。
コミットする直前に`.git/hooks/pre-commit`スクリプトが実行され、Lintと静的解析を行います。

## 前提
`PHP`、`composer`、`node.js`、`npm`と`eslint`、`phpcs`、`larastan`はインストール済みであるものとします。

```bash:.git/hooks/pre-commit
#!/bin/bash
<< COMMENT
MIT License

Copyright (c) 2022 hidao80
COMMENT

php_target_files=`git diff --cached --name-only | grep .php`
js_target_files=`git diff --cached --name-only | grep ".js\|.vue\|.jsx\|.tsx\|.ts\|.json\|.html\|.twig"`
phpcs=`composer global config bin-dir --absolute 2> /dev/null`/phpcs
phpcs_ruleset=./phpcs.xml
phpstan=`composer global config bin-dir --absolute 2> /dev/null`/phpstan
viewer=code

# メインで他のシェルを使っているときのためにこのスクリプトで使う環境変数をセットする
export NODE_PATH=`npm root -g`

if [ -e ./storage/logs ]; then
    # Laravelプロジェクトのリポジトリの場合
    phpstan_extension="-c `composer global config home --absolute 2> /dev/null`/vendor/nunomaduro/larastan/extension.neon"
    output=./storage/logs/git_hook.log
else
    # Laravelプロジェクトではないリポジトリの場合
    phpstan_extension=""
    output=./git_hook.log
fi

# pre-commitのログを削除する
echo "" > $output

# ステージングしたファイルにJavaScriptやjsonがなければスキップ
if [ -z "$js_target_files" ]; then
    echo "[eslint] js/json files not found." >> $output 2>&1
else
    echo "[eslint] running..." >> $output 2>&1
    eslint $js_target_files >> $output 2>&1
fi

# ステージングしたファイルにPHPがなければスキップ
if [ -z "$php_target_files" ]; then
    echo "[larastan] php files not found." >> $output 2>&1
else
    echo "[phpcs] running..." >> $output 2>&1
    $phpcs --standard=$phpcs_ruleset $php_target_files >> $output 2>&1

    echo "[larastan] running..." >> $output 2>&1
    php -d memory_limit=-1 $phpstan analyse -l 5 $phpstan_extension $php_target_files >> $output 2>&1
fi

# VS Codeで今実行したpre-commitのログファイルを開く
$viewer $output
```

## 解説

### `git diff --cached --name-only`
リポジトリでステージングしている（`git add .`など）ファイル名のリストを取得するコマンドです。
この後のLinterや静的解析に変更したファイルだけを渡して処理時間を抑える工夫です。

### `composer global config bin-dir --absolute 2> /dev/null`
`composer`のグローバルでインストールしたときの保存先を取得します。
OSの違いやインストールオプションによって異なるので、コマンドで取得するようにしています。
シバンで`bash`を指定しているので、Windows環境でもディレクトリ区切り文字を`\`にする必要はありません。

### ``export NODE_PATH=`npm root -g` ``
`npm`のグローバルでインストールしたときの`node_modules/`の保存先を取得します。
OSの違いやインストールオプションによって異なるので、コマンドで取得するようにしています。
環境変数`NODE_PATH`がないと`eslint`に必要なパッケージ`eslint-plugin-*`が読み込めず、`eslint`が動かない可能性があります。

### ファイルリストが空のとき
`JavaScript`と`PHP`のそれぞれでステージングしているファイルのリストを取得します。
`JavaScript`のファイルリストが空ならば`eslint`は実行しません。
`PHP`のファイルリストが空ならば`phpcs`と`phpstan`は実行しません。

### `$viewer $output`
今回`pre-commit`スクリプトが実行したときに出力したログファイルをVS Codeで開きます。
VS Codeが入っていない、またはログの確認に使用したいプレビューアーがある場合は変数`viewer`の値をそのプログラム名と置き換えてください。

### ライセンス
本`pre-commit`スクリプトはMITライセンスで配布しています。

## ログファイルの例
```

[eslint] js/json files not found.
[phpcs] running...
. 1 / 1 (100%)


Time: 31ms; Memory: 6MB

[larastan] running...
 0/1 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%[1G[2K 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ---------------------------------------------------------------------- 
  Line   aaa.php                                                               
 ------ ---------------------------------------------------------------------- 
  10     PHPDoc tag @param for parameter $id with type string is incompatible  
         with native type int.                                                 
  12     Function fucn() with return type void returns mixed but should not    
         return anything.                                                      
  12     Undefined variable: $id2                                              
  14     Unreachable statement - code above always terminates.                 
 ------ ---------------------------------------------------------------------- 


 [ERROR] Found 4 errors                                                         


```

## 応用

`.git/hooks/commit-msg`スクリプトでコミットメッセージをチェックし、わざと異常終了するようなコミットメッセージでコミットを試みることで、コミット前にソースの自動解析だけをすることができます。

また、`.git/hooks/pre-commit`でエラーや警告が出た場合、スクリプトを異常終了させる（`exit 1`など）ことで、Lintと静的解析による不具合チェックを通らないとコミットできないようにもできます。（ただし、緊急時に困ることがありそう）

## ダウンロード

本スクリプトはgistからダウンロードできます。

https://gist.github.com/hidao80/98bd629acd4d407307d74ec152a08987