2025年4月13日日曜日

clj-kondoのhooksを書く


背景

clj-kondoのhooksを使ったメタ情報付与では対応できないマクロに対する解釈の記述方法を把握できたので、関連情報を記事に残します。
前回の自作マクロに対するclj-kondoの警告をマクロへのメタ情報付与で解消する方法をまとめたときに動かし方が分からず諦めた内容です。

使ったもの

  • VSCode v1.98.2
  • calva v2.0.495
    これを通してclj-kondoが呼び出されます。

この記事での設定作成対象 macro-practice.macros/with-random-value

この記事では下記のmacro-practice.macros/with-random-valueに対してclj-kondoの設定を施します。
第1引数が戻り値、第2引数が省略可能な値を受け取るマクロです。
[project dir]/src/macro_practice/macros.clj
(ns macro-practice.macros)

(defmacro with-random-value
[[val-random & [val-max]] & body]
`(let [~val-random (rand-int (or ~val-max 10))]
(try
~@body
(finally
(println "finished using of " ~val-random)))))

hooksが設定されてないと、呼び出した箇所に「Unresolved symbol」の警告が表示されます。


設定内容: .clj-kondoにconfigとhooksを置く

deps.ednを置いているプロジェクトのディレクトリの内の.clj-kondo ディレクトリ(多分calvaを通して既に作られている)にconfigとhooksを置きます。
hooksディレクトリやその中のファイルは.clj-kondoディレクトリ内なら別の名前でも良いのですが、clj-kondoのreadmeに合わせてhooksディレクトリ内に配置しています。
下記の記事やclj-kondoのテストコードを参考にしつつ組み立てました。

Clojureのリンターclj-kondoの使い方
doc/hooks.md

任意の引数があればlet形式、無ければfn形式で扱うhookです。
「&」後に受け取る任意の引数はvectorとして解釈して中身を受け取る必要があります。
vectorとして受け取ったものに:children要素がある条件が自分は分かっておらず、試行錯誤で期待通りに解釈できる表記を探った結果が下記のhookです。
[project dir]/.clj-kondo/hooks/macros.clj
(ns hooks.macros
(:require
[clj-kondo.hooks-api :as api]))

(defn with-random-value [{:keys [node]}]
(let [[binding-vec & body] (rest (:children node))
[val-random & opt-vec] (:children binding-vec)
[val-max] opt-vec] ; opt-vecはchildrenではなくそのまま展開
{:node (api/list-node
(if val-max
(list*
(api/token-node 'let)
(api/vector-node [val-random val-max])
body)
(list*
(api/token-node 'fn)
(api/vector-node [val-random])
body)))})) 

設定対象の関数と定義したhookをanalize-callで紐付けます。
[project dir]/.clj-kondo/config.clj
{:hooks {:analyze-call
{macro-practice.macros/with-random-value hooks.macros/with-random-value}}}

config.ednの評価はファイル保存で行われますが、hooksの中身の解釈はVSCodeを開いたときしか行われないようなので、VSCodeを開き直すか Ctrl + Shift + pを押して「ウィンドウの再読込」を検索して実行します。

設定が成功するとと、任意の引数val-maxがあっても無くても警告が表示されなくなります。


第2引数に未定義の変数を渡すと、期待通りに警告してくれます。


ns (namespace)へのmeta情報でのhooks設定は、そのns内でしか有効にならない

.clj-kondo/config.edn無しでsrc/macro_practice/macros.cljのnsにhooks設定を施すと、macros.clj内では警告が消えます。


しかしながら、このnsを他のnsで呼び出すとその設定は反映されずに警告が出ます。


このため、記事を書いている時点では他のnsでも呼び出せる関数のhooksを設定したい場合は.clj-kondo/config.ednで行うのが良いです。

hooksのapi/list-nodeに渡すのが、fnはlist*、defは[]、letはどちらでもよく、記述が統一されていない

下記の記事を参考に'defに対してhookを記述したところ、自分の環境ではlist*ではなく[]での定義が必要でした。

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

clj-kondoのtestで'defに対して[]で要素を渡しているのを見つけ、それを真似たら期待通りに動きました。

clj_kondo/analysis_test.clj#L1024

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

clj-kondoのapi/list-nodeへの情報受け渡しは[]に変わったのかと思って多用していると、'fnに対してはlist*で渡さないと動かない現象が発生しました。
        ; 'fnをlsit*ではなく[]で定義すると、解釈がおかしくなる
[(api/token-node 'fn)
(api/vector-node [val-random])
body]

fnとして解釈されようとはしているものの、引数を認識できていなさそうな「unused binding」の警告が出ました。


list-nodeなのでlist*を渡すのが正しそうなので、'defの解釈に問題がありそうでした。
'defのhooksを作る場合はご注意ください。

おわり

clj-kondoのhooksを設定し、任意の長さの引数を受け取るmacroの解釈を期待通りに動かせました。
しかしながら、clj-kondoのhooksは隠しフォルダ.clj-kondoに記述するためgitで管理するのに抵抗を感じる上にmacroの定義とファイルが別れてしまうので、前回の記事で紹介したmacroにmeta情報としてlint-asを付与する方法の方がmacroの定義と同じファイルに記述できるため保守しやすくて良いと思いました。
任意の長さではなく不要な場合はnilを渡す形式ならlint-as letで事足ります。

参考

Clojureのリンターclj-kondoの使い方
doc/hooks.md
clj_kondo/analysis_test.clj#L1024
自作マクロに対するclj-kondoの警告はメタ情報付与で解消できる

0 件のコメント :