I have a custom macro that expands into a mount/defstate and I tried to macroexpand-hook it, but end up getting an unresolved symbol
repro case
bash -c '
tmp=$(mktemp -d)
cleanup() { rm -rf "$tmp"; }
trap cleanup EXIT
cd "$tmp" || exit
mkdir -p src/demo .clj-kondo
cat > deps.edn <<"EOF"
{:paths ["src"]
:deps {mount/mount {:mvn/version "0.1.24"}}}
EOF
echo "{}" > .clj-kondo/config.edn
cat > src/demo/api.clj <<"EOF"
(ns demo.api
(:require [mount.core :refer [defstate]]))
(defmacro defclient
{:clj-kondo/macroexpand-hook true}
[sym]
`(defstate ~sym :start (identity 1)))
EOF
cat > src/demo/repro.clj <<"EOF"
(ns demo.repro
(:require [demo.api :as api]))
(api/defclient client)
client
EOF
clj-kondo --version
clj-kondo --dependencies --copy-configs \
--lint "$(clojure -Srepro -Spath)" &&
clj-kondo --lint src --fail-level error --report-level error
'clj-kondo v2026.08.04
Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=KeychainStore
Configs copied:
- .clj-kondo/imports/mount/mount
src/demo/repro.clj:4:16: error: Unresolved symbol: client
linting took 26ms, errors: 1
does it work when you fully qualify defstate?
The important parts are:
(defmacro defclient
{:clj-kondo/macroexpand-hook true}
[sym]
`(defstate ~sym :start (identity 1)))
(api/defclient client)
it doesn't
and you also have a hook for mount defstate?
what if you literally use mount defstate, does that "work"?
I don't but mount itself does publish one
and its hook works fine
let me add a few more bits to the repro
Okay, here's a larger repro with a few cases, marked with comments
bash -c '
tmp=$(mktemp -d)
cleanup() { rm -rf "$tmp"; }
trap cleanup EXIT
cd "$tmp" || exit
mkdir -p src/demo .clj-kondo
cat > deps.edn <<"EOF"
{:paths ["src"]
:deps {mount/mount {:mvn/version "0.1.24"}}}
EOF
echo "{}" > .clj-kondo/config.edn
cat > src/demo/api.clj <<"EOF"
(ns demo.api
(:require [mount.core :as mount :refer [defstate]]))
(defmacro defclient1
{:clj-kondo/macroexpand-hook true}
[sym]
`(defstate ~sym :start (identity 1)))
(defmacro defclient2
{:clj-kondo/macroexpand-hook true}
[sym]
`(mount/defstate ~sym :start (identity 1)))
EOF
cat > src/demo/repro1.clj <<"EOF"
(ns demo.repro1
(:require [demo.api :as api]))
(api/defclient1 client1) ;; client1 unresolved
client1
EOF
cat > src/demo/repro2.clj <<"EOF"
(ns demo.repro2
(:require [demo.api :as api]))
(api/defclient2 client2) ;; client2 unresolved
client2
EOF
cat > src/demo/repro3.clj <<"EOF"
(ns demo.repro3
(:require [demo.api :as api]
[mount.core]))
(api/defclient1 client1) ;; client1 unresolved
client1
(api/defclient2 client2) ;; no error (referencing mount directly?)
client2
EOF
cat > src/demo/repro_mount.clj <<"EOF"
(ns demo.repro-mount
(:require [mount.core :as mount :refer [defstate]]))
;; no errors using mount directly
(defstate state1 :start (identity 1))
(mount/defstate state2 :start (identity 1))
state1
state2
EOF
clj-kondo --version
clj-kondo --dependencies --copy-configs --lint "$(clojure -Srepro -Spath)" &&
clj-kondo --lint src --fail-level error --report-level error
'
Important bits: (using refer and as in the 2 macros)
(defmacro defclient1
{:clj-kondo/macroexpand-hook true}
[sym]
`(defstate ~sym :start (identity 1)))
(defmacro defclient2
{:clj-kondo/macroexpand-hook true}
[sym]
`(mount/defstate ~sym :start (identity 1)))
(ns demo.repro1
(:require [demo.api :as api]))
(api/defclient1 client1) ;; client1 unresolved
(ns demo.repro2
(:require [demo.api :as api]))
(api/defclient2 client2) ;; client2 unresolved
client2
When I explicitly include mount, the second case clears:
(ns demo.repro3
(:require [demo.api :as api]
[mount.core]))
(api/defclient1 client1) ;; client1 unresolved
client1
(api/defclient2 client2) ;; no error (referencing mount directly?)
client2
Can you make a Github issue so this doesn't get lost?
absolutely
Here is another interesting one I just found ๐งต
printf '%s\n' \
'(ns demo.minimal (:require [clojure.test :refer :all]))' \
'(def client (identity client))' \
| clj-kondo --lint - --filename demo/minimal.clj --report-level error
demo/minimal.clj:2:23: error: Unresolved symbol: client
linting took 40ms, errors: 1whereas
printf '%s\n' \
'(ns demo.minimal)' \
'(def client (identity client))' \
| clj-kondo --lint - --filename demo/minimal.clj --report-level error
linting took 38ms, errors: 0
The tricky code is (def client (identity client)) which is valid (but not too useful) clojure
and it's considered valid, unless the namespace uses (:require [clojure.test :refer :all])
(deleted my off-topic message in github, sorry lol)
@nbtheduke the repro case is a minimal reduction of the problem I saw (with mount, again, but it turns out mount is not required for it)
yeah, i thought it was silly and then thought about it for two seconds and realized that the behavior is how recursive functions work
PR submitted: https://github.com/clj-kondo/clj-kondo/pull/3008