Toyokumo Tech Blog

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

Clojureのリンターclj-kondoの使い方

開発本部の飯塚です。

今回は Clojure のリンターである clj-kondo の使い方をちょっと踏み込んだところまで説明できたらと思います。

github.com

Clojure でプログラムを書いている人であればすでにほとんどの方は使っているのではと思うのですが、 もし使っていなければ使う手助けに、使っている場合はこんな事もできるかという発見に繋がれば良いなと思います。

なお clojure-lsp は clj-kondo を使っていて、Calva は clojure-lsp を使っているので、直接 clj-kondo を使っていなくても恩恵に預かってる方も多いかと思います。

導入

まずは clj-kondo コマンドが使えるようにしましょう。 導入手順は以下にまとまって、各種パッケージマネージャーでも利用可能なので導入で手間取ることはないかと思います。

github.com

各エディタでの設定については以下からご利用のエディタの記事を参照してください。

tech.toyokumo.co.jp

使い方

エディタ連携が出来ているとエディタ上で警告やエラーを表示してくれますが、一旦 clj-kondo コマンドでの使い方を説明します。(後段でも clj-kondo を利用するため)

$ clj-kondo --lint リント対象のファイルもしくはディレクトリ

基本はこの --lint オプションです。 リント対象が複数ある場合は --lint src:test のようにコロン区切りで指定できます。

以下に簡単な実行例を載せておきます。 使われていない private な var に関して警告が出ていることがわかるかと思います。

$ cat src/foo/core.clj
(ns foo.core)

(def ^:private hello "world")

$ clj-kondo --lint src
src/foo/core.clj:3:16: warning: Unused private var foo.core/hello
linting took 189ms, errors: 0, warnings: 1

それ以外のオプションは --help オプションの出力を参照してください。

設定

clj-kondo では様々な種類のリンターが用意されていて、それぞれに対して細かく設定を定義することができます。

github.com

設定ファイルはいくつかの場所に配置可能ですが、基本的にはプロジェクト単位で設定内容も変わるのでプロジェクトディレクトリ配下に .clj-kondo/config.edn を用意するのが一般的かと思います。

以下に設定されがちな内容を含む config.edn のサンプルを載せておきます。

{;; 各種リンターの設定
 :linters {;; 未解決できない var に関する設定
           :unresolved-symbol {:exclude [;; foo という var すべてを除外
                                         foo
                                         ;; clojure.core/seq 内で使われている bar という var を除外
                                         (clojure.core/seq [bar])]}}

 ;; 既存のリントを使い回す設定
 :lint-as {;; mydef は clojure.core/def とみなす
           foo.core/mydef clojure.core/def}

 ;; 除外設定
 :output {:exclude-files [;; ファイルの除外
                          "foo/bar.clj"
                          ;; ディレクトリの除外
                          "baz"]}}

その他、どんなリンターがあり、どのように設定するかは以下を参照してください。 github.com

マクロ

clj-kondo は Clojure のコードを静的に解析して結果を出しているため、 コンパイル時に別の式に展開されるマクロを使ったコードはそのままでは正しくチェックしてくれません。 (ビルトインで提供されているマクロは clj-kondo 側でよしなにしてくれるので、主に自前で定義したマクロが対象です)

例えば以下ようなマクロの場合、clj-kondo は mydef の展開結果を知らないので fooUnresolved symbol 警告を出してしまいます。

(defmacro mydef [sym value]
  `(def ~sym ~value))

(mydef foo 1)

この警告を解決するには lint-as を使う方法と hooks を使う方法の2つがあります。 lint-as については config.edn のサンプルとして既に紹介していました。

 ;; 既存のリントを使い回す設定
 :lint-as {;; mydef は clojure.core/def とみなす
           foo.core/mydef clojure.core/def}

この設定により mydefclojure.core/def とみなされるので fooUnresolved symbol にならなくなります。

Hooks

では以下のようなマクロだった場合はどうでしょうか? 引数の順番がおかしいので今回は lint-as は使えません。

(defmacro myrdef [sym value]
  `(def ~value ~sym))

(myrdef 2 bar)

この例に限らず、少し複雑なことをマクロで実現しようと思うと lint-as でカバーできないことは多々あるかと思います。 そのときに使えるのが Hooks という機能です。

github.com

Hooks では babashka/sci を使って展開結果を clj-kondo に教えてあげることで、マクロを正しくチェックできるようにする機構です。 展開処理は前述の通り Clojure 本体ではなく babashka/sci により行われるので、defmacro のコードとは別に展開するためのコードが必要になることに注意が必要です。

まずは展開処理を用意します。細かい書き方は Hooks のドキュメントを参照してください。ここでは雰囲気だけ掴んでください。 プロジェクトディレクトリ配下に .clj-kondo/hooks/myrdef.clj を以下の内容で作ってください。

(ns hooks.myrdef
  (:require
   [clj-kondo.hooks-api :as api]))

(defn myrdef
  [{:keys [node]}]
  (let [[value sym] (rest (:children node))
        new-node (api/list-node
                   (list* (api/token-node 'def)
                          sym
                          value))]
    {:node new-node}))

次に用意した Hook を利用する設定を .clj-kondo/config.edn に追加します。

{:lint-as {foo.core/mydef clojure.core/def}

 ;; 追加
 :hooks {:analyze-call {foo.core/myrdef hooks.myrdef/myrdef}}}

これにより clj-kondo は (myrdef 2 bar)(def bar 2) に展開されることを知れ、ビルドインの clojure.core/def に対するものと同じチェックを実施してくれます。

上記の例だけだと任意のマクロに対して Hooks を実際に設定するのは難しいと思います。 clj-kondo では以下のリポジトリにていくつかのライブラリに対する Hooks を含む設定を公開しているので、ライブラリ側の実装と設定を見比べると理解しやすいかと思います。

github.com

ライブラリが提供する設定の取り込み

各種リンターの設定や Hooks の設定は自プロジェクトで定義している関数やマクロ向けのものはともかく、 外部ライブラリで提供されているものに関しても自分で設定するのは面倒です。(特に Hooks は展開するためのコードを書かないとなので、ライブラリ側の動作を把握する必要があり難易度が高い)

幸い clj-kondo ではライブラリが提供する clj-kondo 向け設定を取り込む機能があります。

例として seancorfield/next-jdbc が提供する clj-kondo 向け設定を取り込む方法を紹介します。 まず deps.edn に seancorfield/next-jdbc への依存を追加します。

{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}
        ;; 追加
        com.github.seancorfield/next.jdbc {:mvn/version "1.2.780"}}}

そして以下のコマンドを実行してください。

$ clj-kondo --lint "$(clojure -Spath)" --copy-configs --skip-lint

すると .clj-kondo/com.github.seancorfield/next.jdbc 配下に設定が取り込まれていることが確認できるかと思います。 これらの設定は自動的に読み込まれるので .clj-kondo/config.edn で Hooks の設定を追加する必要はありません。

このように設定が難しい Hooks についてもライブラリ作者が提供さえしてくれていれば簡単に利用することが可能です。 ちなみにライブラリ側で提供している設定の実体は以下のように resources/clj-kondo.exports 配下に置かれたファイルになります。

https://github.com/seancorfield/next-jdbc/tree/develop/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc

CI の設定

ここまででローカルでの clj-kondo のチェックならびに設定変更の方法がある程度わかっていただけたかと思います。 なので次は CI でもローカル同様にチェックできるようにしましょう。

弊社では CI に GitHub Actions を使っているので、GitHub Actions を使った例に限られること予めご承知おきください。

ただやることは至極簡単で .github/workflows/lint.yml を以下の内容で用意するだけです。

name: Lint
on: push

jobs:
  clj_kondo:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: DeLaGuardo/setup-clojure@9.2
        with:
          clj-kondo: 'latest'
      - run: clj-kondo --lint src

clj-kondo コマンドのセットアップに使っている DeLaGuardo/setup-clojure は clj-kondo 以外にも Clojure に関するいくつかのコマンドのセットアップに対応しているので何かとお世話になるかと思います。

github.com

番外編: 解析データの利用

clj-kondo はリンターなのでリンターとしてのみ使っている方がほとんどだと思います。 ここでは clj-kondo の大きな強みの1つとして解析データを利用できることを知ってもらえればと思います。 (clojure-lsp はこの解析データを使って様々な機能を提供しているはずで、拙作の vim-iced でもこの解析データを利用した機能を提供しています)

github.com

おもむろに以下のコマンドをプロジェクト配下で実行してみてください。 JSON が大量に出力されたかと思います。これが clj-kondo による解析データです。

$ clj-kondo --lint src:test --config '{:output {:analysis true :format :json}}'

この解析データでは例えば以下ようなことがわかります。(勿論これがすべてではありません)

  • namespace の定義情報(どのファイルでどのnsが定義されているか)
  • namespace の利用情報(どのファイルでどのnsがどのnsをどういうエイリアスで利用しているか)
  • var の定義情報(どのファイルでどのvarがどこに定義されているか)
  • var の利用情報(どのファイルでどのvarがどのvarを利用しているか)

では実用の例として統一されていない namespace エイリアスを検出するためのデータ取得を紹介します。

$ clj-kondo --lint src:test --config '{:output {:analysis true :format :edn}}' \
    | jet -q ':analysis :namespace-usages (map (select-keys [:to :alias])) (filter :alias) (remove (= :alias #jet/lit sut)) distinct' \
    | bb -I '(->> (first *input*) (group-by :to) (filter #(>= (count (second %)) 2)) clojure.pprint/pprint)'

jq のクエリは私には覚えきれなかったので、出力フォーマットは EDN として jet を使って解析データの整形をしています。(Clojurian としてはこちらのクエリの方が覚えやすい) やっていることは :namespace-usages の中からどのnsをどのエイリアスで使っているかの抜き出しが基本です。 sut の除外などはプロジェクトによっては不要だと思うのでクエリは適宜修正してください。

これを実行すると同じ ns 名に異なるエイリアスが2つ以上あるものを列挙してくれます。 今回は無理やりワンライナーにしましたが、出力した JSON/EDN をREPL 上であれこれいじるのも楽しいかと思います。

最後に

いかがだったでしょうか? ここまでの内容を通して最初に書いた以下の文が達成できていたら幸いです。

もし使っていなければ使う手助けに、使っている場合はこんな事もできるかという発見に繋がれば良いなと思います。


トヨクモでは一緒に働いてくれる技術が好きなエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。