Emacsで自動修正を実現する auto-fix.el

2019-01-30T13:46:18+0900
Emacs

AtomからEmacsに引越しする中で、AtomにあってEmacsにはなく、これがないと快適なプログラミングは厳しいというパッケージや機能が幾つかありました。

その中のひとつが、コードの自動修正機能を提供するパッケージです。

エディタでコードを自動修正する #

個人的な感覚ではGo言語とgofmtの登場以降、いわゆるインデントのタブ・スペース論争やコーディングスタイルについては、プロジェクト毎に利用するコードフォーマッタに任せるという流れで決着がついたと思っています。

最近良く書くJavaScriptやTypeScriptでは、ESLint、TSLint、Prettierが主流になったお陰もあり、僕みたいな様々な会社のプロジェクトで開発を行う人間も、インデント、クォート、文末のセミコロンなどの修正はプログラムに任せて、僕は適当に書いて保存するだけで自然に統一がはかられるようになりました。

Atomでは、この自動修正とエディタとの連携はlinter-eslintatom-beautifyなどのプラグインによって実現していましたが、Emacsでは、個別の言語で修正させるパッケージはちらほらありますが、汎用的に使えるパッケージは僕の中ではまだ見つかっていません。

そこで、Elispの練習がてら自分でパッケージを作成してみることにしました。

汎用自動修正パッケージauto-fix.el #

auto-fix.el

auto-fix.elgo-mode.elgofmt()を参考にして汎用的なコードの自動修正を実現するパッケージです。

コードの自動修正を実現するための方式として、ファイルを直接書き換える方式とバッファを書き換える方式の2種類がありますが、Emacsは設定によってはファイルを直接書き換えてしまうと読み込み直す手間が発生するため、go-mode.elで使われているファイルを保存する前にバッファを書き換える方式を採用しました。

また、コードの自動修正が必要かどうかを判別するためのテンポラリファイルの作成場所に、システムのtmpディレクトリを利用してしまうと、自動修正プログラムが利用する設定ファイルでプロジェクト内のディレクトリに関する設定がある場合(例えば特定のディレクトリのファイルは対象外にするなど)にリスペクトされないようになってしまうため、修正の対象となるファイルと同じディレクトリに作成するようにしています。

auto-fix.elの使い方 #

auto-fix.elの使い方はgo-mode.elと同じようにauto-fix-before-save()before-save-hook()にひっかけて利用します。

そして、auto-fix.elはマイナーモードとして実装されているため、auto-fix-commandauto-fix-optionという2つのバッファローカル変数に修正したいプログラムと自動修正を実行するための引数をセットした上で、メジャーモードのフックで有効化します。

例えばシンプルにRubyでrubocopを実行させたい場合は次のような設定になります。

(add-hook 'auto-fix-mode-hook
          (lambda () (add-hook 'before-save-hook #'auto-fix-before-save)))

(defun setup-ruby-auto-fix ()
  (setq-local auto-fix-command "rubocop")
  (setq-local auto-fix-option "-a")
  (auto-fix-mode +1))

(add-hook 'ruby-mode-hook #'setup-ruby-auto-fix)

これでruby-modeでファイルを編集して保存するだけで、ファイル保存時にrubocopによる自動修正が実行されるようになります。

flycheckとの組み合わせ #

僕はflycheckを利用していてプロジェクトごとの修正プログラムを利用しているため、次のような設定で利用しています。

;; flycheck
(defun my-use-local-lint ()
  "Use local lint if exist it."
  (let* ((root (locate-dominating-file
                (or (buffer-file-name) default-directory) "node_modules"))
         (eslint (and root (expand-file-name "node_modules/.bin/eslint" root)))
         (tslint (and root (expand-file-name "node_modules/.bin/tslint" root))))
    (when (and eslint (file-executable-p eslint))
      (setq-local flycheck-javascript-eslint-executable eslint)
      (setq-local auto-fix-command eslint))
    (when (and tslint (file-executable-p tslint))
      (setq-local flycheck-typescript-tslint-executable tslint)
      (setq-local auto-fix-command tslint))))

(add-hook 'flycheck-mode-hook #'my-use-local-lint)

;; auto-fix
(add-hook 'auto-fix-mode-hook
          (lambda () (add-hook 'before-save-hook #'auto-fix-before-save)))

(add-hook 'js-mode-hook #'auto-fix-mode)
(add-hook 'typescript-mode-hook #'auto-fix-mode)

こちらはflycheckを(global-flycheck-mode t)で有効化した前提でのJavaScriptとTypeScriptの設定例です。

JSやTSファイルを開くとflycheck-modeが有効になり、そのタイミングでflycheckのチェッカープログラムとauto-fix-commandにプロジェクトごとの修正プログラムをセットしています。なお、auto-fix-optionのデフォルト値は--fixであるため、ESLintとTSLintにはオプションを設定する必要はありません。

まとめ #

現在こちらのパッケージはまだ試用段階であるためMELPAには登録していませんが、このまま利用してみて問題がなければいずれ登録しようと思っています。

最後にこちらのパッケージを作成するにあたり、Slackのemacs-jp.slack.comでヒントをくれた@tadsanにこの場を借りて御礼申し上げます。