こんにちは。開発本部の江口(@egs33)です。
弊社ではClojure製の製品を開発していますが、ClojureのフォーマッターとしてOSSのcljstyleを利用しています。 利用するにあたって機能の追加やバグの修正などのコントリビュートも行いました。
tech.toyokumo.co.jp
上の記事はフォーマッターの内部実装についての説明ですが、
この記事ではcljstyleの使い方を説明したいと思います。
また、この記事はバージョン0.12.0の情報をもとに書いています。
この記事はバージョン0.15.0の情報をもとに書いています。(2022年6月更新)
- Clojureのフォーマッターの現状
- cljstyleとは
- 実行方法
- cljstyleの設定
- .cljstyle ファイルの場所
- 各設定の内容
- :files / :extensions
- :files / :pattern
- :files / :ignore
- :rules / :indentation / :list-indent
- :rules / :indentation / :indents
- :rules / :whitespace / :remove-surrounding?
- :rules / :whitespace / :remove-trailing?
- :rules / :whitespace / :insert-missing?
- :rules / :blank-lines / :trim-consecutive?
- :rules / :blank-lines / :insert-padding?
- :rules / :eof-newline / :enabled?
- :rules / :comments / :inline-prefix
- :rules / :comments / :leading-prefix
- :rules / :vars / :enabled?
- :rules / :functions / :enabled?
- :rules / :types / :enabled?
- :rules / :types / :types?
- :rules / :types / :protocols?
- :rules / :types / :reifies?
- :rules / :types / :proxies?
- :rules / :namespaces / :enabled?
- :rules / :namespaces / :indent-size
- :rules / :namespaces / :break-libs?
- :rules / :namespaces / :import-break-width
- インデントの設定
- Clojure Style Guide用の設定
- エディタとの連携
- 最後に
- 更新履歴
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をインストールします。 macの場合はHomebrewから
brew install --cask cljstyle
でインストールすることができます。
LinuxなどHomebrewを使っていない環境では https://github.com/greglook/cljstyle#installation を見てインストールします。
$ cljstyle version # cljstyleのバージョンの表示 mvxcvi/cljstyle 0.15.0 (1f58e2e7af4c193aa77ad0695f6c2b9ac2c5c5ec) $ cljstyle check # カレントディレクトリ以下のフォーマットのチェック $ cljstyle fix # カレントディレクトリ以下のフォーマットの修正
どちらも後ろにファイル名をつけることでそのファイルのみのフォーマットを行うことができます。
より詳しい使用方法はcljstyle -h
で見ることができます。
cljstyleの設定
cljstyleは .cljstyle
ファイルにフォーマットの設定を記述します。
.cljstyle
ファイルの内容は以下のような設定をClojureのmapとして記述したものです。
{:files {:extensions #{"clj" "cljs" "cljc" "cljx"} :pattern nil :ignore #{".git" ".hg"}} :rules {:indentation {:enabled? true :list-indent 2 :indents {}} :whitespace {:enabled? true :remove-surrounding? true :remove-trailing? true :insert-missing? true} :blank-lines {:enabled? true :trim-consecutive? true :max-consecutive 2 :insert-padding? true :padding-lines 2} :eof-newline {:enabled? true} :comments {:enabled? true :inline-prefix " " :leading-prefix "; "} :vars {:enabled? true} :functions {:enabled? true} :types {:enabled? true :types? true :protocols? true :reifies? true :proxies? true} :namespaces {:enabled? true :indent-size 2 :break-libs? true :import-break-width 60}}}
ちなみに上の設定は :indent
以外はデフォルトのルールです。
.cljstyle
ファイルの場所
設定ファイルは各フォルダに対して適用させることができます。 例えば以下のようなフォルダ構成の場合、
a ├── .cljstyle └── b ├── c │ ├── .cljstyle │ └── foo.clj └── d ├── .cljstyle └── e └── bar.clj
foo.clj
のフォーマットをする場合には a/b/c/.cljstyle
と a/.cljstyle
が使用されます。
深いディレクトリの設定が優先されるのでこの場合はa/b/c/.cljstyle
が優先されます。
同様に bar.clj
のフォーマットをする場合、a/b/d/.cljstyle
と a/.cljstyle
を前者を優先してマージした設定が使用されます。
各設定の内容
以下では :files
/ :pattern
のような表記は設定のmapの :files に対応するvalueの :pattern
を示します。
:files
/ :extensions
デフォルト: #{"clj" "cljs" "cljc" "cljx"}
フォーマットの対象とするファイルの拡張子を指定します
:files
/ :pattern
デフォルト: nil
フォーマットの対象とするファイルを正規表現で指定します。:files
/ :extensions
の指定と:files
/ :pattern
の指定のどちらかに当てはまったファイルがフォーマットの対象になります。
:files
/ :ignore
デフォルト: #{".git" ".hg"}
文字列か正規表現の Set
でフォーマットの対象としないファイルを指定します。
文字列の場合、一致するファイル名のファイルと、一致するディレクトリ名のディレクトリ以下のファイルは無視されます。
正規表現の場合、パスと一致するファイル、ディレクトリが無視されます。
:rules
/ :indentation
/ :list-indent
デフォルト: 2
リストのインデントサイズを指定します。
;; :list-indent-sizeが2の場合 (foo bar baz) ;; :list-indent-sizeが1の場合 (foo bar baz)
:rules
/ :indentation
/ :indents
コードをフォーマットする際のインデントのルールを指定します。 詳しくは後述します。
:rules
/ :whitespace
/ :remove-surrounding?
デフォルト: true
フォームの内部を囲む空白を ( foo )
=> (foo)
のように削除します。
:rules
/ :whitespace
/ :remove-trailing?
デフォルト: true
行の最後にある空白を削除します。
:rules
/ :whitespace
/ :insert-missing?
デフォルト: true
要素間に (foo(bar))
=> (foo (bar))
のように空白を追加します。
:rules
/ :blank-lines
/ :trim-consecutive?
デフォルト: true
:rules
/ :blank-lines
/ :max-consecutive
で指定される数より多い連続する空行を削除します。
:rules
/ :blank-lines
/ :max-consecutive
のデフォルトは 2
です。
:rules
/ :blank-lines
/ :insert-padding?
デフォルト: true
トップレベルの複数行の非コメントフォーム間に、少なくとも :padding-lines
行の空行をもつようにします。
:padding-lines
のデフォルトは 2
です。
:rules
/ :eof-newline
/ :enabled?
デフォルト: true
ファイル末尾に改行がない場合に改行を追加します。
:rules
/ :comments
/ :inline-prefix
デフォルト: " "
インラインコメント(別のフォームに同じ行で続くコメント)のプリフィックスを指定します。
:rules
/ :comments
/ :leading-prefix
デフォルト: "; "
コメントだけの行のコメントのプリフィックスを指定します。
:rules
/ :vars
/ :enabled?
デフォルト: true
def
フォームが複数行になっているときとdoc-stringを持つ場合に改行を入れます。
具体的には以下のようになります。
;; before (def x (f 1 2)) (def y "doc" 100) ;; after (def x (f 1 2)) (def y "doc" 100)
:rules
/ :functions
/ :enabled?
デフォルト: true
defn
やdefn-
のフォームの内部で改行させます。
また、 fn
フォームが複数行にわたっている場合に改行させます。
具体的には以下のようになります。
;; before (defn f [x] (fn [y] (+ x y))) ;; after (defn f [x] (fn [y] (+ x y)))
:rules
/ :types
/ :enabled?
デフォルト: 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]))
:rules
/ :types
/ :types?
デフォルト: true
deftype
と defrecord
で内部の改行をさせるかを指定します。
:rules
/ :types
/ :protocols?
デフォルト: true
defprotocol
で内部の改行をさせるかを指定します。
:rules
/ :types
/ :reifies?
デフォルト: true
reify
で内部の改行をさせるかを指定します。
:rules
/ :types
/ :proxies?
デフォルト: true
proxy
で内部の改行をさせるかを指定します。
:rules
/ :namespaces
/ :enabled?
デフォルト: 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)))
:rules
/ :namespaces
/ :indent-size
デフォルト: 2
リストのインデントサイズを指定します。
:rules
/ :indentation
/ :list-indent
と同じですが、nsのフォーマットはrequireのソートなど処理が別になっているため、設定も別れています。
:rules
/ :namespaces
/ :break-libs?
デフォルト: true
ns
フォーム内の :require
や:import
の直後に改行を入れるかを指定します。
;; :break-libs? true (ns user (:require [clojure.core.async :as as] [clojure.string :as str]) (:import (java.util ArrayList UUID))) ;; :break-libs? false (ns user (:require [clojure.core.async :as as] [clojure.string :as str]) (:import (java.util ArrayList UUID)))
:rules
/ :namespaces
/ :import-break-width
デフォルト: 60
単一クラスのインポートの文字数が :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)))
インデントの設定
:indents
には以下のようなmapを設定します。
{if [[:block 1]] do [[:block 0]] fn [[:inner 0]]}
マップのkeyがインデントを適用するフォーム、valueがどのようなルールを適用するかを表します。
デフォルトのルールは cljstyle/indents.clj at main · greglook/cljstyle · GitHubにあります。
インデントを適用するフォーム
マップの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)
上のように cond
や case
のような条件と式を複数受け取るようなフォームに使うことが想定されています。
Clojure Style Guide用の設定
github.com 以下のように設定すると上のClojure Style Guideに沿ったソースコードに(全てではないですが)フォーマットでき、弊社で利用しています。
{:rules {:indentation {:list-indent 1} :blank-lines {:max-consecutive 1 :padding-lines 1} :vars {:enabled? false} :functions {:enabled? false} :types {:enabled? false} :namespaces {:indent-size 1}}}
エディタとの連携
vim
cljstyle/integrations.md at main · greglook/cljstyle · GitHubにあります。
IntelliJ IDEA (Ultimate)
IntelliJ IDEA (Ultimate)のファイル監視機能で以下のように設定すると 保存時に自動でフォーマットされます。
ClojureScriptのファイルや .cljc
ファイルなどにも適用する場合は設定のFile TypeをClojureScriptにしたものなどの設定を別で作る必要があります。
最後に
cljstyleの設定方法についてなるべく詳しく説明してみました。 cljstyleでみなさんのClojure lifeがよりよくなることを願っています。
更新履歴
- 2020/02/17 初版
- 2022/06/15 cljstyle 0.15.0の設定ファイル形式に対応しました。
トヨクモ株式会社ではClojureを書きたいエンジニア、PHP/Vue.jsを書きたいエンジニア、技術が好きなエンジニアを募集しております。
よろしければ採用ページをご覧ください。