Today fellow Clojurian Søren Knudsen asked the following question on Slack:
Say I'd like an overview of which fns in my Clojurescript app don't have
:xmetadata and aren't children of functions that have:x. I'd love this overview as data.Anyone know a relevant analysis tool for this purpose?
Let's represent this problem in code form. Read it from bottom to top.
(defn grandchild [] ; no :x, but reachable via child: ignore
:leaf)
(defn child [] ; no :x, called by ^:x grandparent: ignore
(grandchild))
(defn ^:x grandparent [] ;; has :x metadata, ignore
(child))
(defn standalone [] ;; has no :x metadata and not reachable from anything with :x metadata, include
:other)
It turns out that clj-kondo analysis data is well suited to solve this problem. In this blog post, let's write a babashka script, that uses the clj-kondo pod. This bit of setup lets you do that. Of course you could also use clj-kondo as a regular JVM dependency, but we're going for ease here, since it's just a tiny script at this point.
#!/usr/bin/env bb
(require '[babashka.pods :as pods]
'[clojure.set :as set])
(pods/load-pod 'clj-kondo/clj-kondo "2025.06.05")
(require '[pod.borkdude.clj-kondo :as clj-kondo])
Clj-kondo lets you find var-definitions and var-usages. Clj-kondo can also include var metadata. The arguments to clj-kondo's run! API function then should look like this:
(def analysis
(-> (clj-kondo/run! {:lint ["src"]
:config {:analysis {:var-definitions {:meta [:x]}
:var-usages true}}})
:analysis))
To illustrate how it works, we'll introduce a multi-namespace project:
;; src/app/core.cljs
(ns app.core
(:require [app.util :as util]))
(defn ^:x grandparent []
(util/child))
(defn standalone []
:other)
;; a top level var usage, not inside any var definition:
(util/child)
;; src/app/util.cljs
(ns app.util)
(defn grandchild []
:leaf)
(defn child []
(grandchild))
To illustrate what a var usage looks like in clj-kondo's analysis data, let's look at the usage in app.core of util/child:
{:from app.core
:from-var grandparent
:to app.util
:name child
...}
The :from key describes from which namespace the reference was used. The :from-var key describes in which var definition the var was used, and this is the key ingredient of tracking transitive var usages. The :to + :name keys describe which var was used.
In clj-kondo's analysis you can request metadata from vars with :meta [:x] (or all metadata with true). To distinguish all project vars from those that have :x metadata we can do the following:
(defn fq [ns name] (symbol (str ns) (str name)))
(def defs (:var-definitions analysis))
(def project-vars (set (map #(fq (:ns %) (:name %)) defs)))
(def with-x (set (keep #(when (-> % :meta :x) (fq (:ns %) (:name %))) defs)))
Here project-vars is a set of symbols of all the project vars and with-x are only those that have :x metadata.
Now we're ready to build the call graph that lets us solve our problem. In the following we're making a map that looks like: caller -> callees, but we limit callees only to project vars since we're not interested in vars like cljs.core/assoc, reagent.core/atom etc.
(def graph
(reduce (fn [g {:keys [from from-var to name]}]
(let [callee (fq to name)]
(if (and from-var (contains? project-vars callee))
(update g (fq from from-var) (fnil conj #{}) callee)
g)))
{}
(:var-usages analysis)))
The from-var condition leaves out any top level var usages. The (contains? project-vars callee) takes care of filtering only on project vars. After running this, we'll end up with a graph (map) that looks like:
{app.core/grandparent #{app.util/child}
app.util/child #{app.util/grandchild}}
So app.core/grandparent calls app.util/child and app.util/child calls app.util/grandchild.
Next we write a function to find out what vars are reachable from a set of vars starts.
(defn reachable [starts]
(loop [seen #{}
todo (set starts)]
(if (empty? todo)
seen
(let [seen (into seen todo)
used-vars (set (mapcat graph todo))
unvisited (set/difference used-vars seen)]
(recur seen unvisited)))))
(def children (set/difference (reachable with-x) with-x))
(prn {:graph graph
:with-x with-x
:children-of-x children
:without-x (set/difference project-vars with-x children)})
The reachable function just calculates the transitive closure of the graph, given a set of starting nodes (vars). The children var is the set of reachable vars without the starting points (the vars with :x metadata).
{:graph {app.core/grandparent #{app.util/child}
app.util/child #{app.util/grandchild}}
:with-x #{app.core/grandparent}
:children-of-x #{app.util/child app.util/grandchild}
:without-x #{app.core/standalone}}
So the answer we were looking for is #{app.core/standalone}. This function is neither a transitive child of any function with :x metadata, nor does it have any :x metadata itself.
Here's the full script once again.
#!/usr/bin/env bb
(require '[babashka.pods :as pods]
'[clojure.set :as set])
(pods/load-pod 'clj-kondo/clj-kondo "2025.06.05")
(require '[pod.borkdude.clj-kondo :as clj-kondo])
(def analysis
(-> (clj-kondo/run! {:lint ["src"]
:config {:analysis {:var-definitions {:meta [:x]}
:var-usages true}}})
:analysis))
(defn fq [ns name] (symbol (str ns) (str name)))
(def defs (:var-definitions analysis))
(def project-vars (set (map #(fq (:ns %) (:name %)) defs)))
(def with-x (set (keep #(when (-> % :meta :x) (fq (:ns %) (:name %))) defs)))
;; caller -> callees, project vars only
(def graph
(reduce (fn [g {:keys [from from-var to name]}]
(let [callee (fq to name)]
(if (and from-var (contains? project-vars callee))
(update g (fq from from-var) (fnil conj #{}) callee)
g)))
{}
(:var-usages analysis)))
(defn reachable [starts]
(loop [seen #{} todo (set starts)]
(if (empty? todo)
seen
(let [seen (into seen todo)
used-vars (set (mapcat graph todo))
unvisited (set/difference used-vars seen)]
(recur seen unvisited)))))
(def children (set/difference (reachable with-x) with-x))
(prn {:graph graph
:with-x with-x
:children-of-x children
:without-x (set/difference project-vars with-x children)})
I hope you learned how useful clj-kondo analysis data can be for tracking relations between vars and that you can use this data in casual babashka scripts as well!
Published: 2026-06-10
In this post I'll give updates about open source I worked on during March and April 2026.
To see previous OSS updates, go here.
I'd like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn't exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

Current top tier sponsors:
Open the details section for more info about sponsoring.
If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!
Babashka Conf 2026 is happening on May 8th in the OBA Oosterdok library in Amsterdam! David Nolen, primary maintainer of ClojureScript, will be our keynote speaker. We're excited to have Nubank, Exoscale, Bob, Flexiana and Itonomi as sponsors. Nubank and Exoscale are hiring. Wendy Randolph will be our event host. For the schedule and other info, see babashka.org/conf. Join the babashka-conf Slack channel on Clojurians Slack for last minute communication. The day after babashka conf, Dutch Clojure Days 2026 will be happening, so you can enjoy a whole weekend of Clojure in Amsterdam. Hope to see many of you there!
In the last two months I spent significant time organizing babashka conf, but made progress in several projects as well.
My upstream work to enable async/await in ClojureScript was merged in the beginning of March. The implementation mirrors squint. Thanks David for reviewing and merging. Also deftest now supports an ^:async annotation so you can use async/await and don't need to mess around with the cljs.test/async macro anymore:
I'll be presenting this work at the Dutch Clojure Days.
Rebel-readline is now bb compatible. The work involved mainly exposing more JLine stuff and making sure rebel-readline didn't hit any internal JLine APIs. One step to drive this to completion was to make a dependency, compliment, bb compatible. Thanks both to Bruce and Alexander for the cooperation.
Squint now supports cljs.test and multimethods! clojure-mode was ported to use the new cljs.test.
On the cream front, I put in effort to make the binary smaller and have been keeping up with the new GraalVM EA releases. I've been posting bug reports to the crema maintainer. Currently there's still an unfixed bug around core.async that I have trouble reproducing in pure Java. I also added lots of library tests to CI so I can ensure stability in the long run. For now it remains experimental, but the direction is promising.
A performance PR to weavejester/dependency speeds up depend, depends? and topo-sort significantly, so clerk notebooks render faster.
The cljfmt library, also by @weavejester, now fully runs from source in babashka. The Java diff library that wasn't bb-compatible was replaced with text-diff, but only for the babashka path. The JVM build of cljfmt still uses the original Java diff library, with a possible switch later once text-diff has matured.
Several SCI fixes were made to improve Clojure compatibility between babashka and Clojure. E.g. records can now support extending to IFn which was a blocker for some Clojure libs that tried to run in bb so far.
Clj-kondo 2026.04.15 got a few new linters thanks to @jramosg for stewarding most of these. It also has better out of the box potemkin support, and @alexander-yakushev contributed a wave of performance improvements.
Updates per project below. Bullets are highlights; see each project's CHANGELOG.md for the full list.
babashka: native, fast starting Clojure interpreter for scripting.
Completer, Highlighter, ParsedLine, Writer, Readerclojure.repl/special-doc and clojure.repl/set-break-handler!clojure.main/repl-readorg.jline.reader.Buffer to class allowlistclojure.java.javadoc namespace with javadoc available in REPL #1933(doc var), (doc set!) and other special forms #1932(source inc) and (source babashka.fs/exists?) for built-in vars #1935BABASHKA_REPL_HISTORY env var for configurable REPL history location #1930deftype and defrecord inside non-top-level forms (e.g. let, testing) #1936java.util.HexFormat interop support:as-alias-version as an alias for --versionclojure.lang.EdnReader$ReaderException--prepare flag skipping next tokenclojure.data.xml.tree/{flatten-elements,event-tree}, clojure.data.xml.event record constructors, and clojure.data.xml.jvm.parse/string-sourcejava.net.Proxy and java.net.Proxy$Type Java classes (@jeeger)java.lang.reflect.Constructor, java.lang.reflect.Executable, java.util.stream.Collectors, java.util.Comparator (for reify), and morenextjournal.markdown to 0.7.255, edamame to 1.5.39, data.xml to 0.2.0-alpha11, jsoup to 1.22.2, rewrite-clj to 1.2.54, tools.cli to 1.4.256, transit-clj to 1.1.357, fs to 0.5.32SCI: Configurable Clojure/Script interpreter suitable for scripting
recur with 20+ args in loop (#1035)recur arity, throw when it doesn't match (#1034)IFn on defrecord, deftype and reify (#808, #1036)IPrintWithWriter as protocol (#1032)doc macrons-mapclj-kondo: static analyzer and linter for Clojure code that sparks joy.
:not-nil? which suggests (some? x) instead of (not (nil? x)), and similar patterns with when-not and if-not (default level: :off):protocol-method-arity-mismatch which warns when a protocol method is implemented with an arity that doesn't match any arity declared in the protocol (@jramosg):missing-protocol-method-arity (off by default) which warns when a protocol method is implemented but not all declared arities are covered:redundant-declare which warns when declare is used after a var is already defined (@jramosg)import-fn, import-macro, and import-defimport-vars :refer and :rename syntaxpmap and future-related functions (future, future-call, future-done?, future-cancel, future-cancelled?) (@jramosg)throw with string in CLJS no longer warns about type mismatch (@jramosg)/bin/clj-kondo (@harryzcy)StackOverflowError occurs during analysiscream: Clojure + GraalVM Crema native binary
clojure.core.async-testhttpkit, nextjournal/markdown, clj-yaml, core.async ioc-macrossquint: CLJS syntax to JS compiler
defmulti, defmethod, get-method, methods, remove-method, remove-all-methods, prefer-method, prefers, plus hierarchy ops isa?, derive, underive, make-hierarchy, parents, ancestors, descendants (#806)cljs.test/report is now a multimethod, extensible via defmethod. test-var now fires :begin-test-var / :end-test-var events.await in async functions, in anticipation of CLJS next. The legacy js-await and js/await forms continue to work as aliases for now.cljs.test / clojure.test support: deftest, is, testing, are, use-fixtures, async, run-testswith-meta now preserves callability when applied to a function.cljc files via :require (no need for :require-macros); resolve qualified symbols from macro expansionssquint.compiler/compile* and squint.compiler/transpile* which accept either a string or a sequence of pre-parsed forms, skipping the forms -> string -> forms roundtrip for SSR use cases#html / #jsx were erased when an attrs map was present without a :class keycherry: Experimental ClojureScript to ES6 module compiler
await as a special form, in anticipation of CLJS next:require-macros clauses with :refer now properly accumulate instead of overwriting each othercherry.test with clojure.test-compatible testing API: deftest, is, testing, are, use-fixtures, async, run-tests. Macros are compiler built-ins (shared with squint), so no :require-macros plumbing is needed in user code.nbb: Scripting in Clojure on Node.js using SCI
IFn on defrecord and reifyp/then results)fs: file system utility library for Clojure
touch fn (@lread & @borkdude)Coercions and Returns / Argument Naming Conventions sections to README (@lread):nofollow-links option (@lread)split-ext and extension on dotfiles with parent dirs (e.g. foo/.gitignore)gzip & gunzip now honor dest dir when specified (@lread)umask on created files and directories (@lread)clerk: Moldable Live Programming for Clojure
weavejester/dependency (#808)v0.12.51 (#793), enables async/await in viewer functionspresent+reset! (#809)build-graph crash on non-Clojure source files (#810)edamame: configurable EDN and Clojure parser with location metadata and more
Nextjournal Markdown: A cross-platform Clojure/Script parser for Markdown
:disable-footnotes true to disable parsing footnotes #67quickdoc: Quick and minimal API doc generation for Clojure
grasp: Grep Clojure code using clojure.spec regexes
grasp.implbabashka.nrepl: The nREPL server from babashka as a library
send to prevent interleaved bencode frames from concurrent writesinfo and lookup op refinements: lookup carries nested info map whereas info is a flatmappod-babashka-instaparse: instaparse from babashka
add-line-and-column-info-to-metadata--features=clj_easy.graal_build_time.InitClojureClasses to native-imageinstaparse-bb: Use instaparse from babashka
add-line-and-column-info-to-metadata and get-failureparser (e.g. :output-format :enlive)java.net.URL for grammarsbabashka-sql-pods: babashka pods for SQL databases
next.jdbc, cheshire (Jackson 2.12 -> 2.20), PostgreSQL, MSSQL, HSQLDB, MySQL Connector/J drivershttp-client: HTTP client built on java.net.http
httpstat.us examples with httpbin.org in testsneil: A CLI to add common aliases and features to deps.edn-based projects
deps.clj: a faithful port of the clojure CLI bash script to Clojure
Contributions to third party projects:
depend, depends?, and topo-sort:bb reader conditionals to replace the AutoFlattenSeq deftype with plain vectors plus metadata markers, swap the Segment deftype for a reify-based CharSequence, and add a CI test runner. Open, awaiting review.These are (some of the) other projects I'm involved with but little to no activity happened in the past two months.
Published: 2026-05-04
Tagged: clojure oss updates
In this post I'll give updates about open source I worked on during January and February 2026.
To see previous OSS updates, go here.
I'd like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn't exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

Current top tier sponsors:
Open the details section for more info about sponsoring.
If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!
Babashka Conf 2026 is happening on May 8th in the OBA Oosterdok library in Amsterdam! David Nolen, primary maintainer of ClojureScript, will be our keynote speaker! We're excited to have Nubank, Exoscale, Bob and Itonomi as sponsors. Wendy Randolph will be our event host / MC / speaker liaison :-). The CfP is now closed. More information here. Get your ticket via Meetup.com (there is a waiting list, but more places may become available). The day after babashka conf, Dutch Clojure Days 2026 will be happening, so you can enjoy a whole weekend of Clojure in Amsterdam. Hope to see many of you there!
I spent a lot of time making SCI's deftype, case, and macroexpand-1 match JVM Clojure more closely. As a result, libraries like riddley, cloverage, specter, editscript, and compliment now work in babashka.
After seeing charm.clj, a terminal UI library, I decided to incorporate JLine3 into babashka so people can build terminal UIs. Since I had JLine anyway, I also gave babashka's console REPL a major upgrade with multi-line editing, tab completion, ghost text, and persistent history. A next goal is to run rebel-readline + nREPL from source in babashka, but that's still work in progress (e.g. the compliment PR is still pending).
I've been working on async/await support for ClojureScript (CLJS-3470), inspired by how squint handles it. I also implemented it in SCI (scittle, nbb etc. use SCI as a library), though the approach there is different since SCI is an interpreter.
Last but not least, I started cream, an experimental native binary that runs full JVM Clojure with fast startup using GraalVM's Crema. Unlike babashka, it supports runtime bytecode generation (definterface, deftype, gen-class). It currently depends on a fork of Clojure and GraalVM EA, so it's not production-ready yet.
Here are updates about the projects/libraries I've worked on in the last two months in detail.
NEW: cream: Clojure + GraalVM Crema native binary
eval, require, and library loadingdefinterface, deftype, gen-class, and other constructs that generate JVM bytecode at runtime.java source files directly, as a fast alternative to JBangbabashka: native, fast starting Clojure interpreter for scripting.
bb repl) improvements: multi-line editing, tab completion, ghost text, eldoc, doc-at-point (C-x C-d), persistent historydeftype with map interfaces (e.g. IPersistentMap, ILookup, Associative). Libraries like core.cache and linked now work in babashka.babashka.terminal namespace that exposes tty?deftype supports Object + hashCodereify with java.time.temporal.TemporalQueryreify with methods returning int/short/byte/floatSCI: Configurable Clojure/Script interpreter suitable for scripting
deftype now macroexpands to deftype*, matching JVM Clojure, enabling code walkers like riddleycase now macroexpands to JVM-compatible case* format, enabling tools like riddley and cloverageasync/await in ClojureScript. See docs.defrecord now expands to deftype* (like Clojure), with factory fns emitted directly in the macro expansionmacroexpand-1 now accepts an optional env map as first argumentproxy-super, proxy-call-with-super, update-proxy and proxy-mappingsthis-as in ClojureScriptclj-kondo: static analyzer and linter for Clojure code that sparks joy.
@jramosg, @tomdl89 and @hugod have been on fire with contributions this period. Six new linters!
:duplicate-refer which warns on duplicate entries in :refer of :require (@jramosg):aliased-referred-var, which warns when a var is both referred and accessed via an alias in the same namespace (@jramosg):is-message-not-string which warns when clojure.test/is receives a non-string message argument (@jramosg):redundant-format to warn when format strings contain no format specifiers (@jramosg):redundant-primitive-coercion to warn when primitive coercion functions are applied to expressions already of that type (@hugod)array, class, inst and type checking support for related functions (@jramosg)clojure.test functions and macros (@jramosg):condition-always-true linter to check first argument of clojure.test/is (@jramosg):redundant-declare which warns when declare is used after a var is already defined (@jramosg)pmap and future-related functions (@jramosg)squint: CLJS syntax to JS compiler @tonsky and @willcohen contributed several improvements this period.
squint.math, also available as clojure.math namespacecompare-and-swap!, swap-vals! and reset-vals! (@tonsky)dotimes with _ binding (@tonsky)shuffle not working on lazy sequences (@tonsky):require-macros with :refer now accumulate instead of overwriting (@willcohen)-0.0)prn js/undefined as nilyield* IIFEscittle: Execute Clojure(Script) directly from browser script tags via SCI
async/await. See docs.js/import not using evalthis-as#<Promise value> when a promise is evaluatednbb: Scripting in Clojure on Node.js using SCI
(js/Promise.resolve 1) ;;=> #<Promise 1>fs - File system utility library for Clojure
clerk: Moldable Live Programming for Clojure
neil: A CLI to add common aliases and features to deps.edn-based projects.
neil test now exits with non-zero exit code when tests failcherry: Experimental ClojureScript to ES6 module compiler
:require-macros clauses with :refer now properly accumulate instead of overwriting each otherContributions to third party projects:
cli/cli to cli/parse-opts, bumped riddleyThese are (some of the) other projects I'm involved with but little to no activity happened in the past month.
Published: 2026-03-05
Tagged: clojure oss updates