ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

既存の Laravel プロジェクトに自動構文チェックを軽く入れる

Laravel 始めました。今回はコードの自動構文チェックをすごく軽く入れる話。ゆくゆくは CI に含める予定だけどまだその時間と根回しがないので簡単にやります。

どんな効果を狙っているか?

  • レビューにおいて構文に関する指摘が大半を占めているのでその時間を削減する
  • 構文に関する規約が暗黙知化しているので (レビューで指摘が多く上がる原因のひとつ)、規約を管理できる構成にする
  • 書くのが面白そう!

どんなものを作るか?

  • git のプレコミットフック (git/hooks/pre-commit) で追加/変更したコードの構文チェックをして、違反していたらコミットできなくする (コミットできない、はやりすぎか?)
  • 違反している箇所については構文チェックライブラリのエラーを出す (変更行だけチェックできないか? -> 今回はできなかった...)
  • 緊急時のためにフックを無視する手段も作っておく(すでにプレコミットフックとして仕組みがある? -> --no-verify option)
  • プレコミットフックがない環境に影響を与えない (まあ多分 pre-commit 配置しなければ良いだけと思う)
  • 構文チェックのルールはファイルで管理できるようにする (構文チェックライブラリが標準で備えているはず)
  • とりあえずチェック対象は、jsphp (当面は拡張子によって判定)

準備

使うものとしては以下を考えています

  • phpcs (PHPCodeSniffer)
  • eslint

Laravel と言っていますが、git 使ってれば応用効くと思います。それぞれ composer , npm (or yarn) で開発環境の依存ライブラリとして入れます。

(これらがそもそもグローバルインストールされている状態だとどうなるんだろう…?それも忘れずに確認しなきゃ…)

pre-commit 作ってみる

まずは何も構文チェックライブラリ入れずにフックだけ作成してみます。処理の流れとしては、 git commit を実行した直後に、pre-commit が呼ばれ、スクリプトの戻り値(?) が 0 以外の時に commit が停止されるというものです。

.git/hooks/pre-commit

#!/bin/sh

echo hoge
exit 1

また、スクリプトの実行権限を 755 に変更しておきましょう。

$ touch fuga
$ git add .
$ git commit
hoge

とりあえず commit 時にフックが実行されるのがわかりました。あとは処理を書いていきます。

対象ファイルの抽出

git のコマンドを使用して HEAD との比較で、追加、更新があったファイルを取得します。コードは以下のようになります。

#!/bin/sh

# Get against commit hash to compare.
if git rev-parse --verify HEAD >/dev/null 2>&1
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# Redirect output to stderr.
exec 1>&2

# Get added and modified files.
files=`git diff-index --cached --name-only --diff-filter=AM ${against}`

echo ${files}

比較結果は files に、スペース区切りの文字列で入ってきます (配列として取り扱うかどうかは悩みましたが、特に必要ないので git diff-index ... の戻り値をそのまま使います。)

ここでは --diff-filter=AM によって追加 (A)、更新 (M) のあったファイルのみを抽出しています。削除やリネームしただけのファイルはチェックしません。 (削除ファイルやリネームファイルのチェックは、自分が書いたわけでも無いファイルの構文を直せということになり、プロジェクトの整理作業に負のモチベーションを与える気がします。)

exec 1>&2 は標準出力をエラー出力にリダイレクトしています。このスクリプトで出力が行われることは、即ちエラーだからです。 (と偉そうにいってますが、この行までは全て .git/hooks/pre-commit.sample から盗んできたものです。)

対象ファイルを拡張子で分類

ここまでくると特別なことはありません。シェルスクリプトを書くだけです。

...
# ↓チェック対象のファイル取得後の処理

# Check syntax.
is_error=0
output=""

php_files=""
js_files=""
for f in ${files}
do
  extension=${f##*.}
  case ${extension} in
    php)
      php_files+="${f} "
    ;;
    js)
      js_files+="${f} "
    ;;
  esac
done

if [ -n "${php_files}" ]
then
  output+=`phpcs ${php_files}`
  is_error+=$?
fi

if [ -n "${js_files}" ]
then
  output+=`eslint ${js_files}`
  is_error+=$?
fi

if [ ${is_error} -gt 0 ]
then
  is_error=1
  echo "${output}" | less
else
  is_error=0
fi

exit ${is_error}

これで一応、複数ファイルに対してまとめて構文チェックを行えるようになりました ( 上のスクリプトは構文チェックコマンド入っていないと動かないですが )。出力が流れてしまうので、構文エラーに関しては less に渡して表示するようにしています。色々思うところは以下かなと思います。

  • 文字列連結の半角スペースがダサいので配列使うべきでは?
  • 拡張子ごとにほぼ同じ処理を書いているので、新しい構文チェック追加時に変更大きいよ?

本当は、構文チェックライブラリに変更対象ファイル全部渡して、そっちでフィルタリングしてほしいという気持ちなので今の所シンプルすぎるくらいがちょうどいいのかなと思います。

phpcs, eslint の設定

phpcs

開発環境の依存パッケージに PHPCodeSniffer を追加します。

composer require --dev 'squizlabs/php_codesniffer'

phpcs./vendor/bin/phpcs に入っているのでスクリプトからはそれを読むことにします。

phpcs は実行時に --standard= オプションで使用するルールや設定ファイルを指定することができます。そのほかにも設定ファイルを phpcs.xml という名前で作成しておけば自動的にそのファイルを読んでくれるようです。(自分ディレクトリから親のディレクトリを順に登って行って phpcs.xml を見つけたら使用してくれるみたいです)

PSR2 を継承しつつ色々調整できるように雛形 phpcs.xml を作成してみました。

<?xml version="1.0"?>
<ruleset name="MyRule">
  <description>Coding standard based on PSR2 with some additions for my project.</description>
    <!--
      You can add your rules below.
      For example, you can include new standard, like that;
      <rule ref="PEAR" />

      If you want to know more about phpcs,
      See: https://github.com/squizlabs/PHP_CodeSniffer/wiki
      See also to know the notation of this file: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml
    -->

  <!-- Include the whole PSR2 standard -->
  <rule ref="PSR2">
    <!--
      You can exclude specific standard by adding exclude_tag and name_attribute here;
      <exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace" />
    -->
  </rule>
</ruleset>

次は .git/hooks/pre-commit で、ローカルのコマンドと、設定ファイルのパスを使うようにスクリプトを修正します。(設定ファイルは存在チェックだけ必要としています)

app_root_path=`git rev-parse --git-dir`/..
phpcs=${app_root_path}/vendor/bin/phpcs
phpcs_config=${app_root_path}/phpcs.xml

## Execute check.
### PHP
if [ -n "${php_files}" ]
then
  # Check installation of phpcs.
  if [ -e "${phpcs}" ]
  then
    # Check existence of cording standard file.
    if [ -e "${phpcs_config}" ]
    then
      # For upgrading rule file, add -s option to display rule's name.
      output+=`${phpcs} -s ${php_files}`
      is_error+=$?
    else
      output+='\nNOTE: phpcs configuration file is not found. going to check based on psr-2'
      output+='\n'
      output+=`${phpcs} --standard=PSR2 ${php_files}`
      is_error+=$?
    fi
  else
    output+='\nNOTE: phpcs is not installed. php syntax checking is skipped.'
  fi
fi

うん、すごく読みにくい気がします。他の構文チェックする前にシェルスクリプトの構文チェックしろよ的なブーメランですね。

phpcs.xml が見つかった時だけ -s オプションを渡してルール名を表示しておいてあげます。ルールファイルの修正の障壁にならないためにも。

ファイルがないときは何もしない、コマンドがないときはお知らせ、設定ファイルあるときはそれで実行、ないときはPSR2で実行という流れです。

eslint

急に疲れてきてしまったのですが、だいたい phpcs と同じになります。こちらのインストールは yarn add --dev eslintnpm install --dev eslint で行います。

開発環境の依存性でインストールされた後は、初期の雛形ファイルを作成します。 Use popular style guide などで適当に js ファイルとして作っておきました。(ファイル形式も pre-commit でみなきゃいけないかもしれないけど今は js 決め打ちで行きます)

$ yarn add --dev eslint
$ ./node_modules/.bin/eslint --init
$ ls .eslint.js
.eslint.js
$ yarn # init 後にもう一回パッケージのダウンロードが必要...

先ほど同様に .git/hooks/pre-commit を修正します。変更内容みたい方いましたら github でご確認ください。 pre-commit (すみません疲れてしまいました)

まとめ

shell じゃなくて php か js で書けばよかったのではと今更ながら思います。特に制御構文の省略記法とかどれを使っていいか悩んでしまい、ものすごくベタに書きました。それこそ構文チェックが必要な気がします。

まあ、ないよりはマシか、、、という気持ちで書きました。最低限動いたら後は使いながら修正していけばいい、という言い訳をしておきます。