TOYOKUMO Tech Blog

トヨクモ株式会社の開発本部のブログです。

Clojureのフォーマッターcljstyleの使い方

こんにちは。現在アルバイトで、2020年4月に入社予定の開発本部の江口(@egs33)です。

弊社ではClojure製の製品を開発していますが、ClojureのフォーマッターとしてOSSのcljstyleを利用しています。 利用するにあたって機能の追加やバグの修正などのコントリビュートも行いました。

tech.toyokumo.co.jp 上の記事はフォーマッターの内部実装についての説明ですが、 この記事ではcljstyleの使い方を説明したいと思います。 また、この記事はバージョン0.12.0の情報をもとに書いています。

Clojureのフォーマッターの現状

近年コードを書いていくときは、golangのgoimportsやJavaScriptのESLintなどを使って共通したフォーマットを当てていくことが広く行われていると思います。 しかし似たようなことをClojureでやろうとした時に実用的なものがない状態が続いていました。

例えば「Clojure formatter」と検索して一番にヒットするのはcljfmtだと思います。 ですがこれはnsマクロの :require のソートをしてくれなかったり、Leiningenプラグインとして使用されることを想定しており、起動時間がかかってしまうため、エディタと連携した使用には耐えられないなどの問題がありました。 そのため、弊社製品にもフォーマッターを自動的に適用できない状態で、レビュー時にインデントを指摘せざるを得ない状況でした。

cljstyleとは

github.com cljstyleは前述したcljfmtをforkして作られた、オープンソースのClojureコードのフォーマッターです。 cljstyle自体もClojureで開発されていて、GraalVMでネイティブバイナリにビルドされているため高速に実行できます。

しかし、0.9.0の時点では弊社の製品に適用するには機能が足りなかったので、次のようなPRを書いて改善した上で、製品に適用しました。

Clojureのスタイルガイドによると、リストのインデントは1つであることが一般的ですが、元々のcljstyleにおいては2つ固定で設定不可能でした。 これを :list-indent-size で設定可能にするというPRを出しました(#21#25)。 また、コメントのインデントに関するバグを修正しました(#32)。

これらのPRがマージされた0.12.0から実際に弊社製品でも使用しています。

;; :list-indent-size 2 (default)
(foo
  bar
  baz)

;; :list-indent-size 1
(foo
 bar
 baz)

実行

cljstyleの実行ファイルをReleaseページからダウンロードし、PATHの通っているところに配置します。

$ cljstyle version # cljstyleのバージョンの表示
mvxcvi/cljstyle 0.12.0 (e6b85861ae7e4333f224d646d97dd695a8e3b1e5)
$ cljstyle check # カレントディレクトリ以下のフォーマットのチェック
$ cljstyle fix # カレントディレクトリ以下のフォーマットの修正

どちらも後ろにファイル名をつけることでそのファイルのみのフォーマットを行うことができます。 より詳しい使用方法はcljstyle -hで見ることができます。

cljstyleの設定

cljstyleは .cljstyleファイルにフォーマットの設定を記述します。 .cljstyle ファイルの内容は以下のような設定をClojureのmapとして記述したものです。

{:indentation? true
 :list-indent-size 2
 :indents {}
 :line-break-vars? true
 :line-break-functions? true
 :reformat-types? true
 :remove-surrounding-whitespace? true
 :remove-trailing-whitespace? true
 :insert-missing-whitespace? true
 :remove-consecutive-blank-lines? true
 :max-consecutive-blank-lines 2
 :insert-padding-lines? true
 :padding-lines 2
 :rewrite-namespaces? true
 :single-import-break-width 30
 :require-eof-newline? true
 :file-pattern #"\.clj[csx]?$"
 :file-ignore #{}}

ちなみに上の設定は :indent以外はデフォルトのルールです。

.cljstyle ファイルの場所

設定ファイルは各フォルダに対して適用させることができます。 例えば以下のようなフォルダ構成の場合、

a
├── .cljstyle
└── b
    ├── c
    │   ├── .cljstyle
    │   └── foo.clj
    └── d
        ├── .cljstyle
        └── e
            └── bar.clj

foo.clj のフォーマットをする場合には a/b/c/.cljstylea/.cljstyle が使用されます。 深いディレクトリの設定が優先されるのでこの場合はa/b/c/.cljstyle が優先されます。 同様に bar.cljのフォーマットをする場合、a/b/d/.cljstylea/.cljstyle を前者を優先してマージした設定が使用されます。

各設定の内容

:file-pattern

デフォルト: #"\.clj[csx]?$"

フォーマットの対象とするファイルを正規表現で指定します。

:file-ignore

デフォルト: #{}

文字列か正規表現の Setでフォーマットの対象としないファイルを指定します。 文字列の場合、一致するファイル名のファイルと、一致するディレクトリ名のディレクトリ以下のファイルは無視されます。 正規表現の場合、パスと一致するファイル、ディレクトリが無視されます。

:indentation?

デフォルト: true

コードのインデントの修正を行うかどうかを指定します。

:list-indent-size

デフォルト: 2

リストのインデントサイズを指定します。

;; :list-indent-sizeが2の場合
(foo
  bar
  baz)

;; :list-indent-sizeが1の場合
(foo
 bar
 baz)

:indents

コードをフォーマットする際のインデントのルールを指定します。 詳しくは後述します。

:line-break-vars?

デフォルト: true

defフォームが複数行になっているときとdoc-stringを持つ場合に改行を入れます。 具体的には以下のようになります。

;; before
(def x (f 1
          2))
(def y "doc" 100)

;; after
(def x
  (f 1
     2))
(def y
  "doc"
  100)

:line-break-functions?

デフォルト: true

defndefn-のフォームの内部で改行させます。 また、 fnフォームが複数行にわたっている場合に改行させます。 具体的には以下のようになります。

;; before
(defn f [x] (fn [y] (+ x
                       y)))

;; after
(defn f
  [x]
  (fn [y]
    (+ x
       y)))

:reformat-types?

デフォルト: true

defprotocol, deftype, defrecord, reify, proxyなどのフォームの内部で改行させます。 具体的には以下のようになります。

;; before
(defrecord Person [name address])

(defprotocol Bird
  (fly [p])
  (walk [p]))

;; after
(defrecord Person
  [name address])


(defprotocol Bird

  (fly [p])

  (walk [p]))

:remove-surrounding-whitespace?

デフォルト: true

フォームの内部を囲む空白を ( foo ) => (foo) のように削除します。

:remove-trailing-whitespace?

デフォルト: true

行の最後にある空白を削除します。

:insert-missing-whitespace?

デフォルト: true

要素間に (foo(bar)) => (foo (bar))のように空白を追加します。

:remove-consecutive-blank-lines?

デフォルト: true

:max-consecutive-blank-linesで指定される数より多い連続する空行を削除します。 :max-consecutive-blank-linesのデフォルトは 2です。

:insert-padding-lines?

デフォルト: true

トップレベルの複数行の非コメントフォーム間に、少なくとも :padding-lines行の空行をもつようにします。 :padding-linesのデフォルトは 2です。

:rewrite-namespaces?

デフォルト: true

nsフォームをフォーマットします。 :requireフォームをアルファベット順に並び替えたりします。

;; before
(ns examples.ns
  "example"
  (:import
    java.util.Date
    (java.util.concurrent
      Executors
      LinkedBlockingQueue))
  (:require
    [clojure.set :as set]
    [clojure.string :as s :refer [blank?]]
    [clojure.java.shell :as sh]))

;; after
(ns examples.ns
  "example"
  (:require
    [clojure.java.shell :as sh]
    [clojure.set :as set]
    [clojure.string :as s :refer [blank?]])
  (:import
    java.util.Date
    (java.util.concurrent
      Executors
      LinkedBlockingQueue)))

:single-import-break-width

デフォルト: 30

単一クラスのインポートの文字数が :single-import-break-widthよりも長い場合 グループ化されます。 具体的には以下のようになります。

;; before
(ns example.namespace
  (:import
    java.io.InputStream))

;; after (:single-import-break-width 10)
(ns example.namespace
  (:import
    (java.io
      InputStream)))

:require-eof-newline?

デフォルト: true

ファイル末尾に改行がない場合に改行を追加します。

インデントの設定

:indentsには以下のようなmapを設定します。

{if [[:block 1]]
 do [[:block 0]]
 fn [[:inner 0]]}

マップのkeyがインデントを適用するフォーム、valueがどのようなルールを適用するかを表します。

デフォルトのルールは https://github.com/greglook/cljstyle/blob/master/core/resources/cljstyle/indents.cljにあります。

インデントを適用するフォーム

マップのkeyにはシンボルと正規表現を指定できます。

  • シンボルの場合
    • foo/barを指定した場合 (foo/bar 1 2)に適用されますが, (bar 1 2)(baz/bar 1 2)には適用されません。
    • fooを指定した場合 (foo 1 2)(a/foo 1 2)に適用されます。名前空間付きのルールがある場合はそちらが優先的に使用されます。
  • 正規表現の場合
    • 名前空間を含むフォームシンボルを文字列としたものにマッチするものに適用されます。 シンボルで指定されたルールがある場合はそちらが優先的に使用されます。

インデントのルール

cljstyleでは :inner, :block, :stair の3種類のルールがあります。 ルールを指定しない場合は通常の関数のインデントとなるので、引数を揃える以下のようなフォーマットになります。

(bar 1 ["a"
        "b"]
     3 4
     5)

(foo
  bar
  baz)

ルールはvectorで [[:inner 0] [:inner 1]]のように指定します。 この場合 [:inner 0][:inner 1]が適用されます。

:inner

:innerルールはパラメータを取り、パラメータで指定された深さのフォームのインデントを2スペースのインデントにします。

;; before
(foo 1
     (bar 2
          3)
     (bar 4
          5)
     [6
      7
      8])

;; after ({foo [[:inner 0]]})
(foo 1
  (bar 2
       3)
  (bar 4
       5)
  [6
   7
   8])

;; after ({foo [[:inner 1]]})
(foo 1
     (bar 2
       3)
     (bar 4
       5)
     [6
      7
      8])

;; after ({foo [[:inner 0] [:inner 1]]})
(foo 1
  (bar 2
    3)
  (bar 4
    5)
  [6
   7
   8])

また、もう一つパラメータを追加することで引数の指定した深さにおいて指定した順番にあるフォームを指定できます。

;; before
(foo (bar1 "1-1"
           "1-2"
           "1-3")
     (bar2 "2-1"
           "2-2"
           "2-3"))

;; after ({foo [[:inner 1 3]]})
(foo (bar1 "1-1"
           "1-2"
       "1-3")
     (bar2 "2-1"
           "2-2"
       "2-3"))

;; after ({foo [[:inner 1 2] [:inner 1 3]]})
(foo (bar1 "1-1"
       "1-2"
       "1-3")
     (bar2 "2-1"
       "2-2"
       "2-3"))

:block

:blockルールはパラメータを1つ取り、改行の前にパラメータで指定された数以下の引数がある場合に :innerルールのようにインデントします。

;; before
(bar
  1
  2
  3)

(bar 1
     2
     3)

(bar 1 2
     3)

(bar 1 2 3)

;; after ({bar [[:block 1]]})
(bar
  1
  2
  3)

(bar 1
  2
  3)

(bar 1 2
     3)

(bar 1 2 3)

:stair

:stairルールはパラメータを1つ取り、基本的に :blockインデントと同じように振る舞いますが、 引数をペアとしてインデントします。

;; before
(cond
  a? :a
  b? :b)

(cond
  a?
  :a
  b?
  :b)

;; after ({cond [[:stair 0]]})
(cond
  a? :a
  b? :b)

(cond
  a?
    :a
  b?
    :b)

上のように condcaseのような条件と式を複数受け取るようなフォームに使うことが想定されています。

Clojure Style Guide用の設定

github.com 以下のように設定すると上のClojure Style Guideに沿ったソースコードに(全てではないですが)フォーマットでき、弊社で利用しています。

{:list-indent-size 1
 :line-break-functions? false
 :line-break-vars? false
 :max-consecutive-blank-lines 1
 :padding-lines 1
 :reformat-types? false}

エディタとの連携

vim

https://github.com/greglook/cljstyle/blob/master/doc/editors.mdにあります。

IntelliJ IDEA (Ultimate)

IntelliJ IDEA (Ultimate)のファイル監視機能で以下のように設定すると 保存時に自動でフォーマットされます。

f:id:egs33:20200214162956p:plain

ClojureScriptのファイルや .cljcファイルなどにも適用する場合は設定のFile TypeをClojureScriptにしたものなどの設定を別で作る必要があります。

最後に

cljstyleの設定方法についてなるべく詳しく説明してみました。 cljstyleでみなさんのClojure lifeがよりよくなることを願っています。


トヨクモ株式会社ではClojureを書きたいエンジニア、PHP/Vue.jsを書きたいエンジニア、技術が好きなエンジニアを募集しております。

よろしければ採用ページをご覧ください。