clj-kondo

borkdude 2026-05-04T12:43:32.656429Z

😍

🤩 7
1
borkdude 2026-05-11T13:54:30.972049Z

@qythium and others: please try ac41fb65b8d0a8df104e2d907614b971c2c8df3b for the new "macros from source" feature.

(defmacro deffoo 
  {:clj-kondo/macro true}
  [x]
  `(def ~x :foo))

(deffoo x)

x ;; x is recognized as var now!

😍 1
🎉 5
exitsandman 2026-05-11T18:35:29.384049Z

(ns emi.enclojures.kondo-xp
  (:require [clojure.string :as ztr]))

(declare -not-in-kondo)

(defmacro if-kondo
  {:clj-kondo/macro true}
  [kondo clj]
  (if-not (resolve '-not-in-kondo)
    kondo
    clj))

(defmacro resolves
  {:clj-kondo/macro true}
  []
  (if-kondo
    (throw (ex-info (str ztr/blank?) {}))
    nil))

(defmacro doesnt-resolve
  {:clj-kondo/macro true}
  []
  (throw (ex-info (str `ztr/blank?) {})))

(resolves) ;; ok

(doesnt-resolve) ;; kondo gives back `ztr/blank? while clj gives back `str/blank?
my biggest pain point though is that it doesn't seem to play well with hooks, which would require a bit of a migration

borkdude 2026-05-11T18:38:10.485999Z

more details about that = better

exitsandman 2026-05-11T18:39:56.765239Z

(resolves) raises an error with text clojure.string$blank_QMARK_@3ccb0625, i.e. kondo manages to grab the clojure.core/str? object as I would expect, while (doesnt-resolve) can't resolve the ns through the quasiquote and raises an error with content ztr/blank?.

borkdude 2026-05-11T18:42:37.575689Z

this should already work. have you tried the newest commit?

exitsandman 2026-05-11T18:42:59.952059Z

I'm on ac41fb65b8d0a8df104e2d907614b971c2c8df3b , will look now

borkdude 2026-05-11T18:43:40.100429Z

ok try dc52bbb36589cc4c21391df94fa41de9d914ac91 (this is only a cleanup commit though) and make sure you restart clojure-lsp or whatever you're using

borkdude 2026-05-11T18:43:48.577219Z

or better, use the command line

exitsandman 2026-05-11T18:43:58.594929Z

will do

exitsandman 2026-05-11T18:45:59.459719Z

fwiw this is the first time I'm trying the feature so there shouldn't be anything stale

borkdude 2026-05-11T18:46:41.936199Z

$ cat src/emi/enclojures/kondo_xp.clj
(ns emi.enclojures.kondo-xp
  (:require [clojure.string :as ztr]))

(declare -not-in-kondo)

(defmacro if-kondo
  {:clj-kondo/macro true}
  [kondo clj]
  (if-not (resolve '-not-in-kondo)
    kondo
    clj))

(defmacro resolves
  {:clj-kondo/macro true}
  []
  (if-kondo
    (throw (ex-info (str ztr/blank?) {}))
    nil))

(defmacro doesnt-resolve
  {:clj-kondo/macro true}
  []
  (throw (ex-info (str `ztr/blank?) {})))

(resolves)
(doesnt-resolve)
$ cd /tmp/kondo-xp && clojure -M:clj-kondo/dev --lint src
src/emi/enclojures/kondo_xp.clj:25:1: error: clojure.string$blank_QMARK_@3a027c11
src/emi/enclojures/kondo_xp.clj:26:1: error: clojure.string/blank?

exitsandman 2026-05-11T18:47:51.561759Z

neat, let me just set things up and will try

exitsandman 2026-05-11T18:49:07.437629Z

yes, looks fixed both in cmdline and in the clojure-lsp build using that hash

borkdude 2026-05-11T18:49:36.632369Z

you are running clojure-lsp in a jvm or do you compile it ?

exitsandman 2026-05-11T18:49:55.414569Z

jvm

borkdude 2026-05-11T18:50:04.683209Z

makes sense. me too

borkdude 2026-05-11T18:51:57.701989Z

neat if-kondo trick btw

exitsandman 2026-05-11T18:59:17.390699Z

written badly lmao, should resolve the ns-qualified symbol as in the-util-ns.not-in-kondo`

borkdude 2026-05-11T18:59:33.569499Z

?

exitsandman 2026-05-11T19:00:33.794939Z

otherwise it breaks outside or not-in-kondo's ns

exitsandman 2026-05-11T19:01:42.341189Z

at any rate, this feature is great

borkdude 2026-05-11T19:01:46.376679Z

yeah, I don't mind the details, but this trick will work for stuff that should not run in kondo

borkdude 2026-05-11T19:03:22.628269Z

I'm waiting for @qythium to report back and then I'll probably merge if there aren't any other complaints

yuhan 2026-05-08T15:38:44.574649Z

Tried out the new commit, but it still reported a Unresolved symbol: b on the above expr, which I then found was independent of the xref issue - seems that it only analyses up to one level of expansion? ie. with the {:pre ...} commented out so it's self-contained - (when-every [a 12, b 34] (+ a b)) expands to (when-let [a 12] (when-every [b 34] (+ a b))) and the inner form isn't getting sci-macroexpanded.

yuhan 2026-05-08T15:51:47.290749Z

Another example - notice if you swap the order of clauses the _ and n bindings get recognized correctly.

(defmacro cond+
  "clause => test expr | [binding-form test] expr
  Like [[clojure.core/cond]], but literal vectors in test position
  are interpreted as if-let bindings.
  Also supports :let and :do clauses as in better-cond"
  {:arglists '([& clauses])
   :clj-kondo/macro true}
  ([] nil)
  ([test expr & clauses]
   (cond
     (vector? test) `(if-let ~test ~expr (cond+ ~@clauses))
     :else `(if ~test ~expr (cond+ ~@clauses)))))

(let [x "a123b"]
  (cond+ (number? x) x
         [[_ n] (re-matches #"a(\d+)b" x)] (parse-long n)
         :else 0))

yuhan 2026-05-08T15:53:59.561739Z

oh and both these macros are handled correctly if I use the existing :hooks :macroexpand config and simply copy them verbatim

yuhan 2026-05-08T17:01:53.119129Z

Seems like a syntax quote resolution issue - if I edit the recursive calls to use fully qualified heads like (my-ns/cond+ ~@clauses) then it works out. I thought this meant it could be solved by generating another mapping {clj-kondo.gen-macros.my-ns/cond+ clj-kondo.gen-macros.my-ns/cond+} to the config.hooks.macroexpand but that didn't seem to work either

borkdude 2026-05-08T19:58:15.602179Z

Cool if you can repro this to the smallest possible issue I'll take a look on Monday-ish. In the middle of conferences right now

yuhan 2026-05-06T10:38:51.838529Z

Amazing, been wishing for something like this for some time~ It seems like cross-ns references aren't supported though, the analysis only extends to 'helper fns' within the same file? repro:

(ns blub.utils)

(defn binding-vec?
  {:clj-kondo/macro true}
  [v] (and (vector? v) (even? (count v))))

(defmacro dummy {:clj-kondo/macro true} []) ; to generate the file
(ns blub.kondo
  (:require [blub.utils :as u]))

(defmacro when-every
  "Like [[clojure.core/when-let]], but takes any number of binding-test pairs.
  Only executes body if all tests are logical true, otherwise nil.
  Short circuits on first failure."
  {:clj-kondo/macro true}
  [bindings & body]
  {:pre [(u/binding-vec? bindings)]}
  (if (seq bindings)
    `(when-let ~(subvec bindings 0 2)
       (when-every ~(subvec bindings 2)
         ~@body))
    (cons 'do body)))

(when-every [a 12, b 34] (+ a b))
$ clj-kondo --lint src/blub/kondo.clj
WARNING: file blub/utils not found while loading hook
WARNING: error while trying to read hook for blub.kondo/when-every: Could not find namespace: blub.utils.
src/blub/kondo.clj:17:14: error: Unresolved symbol: a
src/blub/kondo.clj:17:20: error: Unresolved symbol: b
linting took 18ms, errors: 2, warnings: 0

borkdude 2026-05-06T12:36:57.287609Z

Not supported .. yet :)

borkdude 2026-05-06T12:55:41.384829Z

let me try to fix it

borkdude 2026-05-06T13:10:31.246949Z

can you try commit 70523461a0e6d578acedb7bf9b226307842564bc?

yuhan 2026-05-12T15:23:54.149959Z

nice, seems to be working on the previous examples, I'll try it out against some hairier macros and report back in a bit.

yuhan 2026-05-12T15:24:17.663549Z

Have you settled re. the naming/API though - I found the use of :clj-kondo/macro true meta a bit odd how it's playing double duty for both the helper defns and the macros themselves

borkdude 2026-05-12T15:24:47.819429Z

I found that a bit odd too. would you prefer :clj-kondo/macro-helper true for the other stuff?

2026-05-12T15:25:22.428469Z

why do the helpers need metadata?

borkdude 2026-05-12T15:25:46.219259Z

because clj-kondo needs to know they should be copied along into the config code

yuhan 2026-05-12T15:26:10.942459Z

yeah I was curious about that too - isn't clj-kondo's analyzer technically able to trace the functional dependencies or something

borkdude 2026-05-12T15:26:12.754829Z

you're absolutely right that clj-kondo can analyze this itself but it complicates stuff

borkdude 2026-05-12T15:26:33.395619Z

I wanted to release this first and maybe make the other annotation optional later

👍 2
borkdude 2026-05-12T15:29:08.980759Z

I think the annotation might be better so people are more aware of that it's still limited to what clj-kondo can execute - not arbitrary things that pull in libs

☝️ 1
yuhan 2026-05-12T15:30:23.099189Z

wondering if it makes sense to extend the existing :clj-kondo/lint-as meta key, since they're sort of mutually exclusive(?) Something like

(defmacro foo {:clj-kondo/lint-as :self} ...) ; or :macroexpanded etc

borkdude 2026-05-12T15:31:53.340129Z

neh

exitsandman 2026-05-12T15:33:35.818379Z

my two cents are on :clj-kondo/for-macroexpansion , or even :clj-kondo/macroexpand-helper uniformly

yuhan 2026-05-12T15:40:40.492599Z

getting a false positive unused-binding here

(defmacro mylet
  {:clj-kondo/macro true}
  [bindings & body]
  `(let ~bindings ~@body))

(mylet [x 123] `(list ~x))
$ clj-kondo --lint src/blub/kondo.clj
src/blub/kondo.clj:10:9: warning: unused binding x

yuhan 2026-05-12T15:48:55.413089Z

also ran into a bit of a hiccup with some macros that close over their helper fns - there's no var in this case to attach metadata to

(letfn [(helper [x] x)]
  (defmacro foo
    {:clj-kondo/macro true}
    [x]
    (helper x)))

exitsandman 2026-05-12T15:49:41.454279Z

first issue has been a thing for a while even in the main

❓ 1
borkdude 2026-05-12T15:55:21.327369Z

please post the first as a separate issue

borkdude 2026-05-12T15:55:27.359899Z

if it is one

borkdude 2026-05-12T15:56:02.695099Z

should we support putting :clj-kondo/macro true on top level forms?

yuhan 2026-05-12T16:02:38.096829Z

oh and I guess this is more of a DX issue but when something like this goes wrong with the SCI evaluation side of things, it doesn't get surfaced in-editor when being used as a flycheck linter? ie. I can only see it printed as a WARNING: error while trying to read hook for blub.kondo/foo: Could not resolve symbol: helper at the top of the call to clj-kondo --lint as a CLI tool

yuhan 2026-05-12T16:02:48.926159Z

oh and valid forms further down in the copied ns don't get run as a result, which can be quite confusing

borkdude 2026-05-12T16:04:13.400779Z

good feedback, I'll take a look at this soon

exitsandman 2026-05-12T18:03:59.962839Z

issue created for the false positive of unused-var

borkdude 2026-05-05T10:24:54.739189Z

I'm going to look into your macro now @k13gomez

borkdude 2026-05-05T10:25:26.089019Z

what is parse-args, can you please make it self-contained?

borkdude 2026-05-05T10:26:33.136959Z

ok, I can reproduce with:

(defmacro defdata
  {:clj-kondo/macro true}
  [& vargs]
  (let [[sym args] vargs]
    `(def ~sym (cons 'do '~args))))

(defdata some-random-id
         "c44eb844-c10e-4726-a529-b7d68452452f")

borkdude 2026-05-05T10:28:37.300819Z

wait, it did work, just needed to restart lsp for some reason, perhaps local dev changes. @k13gomez perhaps you can test this from the command line and make sure you are using the right SHA.

borkdude 2026-05-05T10:29:18.515239Z

your issue may be that parse-args is a function separately from the macro. this isn't supported yet. let me try to work on a solution for that.

jgomez 2026-05-05T12:05:02.115689Z

Yeah I figured, I’ll see if I can make it self contained for some of the macros I maintain, this will be a huge help even if only for self contained macros

jgomez 2026-05-05T12:05:44.561019Z

I was testing it from the command line, built the binary from your branch using GraalVM

borkdude 2026-05-05T12:06:22.841959Z

ok, no need to build the command line, you can just invoke the main function with clj -M -m clj-kondo.main --lint ...

borkdude 2026-05-05T12:06:39.393199Z

I have a global dev alias for this so I can invoke it from any dir

👍 1
borkdude 2026-05-05T15:07:52.546749Z

@k13gomez this example now works:

(defn parse-args
  {:clj-kondo/macro true}
  [args] args)

(defmacro defdata
  {:clj-kondo/macro true}
  [& vargs]
  (let [[sym args] (parse-args vargs)]
    `(def ~sym (cons 'do '~args))))

(defdata xsome-random-id
         "c44eb844-c10e-4726-a529-b7d68452452f")

xsome-random-id
You have to mark the helper function with :clj-kondo/macro true as well and it will be saved along with the macro

borkdude 2026-05-05T15:08:31.409709Z

(exact details are up for debate)

jgomez 2026-05-05T15:09:20.517839Z

ohh nice, Ok let me give that a try

jgomez 2026-05-05T15:34:04.075829Z

Ok, progress report, I couldn't get my exact use case to work initially because it was using clj-commons.digest/md5 for something, so I would get this error:

WARNING: file clj_commons/digest not found while loading hook
WARNING: error while trying to read hook for rules.data/defdata: Could not find namespace: clj-commons.digest.
The thing is, after removing the use of clj-commons.digest namespace I would still get that error. So I debugged it a bit, and what I found was that the autogenerated .clj-kondo/clj_kondo/gen_macros files are not updated after making changes (even running copy-configs, and other things I tried like passing --cache false). Once I removed .clj-kondo/clj_kondo/gen_macros and then re-ran copy-configs, it worked.

borkdude 2026-05-05T15:37:11.922239Z

It should work. I just fixed that! :) are you using the newest SHA?

jgomez 2026-05-05T15:38:35.666489Z

8e31cccb30ff957c66e135ef51de35a80e990bf9

jgomez 2026-05-05T15:39:20.904859Z

Let me see if I can reproduce again

jgomez 2026-05-05T15:43:39.106319Z

Ok, I reproduced it again and what appears to not be updating is the ns declaration of the generated gen-macros namespace, for example after removing clj-commons.digest it still appears in the ns decl.

(ns clj-kondo.gen-macros.rules.data
  (:require [clj-commons.digest :as digest]
            [clara.rules :as-alias r]))

jgomez 2026-05-05T15:44:19.327449Z

the body of the macro was updated and the digest use was removed, but the top level :require in the auto-gen'd namespace is what is out of sync

borkdude 2026-05-05T15:51:50.332839Z

good feedback, let me look at this

borkdude 2026-05-05T15:57:16.679999Z

should be fixed, try again

jgomez 2026-05-05T16:07:11.354239Z

Confirmed, that is fixed, however ran into another edge case, if I add {:clj-kondo/macro true} to a macro, but later remove it, it remains in the auto-gen'd namespace

jgomez 2026-05-05T16:08:44.917079Z

in this example, the generated code below remained the same even after removing the macro meta from defun

(ns clj-kondo.gen-macros.rules.data)

(defmacro defun "defines a new function in the current namespace with the specified symbol name and body value using intern data so that it can be serialized" {:clj-kondo/macro true} [vargs] (let [[sym & args] (parse-args vargs &form)] `(defun.core/defun ~sym ~@args)))

(defmacro defdata "defines a new data var in the current namespace with the specified symbol name and body value in an implicit do using intern data so that it can be serialized" {:clj-kondo/macro true} [sym & args] `(def ~sym (do ~@args)))
It fails because parse-args is not marked as {:clj-kondo/macro true} which I had done on purpose

jgomez 2026-05-05T16:09:38.327119Z

The macros look like this currently:

(defmacro defdata
  "defines a new data var in the current namespace with the specified symbol name and body value in an implicit do using intern data so that it can be serialized"
  {:clj-kondo/macro true}
  [sym & args]
  `(def ~sym (do ~@args)))

(defmacro defun
  "defines a new function in the current namespace with the specified symbol name and body value using intern data so that it can be serialized"
  [vargs]
  (let [[sym & args] (parse-args vargs &form)]
    `(defun.core/defun
       ~sym ~@args)))

borkdude 2026-05-05T16:27:13.840449Z

try again

jgomez 2026-05-05T17:13:38.289139Z

Ok, I tested it again, read the code a bit to understand better what is happening, this is what I am observing: • the manifest file is being updated after removing the :clj-kondo/macro meta from a macro • the generated namespace is still not being updated and still contains the faulty macro A bit verbose perhaps but see the state of files below:

$ tree .clj-kondo/clj_kondo
.clj-kondo/clj_kondo
└── gen_macros
    └── rules
        └── data.clj

3 directories, 1 file

$ tree .clj-kondo/inline-configs
.clj-kondo/inline-configs
└── rules.data.clj
    ├── config.edn
    └── gen-macros.edn

2 directories, 2 files

$ cat .clj-kondo/clj_kondo/gen_macros/rules/data.clj
(ns clj-kondo.gen-macros.rules.data)

(defmacro defun "defines a new function in the current namespace with the specified symbol name and body value using intern data so that it can be serialized" {:clj-kondo/macro true} [vargs] (let [[sym & args] (parse-args vargs &form)] `(defun.core/defun ~sym ~@args)))

(defmacro defdata "defines a new data var in the current namespace with the specified symbol name and body value in an implicit do using intern data so that it can be serialized" {:clj-kondo/macro true} [sym & args] `(def ~sym (do ~@args)))

$ cat .clj-kondo/inline-configs/rules.data.clj/config.edn
{:hooks {:macroexpand {rules.data/defdata clj-kondo.gen-macros.rules.data/defdata}}}

$ cat .clj-kondo/inline-configs/rules.data.clj/gen-macros.edn
[{:orig-ns rules.data, :fn-name defdata, :gen-ns clj-kondo.gen-macros.rules.data, :form-str "(defmacro defdata \"defines a new data var in the current namespace with the specified symbol name and body value in an implicit do using intern data so that it can be serialized\" {:clj-kondo/macro true} [sym & args] `(def ~sym (do ~@args)))", :aliases {}}]

borkdude 2026-05-05T18:24:15.550969Z

what commit SHA are/were you on?

jgomez 2026-05-05T19:09:08.514529Z

on 9c39b1ee6e8b1a3304ae1d85cd311b8d6eb063b1

borkdude 2026-05-05T19:12:05.481289Z

can you try 80ef37e08e3191f2cd9163af55c0744f9ed725e3 - not sure if it fixes this specific problem, if not, I'll try to reproduce yours locally, if I have the code

jgomez 2026-05-05T19:23:59.584619Z

testing it now, if this still won't fix it I will put together the code in a separate micro repo and push it to github

borkdude 2026-05-05T19:25:27.862699Z

that seems good, thanks!

jgomez 2026-05-05T19:30:24.123889Z

it's fixed! works for me now, the files are updated correctly each time

🎉 2
borkdude 2026-05-05T21:06:38.894329Z

Clj-kondo loading a macro from source using the :clj-kondo/macro annotation. Helper functions that are used in the macro should also be annotated. The only restriction is that vars used in the macro at compile time are available in the SCI env where the macro runs (so clojure.core, set, walk, etc are available).

👏 1
5
borkdude 2026-05-19T11:01:18.479019Z

Merged this to master now. Please re-test. I documented the caveats in doc/hooks.md. Won't fix these for next release, so anything beyond that is expected.

exitsandman 2026-05-19T11:58:23.998129Z

heads up that macro was changed into macroexpand-hook

borkdude 2026-05-19T12:00:22.879949Z

yes

borkdude 2026-05-19T12:00:33.991419Z

{:clj-kondo/macroexpand-hook true} for both macros and helpers

exitsandman 2026-05-19T12:09:11.190319Z

looks good to me. BTW, re: the if-kondo docs, was it intentional to leave the version that tries to resolve an unqualified symbol?

borkdude 2026-05-19T12:13:26.107539Z

PR welcome if you want to change it, I didn't think very hard about this one

exitsandman 2026-05-19T12:13:46.606059Z

will do

exitsandman 2026-05-19T12:14:27.610829Z

on which branch?

borkdude 2026-05-19T12:15:02.835909Z

master

borkdude 2026-05-19T12:15:07.119379Z

it's been merged already?

exitsandman 2026-05-19T13:41:54.869819Z

alright, I'll do a simple PR; the test does pass as before. However, of course testing this requires to run a program because the bug really comes up at runtime where the unqualified var isn't resolved properly across namespaces.

borkdude 2026-05-19T13:42:20.697049Z

riiight

borkdude 2026-05-19T13:58:39.947189Z

merged. Thanks for testing everyone!

🎉 2
2026-05-04T13:19:33.458769Z

YOOOOOOOOOOOOO

exitsandman 2026-05-04T13:39:18.025899Z

is this on a dev build?

borkdude 2026-05-04T13:42:19.295679Z

it's locally in a branch :)

jgomez 2026-05-04T13:55:41.534549Z

90% of the linters I end up writing are because of macros

2026-05-04T13:55:49.636849Z

me too

exitsandman 2026-05-04T14:06:36.348649Z

looking forward to it!

borkdude 2026-05-04T14:49:35.978679Z

one issue I'm hitting is this and not sure if it's a bug or feature.

(ns foo)
(require '[clojure.string :as str])
(defmacro dude [& xs]
  `(str/join "," xs))

(ns bar (:require [foo]))
(foo/dude 1 2 3) ;;=> Unresolved namespace `clojure.string`, did you require it?

borkdude 2026-05-04T21:41:37.648479Z

i you want to test, pick the newest commit from this branch. https://github.com/clj-kondo/clj-kondo/tree/macros-from-source Would appreciate it if you could find some edge cases :)

👍 1
2026-05-04T22:09:21.432329Z

i'll try it out with my complex macros lol

borkdude 2026-05-04T22:11:38.244529Z

As long as your macro is kind of a pure function it should kinda work ;)

2026-05-04T22:14:16.775499Z

i won't use it with the one that relies on tools.analyzer 😉

borkdude 2026-05-04T22:14:38.130879Z

that will certainly not work

jgomez 2026-05-04T22:22:18.675429Z

ah, I was planning to try it with the macros inside my clara-rules fork

jgomez 2026-05-04T22:22:28.141769Z

I guess i'll try it anyways and let you know how it fares

borkdude 2026-05-04T22:31:37.406739Z

try it out and report issues, we'll see where we end up :)

jgomez 2026-05-04T22:41:43.140249Z

I've sort of reduced this macro to a simple example to show, but basically these don't work:

(defmacro defdata
  {:clj-kondo/macro true}
  [& vargs]
  (let [[sym args] (parse-args vargs &form)]
    `(def ~sym (cons 'do '~args))))
Uses of it like:
(defdata some-random-id
  "c44eb844-c10e-4726-a529-b7d68452452f")
Get:
Unresolved symbol: some-random-id
Is it fair to assume this can only lint self-contained macros (the above example calls to parse-args