2025年4月6日日曜日

自作マクロに対するclj-kondoの警告はメタ情報付与で解消できる


背景

プログラミング言語clojureにはマクロ定義機能があります。
それを使うことで冗長な変数呼び出しをまとめたり、処理や変数の定義と後始末の管理が容易になったりします。

VSCodeでclojureを書く際にプラグインcalvaを入れると、clojureのフォルダを開いた際に静的解析プログラム(linter)であるclj-kondoが有効になります。
言語に定義されているマクロ(when や -> など)は取り扱いがclj-kondoで予め定義されているため問題なく扱えます。

問題は自分でマクロを書いた時でして、clj-kondoが扱い方を知らないためマクロを定義しただけでは問題なく実行できるものでも警告が出てしまいます。
調査試行の末にclj-kondoの警告をmeta情報の付与で制御できると分かったので、関連情報や具体例を記事に残します。

使ったもの

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

letやfnなど既存の定義と同様で良いなら、メタ情報として clj-kondo/lint-as で同様の扱いを指示

マクロへのメタ情報付与によってclj-kondoの振る舞いを変更できます。
メタ情報はマクロ名と変数の配列の間に記述します。変数の配列の前です。配列の後では無いです。

下記のマクロは利用時に値の入力と変数の出力(受取)があるので、clj-kondo/lint-asでclojure.core/letとして扱うように指示しています。
マクロへの値の入力が無く値の出力だけなら、clojure.core/fnを指定すると良いです。
適切な設定をすれば警告を解消できます。
(defmacro with-random-value
{:clj-kondo/lint-as 'clojure.core/let}
[[val-random val-max] & body]
`(let [~val-random (rand-int ~val-max)]
(try
~@body
(finally
(println "finished using of " ~val-random)))))

(defn run-macro []
(with-random-value [val-from-macro 100]
(println val-from-macro)))

メタ情報を付与してないと、変数に対して「Unresolved symbol」の警告が出ます。

変数用の配列が可変長の場合はunresolved-symbolを無視させれば警告は消えるが、入出力の判別はしてくれなくなる

macroに対する引数の数に応じて、入力があれば let、無いなら fnとして扱いたい場合は、メタ情報の設定例にあるように unresolved-symbol を無視させれば警告は消せます。
(defmacro with-random-value
{:clj-kondo/ignore [:unresolved-symbol]}
[[val-random & [val-max]] & body]
`(let [~val-random (rand-int (or ~val-max 10))]
(try
~@body
(finally
(println "finished using of " ~val-random)))))

しかしながら、この書き方だとletのように第2引数が入力用の前提で解析してくれないため、静的解析では警告が出ないものの実行時に動かないことが起こり得るので今ひとつです。

lint-as letなら、配列で渡す偶数番目の値が未定義なら警告してくれます。

一方ignore unresolved-symbolだと、値の入出力を識別してくれないようで定義していないval-max-undefinedに警告が出てくれません。

これだと可変長は止めてlint-as letを維持しつつ渡したい値が無い場合はnilを渡す書き方をする方が、静的解析の恩恵を受けられます。

複数の変数の出口と入口を管理する場合は、配列でまとめてlint-as letすれば解析の恩恵を受けられる

出口を最初の配列の1番目の値に配列としてまとめ、入口は最初の配列の2番目の値に配列としてまとめ、入力が無い場合はnilを渡すことで、letと同様の記述をさせて静的解析の恩恵を受けられます。
(defmacro with-mul-plus
{:clj-kondo/lint-as 'clojure.core/let}
[[[val-output-mul val-output-plus] [val-input-a val-input-b]] & body]
`(let [val-a# (or ~val-input-a 1)
val-b# (or ~val-input-b 2)
~val-output-mul (* val-a# val-b#)
~val-output-plus (+ val-a# val-b#)]
(try
~@body
(finally
(println "finished using of " ~val-output-mul " and " ~val-output-plus
" from " val-a# " and " val-b#)))))


独自の定義はclj-kondoのhooksとanalyzerを使えば実現可能らしい

先程紹介したunresolved-symbolを無視させる方法なら警告を消せはするのですが、未定義な値が入力に来ても警告を出してくれなくなるので不都合です。
lint-asでは対応できない独自のマクロに対してclj-kondoを期待通りに動かしたい場合は、hookを使えば良いらしいです。

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

しかしながら、自分が試した範囲では動かせなかったので、この記事でのhooksとanalyzerの解説は止めます。

spec.alphaの定義は参照してくれない

現時点ではclj-kondoはspecの定義を読んでくれないため、specを書いても静的解析の警告は消えません。

「spec書いたのに赤線が消えないのはどうしてだ」と、clj-kondoの挙動を把握するまで戸惑いました。

おわり

マクロを定義したときにclj-kondoが指摘する警告は、マクロのメタ情報として{:clj-kondo/lint-as 'clojure.core/let}や{:clj-kondo/lint-as 'clojure.core/fn}を付与すれば消せると分かりました。
独自の定義のマクロの対してhooksとanalyzerでの解析方法指定はどこか設定が足りてないのか期待通り動かなかったですが、lint-as letの利用で今の所回避できるので一旦は良しです。

参考

Inline macro configuration
Clojureのリンターclj-kondoの使い方
hooks

0 件のコメント :