flymakeでrubocopを環境に合わせて実行

rubyを書く際にコーディングスタイルをrubocopでチェックしたい時、手動でチェックしていると忘れることがあるので違反があったらその場でエディタに警告して欲しいわけですが、これをemacsのflymakeでやる方法のメモ。

.emacsなどをいろんな環境で共有しているので、マシンによってrubocopが入っていたりなかったり、あってもグローバルでなく個々のbundleに入っていたりといった状況に対応できるようにします。(rubocopがない場合はruby -cによりシンタックスチェックのみ行います。)
(flymakeよりflycheckの方がいいのかもしれないですが、パッケージ等を追加で入れなくてもemacsについてくることを重視してflymakeにしてます。なんか時代錯誤 *1 )


まず、下記のshell scriptを ~/bin/ とか適当な場所に置きます。PATHが通ってなくても構いませんが、権限は実行可能にしておきます。

  • cat bin/flymake-ruby.sh
#!/bin/sh
RUBOCOP="rubocop --format emacs"

exists () {
    which "$1" >/dev/null 2>&1
}

# use rubocop directly
exists rubocop && exec $RUBOCOP "$@"

# use rubocop in the bundle
CWD="$PWD"
abspath="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
until [ "$PWD" = "/" ]; do
    if [ -f Gemfile.lock ]; then
        if grep rubocop Gemfile.lock >/dev/null; then
            exec bundle exec $RUBOCOP $abspath 2>&1
        fi
        break
    fi
    cd ..
done
cd "$CWD"

# if no rubocop is available, just use ruby syntax check
ruby -c "$@"


あとは .emacs に、flymakeでrubyスクリプトの編集中にflymake-ruby.shでチェックをかけて、違反があれば赤色表示になるように設定を追加します。以下では M-n / M-p キーでエラーのある位置に飛ぶとともに、エラー内容をminibufferに表示するようにしています。
(ruby-modeやflymake自体の設定などは適宜環境に合わせてください。なお以下ではバッククォートが \ (円マーク) になってしまっていますのでご注意を。)

;; ruby mode
(autoload 'ruby-mode "ruby-mode" nil t)
;(autoload 'ruby-mode "ruby-electric" nil t)
(setq auto-mode-alist (cons '("\\.rb$" . ruby-mode) auto-mode-alist))

;; flymake
(require 'flymake)
(global-set-key "\C-cd" 'flymake-popup-current-error-menu)
(global-set-key "\M-n" 'flymake-goto-next-error)
(global-set-key "\M-p" 'flymake-goto-prev-error)
(defun display-error-message ()
  (message (get-char-property (point) 'help-echo)))
(defadvice flymake-goto-prev-error
    (after flymake-goto-prev-error-display-message) (display-error-message))
(defadvice flymake-goto-next-error
    (after flymake-goto-next-error-display-message) (display-error-message))
(ad-activate 'flymake-goto-prev-error 'flymake-goto-prev-error-display-message)
(ad-activate 'flymake-goto-next-error 'flymake-goto-next-error-display-message)

;; flymake for ruby
(defun flymake-ruby-init ()
  (let* ((temp-file   (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
         (local-file  (file-relative-name
                       temp-file
                       (file-name-directory buffer-file-name))))
    (list "~/bin/flymake-ruby.sh" (list local-file))))
(push '(".+\\.rb$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '(".+\\.rake$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '("Rakefile$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '("^\\(.*\\):\\([0-9]+\\): \\(.*\\)$" 1 2 nil 3) flymake-err-line-patterns)
(push '("^\\(.*\\):\\([0-9]+\\):[0-9]+: \\(.\\): \\(.*\\)$" 1 2 3 4) flymake-err-line-patterns)
(add-hook 'ruby-mode-hook
          '(lambda ()
             ;; Don't want flymake mode for ruby regions in rhtml files and also on read only files
             (if (and (not (null buffer-file-name)) (file-writable-p buffer-file-name))
                 (flymake-mode t))
             ))


rubocop自体の設定は RuboCopの設定アレコレ - Qiita などを参考に。

*1:そもそもemacsが…