nkenbou のはまり日記

良い意味でも悪い意味でもはまっていることを書いていきます。内容はソフトウェア開発全般です。

Cygwin 版 Emacs の flycheck (JSHint) で JavaScript のシンタックスチェックをする

問題

CygwinEmacs で flycheck を使用して JavaScriptシンタックスチェックができるようにするまでには、いくつもの問題があり一筋縄では行きませんでした。

ここでは、試行錯誤して辿り着いた一つの方法を紹介します。なお、僕の環境は gnupack を使用しています。

実際の環境:

  • OS: Windows 7
  • Emacs: gnupack_devel-12.03-2015.02.01
  • flycheck: flycheck-20150412.242
  • Node.js: v0.12.2
  • JSHint: jshint v2.6.3

flycheck のインストール

flycheck のインストールは普通に行います。例えば package.el でインストールするには以下のようにします。

M-x package-refresh-contents

M-x package-install flycheck

Node.js のインストール

flycheck で JavaScriptシンタックスチェックを行う場合、裏では JSHint が使用されます。

JSHint は Node.js の npm コマンドでインストールします。CygwinEmacs を使用しているので、Cygwin 上に JSHint をインストールしたいところですが、Node.js の Wiki に書かれている通り、Node.js で Cygwin がサポートされなくなっています。

仕方がないので、Windows に JSHint をインストールして Cygwin 経由で JSHint を使用することにします。Node.js はインストーラーや nodist からインストールします。詳細なインストール手順は省略しますが、インストールが完了すると npm コマンドが使用できるようになっているはずです。

Node.js のインストーラー: https://nodejs.org/

nodist (GitHub): https://github.com/marcelklehr/nodist

JSHint のインストール

npm コマンドでグローバルに JSHint をインストールします。

npm install jshint --global

js2-mode のフックで flycheck-mode を有効にする

僕は JavaScript を書くときには js2-mode を使用しているので、js2-mode のフックで flycheck-mode を有効にします。

init.el:

(add-hook 'js2-mode-hook
          '(flycheck-mode t))

通常ならここまでの設定をすれば、JavaScript のバッファで flycheck が実行されるようになるはずですが、CygwinEmacs では動作しませんでした。flycheck-mode は有効になっているのですがチェック結果が表示されませんでした。

原因

flycheck が JSHint を呼び出すときは、対象のファイルを /tmp/flycheckXXXX/ にコピーして (XXXX 部分はランダム)、コピーしたファイルに対してチェックを行います 。JSHint へパラメーターとして渡されるファイルのパスは /tmp/flycheckXXXX/対象ファイル.js のようになります。しかし、/ (スラッシュ区切り) や /tmp/Windows では解析できないため、ERROR: Can't open /tmp/flycheckXXXX/対象ファイル.js のようにエラーになってしまいます。

解決方法

解決方法として思いついたのは、JSHint にパラメーターとして渡すファイルパスの / (スラッシュ区切り) や /tmp/ 部分を Windows 上のパスに変換してしまうことでした。試しに flycheck の中のファイルパスを取得する関数に advice を設定して Cygwin 上ファイルパス -> Windows 上ファイルパスに変換するようにしたところ、JSHint は正常に実行されるようになり結果も返ってきましたが、今度は結果をパースする flycheck の別の関数で新たな問題が発生しました。

JSHint からの結果にはチェック対象のファイルパス (JSHint へのパラメーターと同一) が含まれており、flycheck ではそのファイルパスを使ってチェック後の処理を行っていました。しかし、このファイルパスは JSHint に渡す前に Windows 上でのファイルパスに変換してしまっているので正常に処理できなくなっていました。

そこで、これも同じように JSHint からの結果に含まれる Windows 上ファイルパスを元の Cygwin 上ファイルパスに戻してしまえばよいと思い、advice を設定 (Cygwin 上ファイルパス -> Windows 上ファイルパスの変換の時とは別の関数に設定) して元に戻したところ正常に動作することが確認できました。

しかし、最終的には flycheck にいろいろ手を入れすぎるのは嫌だったので、JSHint を実行する前後でファイルパスの変換を行うシェルスクリプトを書いて flycheck からはそのシェルスクリプトを呼ぶように設定することにしました。また、--config オプションのファイルパスも Cygwin 上のファイルパス -> Windows 上のファイルパスに変換しています。

シェルスクリプト (ファイル名: flycheck-jshint-cygwin):

<2015-5-16 Sat>変更: .jshintrc が祖先ディレクトリに存在する場合しない場合の両方で動作するようにした。

#!/bin/bash

filename=$(cygpath -w ${@:$#})

if [ $# -eq 2 ]; then
    output=$(/c/nodist/bin/jshint $1 $filename)
else
    config=$(cygpath -w $3)
    output=$(/c/nodist/bin/jshint $1 $2 $config $filename)
fi

tmp_win=$(cygpath -w /tmp/ | sed -e 's/\\/\\\\/g')
output=$(echo "$output" | sed -e '/^\t*<file name=\".\+\">$/ {s|'${tmp_win}'|/tmp/|; s|\\|/|g}')

echo "$output"

flycheck-jshint-cygwin はパスの通ったところに配置してください。また、/c/nodist/bin/jshint の部分は実際の環境に合わせて jshint のコマンドがインストールされているパスを指定してください。

init.el に書く flycheck の設定:

(when (eq system-type 'cygwin)
  (flycheck-define-checker javascript-jshint
    "A JavaScript syntax and style checker using jshint.

See URL `http://www.jshint.com'."
    :command ("flycheck-jshint-cygwin" "--checkstyle-reporter"
              (config-file "--config" flycheck-jshintrc)
              source)
    :error-parser flycheck-parse-checkstyle
    :error-filter flycheck-dequalify-error-ids
    :modes (js-mode js2-mode js3-mode))
  )

flycheck の設定は flycheck 内に定義済みのものをコピーしてきて、使用コマンドを "jshint" -> "flycheck-jshint-cygwin" に書き換えることで定義を上書きしています。

flycheck-mode の有効化 + α

最後に js2-mode のフックで flycheck-mode を有効にします。

init.el:

(add-hook 'js2-mode-hook
          (lambda ()
            (flycheck-mode t)))
)

ここまでの設定で CygwinEmacsJavaScript の flycheck が動作するようになりました。

また、js2-mode で flycheck を使用する場合は js2-mode で標準で実行されるシンタックスチェックを無効にすることをおすすめします。

(add-hook 'js2-mode-hook
          (lambda ()
            (setq js2-mode-show-parse-errors nil)
            (setq js2-mode-show-strict-warnings nil)
            (flycheck-mode t)))
)