clj-kondo 2026-07-16

Super exciting tweak in type inference Hope it'll help catching bugs during dev more (for free)

👍 7
👍🏻 2
👍🏼 1
👏 1
🎉 8
1

this is awesome! been wanting something like this for so long

btw if I would like to learn about the type inference system in clj-kondo so I can make contributions, where in the codebase would you recommend I start with reading?

probably the types.clj namespace and the documentation in doc on it

An interesting one:

(java.util.Date. (long (inst-ms dt)))
This is flagged as a redundant long coercion (because Kondo deduces that inst-ms always returns a long (well, a Long as an Object I guess). But if you remove the long coercion, you get a reflection warning on the Date constructor call. Thoughts?

I ended up changing the code around it to this:

dt-fmt  (fn [dt]
                  (let [^long ms (inst-ms dt)]
                    (jt/format "MMM dd, yyyy hh:mm a"
                               (jt/offset-date-time (java.util.Date. ms) 0))))

seems like an overly broad inference on kondo's part, the docstring for inst-ms doesn't mention a type and the return type is not hinted or anything. I would be curios what happens if add an extension of the Inst protocol to some other type and had it return a BigInteger what kondo would say

Yeah I'd say this is an FP since you use long for the type hint basically?

Or maybe we should spec inst-ms differently in clj-kondo

Alex Miller (Clojure team) 2026-07-16T15:55:29.818359Z

the intent is certainly to always return a long, should probably have a ^long return type hint

Alex Miller (Clojure team) 2026-07-16T15:55:53.793509Z

if you wanted to make an ask for inst-ms, that seems useful

Seems like it would be nice to have that return type hint on inst-ms. I'd vote for that Ask!

for now making a clj-kondo issue for (long ..) not being a no-op is probably good too, there might be more cases where it optimistically reports it

Not sure how to word that, so I'll leave the issue up to someone else. Given the context of this particular warning, and the lack of return type hint on inst-ms, I'm happy to change the code to make it explicit.

Hello, I am developing a hook and would like to know how can I lint a sample of code. I know I can do this:

(-> {:node
       (api/parse-string
        "
(defplug moog-ladder
  {:lpf 20000
   :reso 0.1
   :ugen/filter (fn [sig] (o/moog-ladder sig (o/clip lpf 30 20000) reso))})")}
      defplug)
But I don’t see any linter errors that may arise. Particularly I currently have this pseudo macro (I know its ugly) that does some tricks on keywords with the ugen ns. I want to be able to ignore some linter errors there:
(-> {:node
       (api/parse-string
        "
(make-synth-fn
         'siny
         (-> {:freq [500 900]
              :amp 1
              :ugen/pan (something)}
             (env1)
             (freq-mixer))
         '(o/out 0
                 (-> (o/sin-osc freq)
                     (:ugen/freq-mixer freq amp)
                     (o/pan2 0)
                     (* amp :ugen/env)))
         {:reset? true})")}

      make-synth-fn))

Have you tried :lint-as clj-kondo.lint-as/def-catch-all?

Sorry if my message was not clear. I don’t have an issue with unresolved-symbol errors. It has to do with my comment below. I am getting invalid-arity errors in things like (:ugen/something a b c) . Which in normal circumstances make total sense but in my use case I would like to omit them. I am trying to figure out why doing this:

(defn ignore-ugen-kw-errors
  [body]
  (let [new-ch
        (->> body
             :children
             (walk/postwalk
              (fn [x]
                (if-let [ch (:children x)]
                  (let [ch* (->> ch
                                 (mapv (fn [c]
                                         (if (some-> c :children  first :k (namespace) (= PLUG_NS))
                                           (vary-meta c assoc :clj-kondo/ignore [:type-mismatch :invalid-arity])
                                           c))))]
                    (assoc x :children ch*))

                  x))))]
    (assoc body :children new-ch)))

Currently for every change I make I have to restart the LSP workspace for the hook changes to apply. That’s why I am asking if there is a way to run the linter from the repl, so that I can debug my code more easily.

Also I believe on master we have some auto-loading now it automatically re-loads every time the file has changed. I can recommend running clojure-lsp nightly which has these latest changes

Perhaps this was already in 2026.05.25 not sure. which version do you have locally?

yeah I am on this one: 2026.05.25

it should pick up on macro changes

but not sure which clojure-lsp you're using and if that has an older clj-kondo

Ah that’s possible, haven’t updated in a while

I recommend the nightly, then you can be sure of the latest additions

One more question. Here’s the data (printed by cider-inspect) of the SeqNode where I am trying to ignore some errors. Is the metadata set correctly?

Class: clj_kondo.impl.rewrite_clj.node.seq.SeqNode
Count: 5

--- Meta Information:
  :row = 11
  :col = 22
  :end-row = 11
  :end-col = 49
  :clj-kondo/ignore = [:type-mismatch :invalid-arity]

--- Analytics:
  Press 'y' or M-x cider-inspector-display-analytics to analyze this value.

--- Contents:
  :tag = :list
  :format-string = "(%s)"
  :wrap-length = 2
  :seq-fn = #function[clj-kondo.impl.rewrite-clj.node.seq/list-node/fn--40689]
  :children = [#KeywordNode{:k :ugen/freq-mixer, :namespaced? nil}
               #TokenNode{:value freq, :string-value "freq"}
               #TokenNode{:value amp, :string-value "amp"}]

--- Path:
  (nth 1) :children (nth 2) :children (nth 2)

--- View mode (press 'v' to cycle, 'P' to pretty-print, 'S' to sort maps):
  â—Źnormal object â—Źpretty sort-maps
The code in question is this:
(make-synth-fn
         'siny
         (-> {:freq [500 900]
              :amp 1
              :ugen/pan (something)}
             (env1)
             (freq-mixer))
         '(o/out 0
                 (-> (o/sin-osc freq)
                     (:ugen/freq-mixer freq amp)
                     (o/pan2 0)
                     (* amp :ugen/env)))
         {:reset? true})

@borkdude (apologies for the tag), do you have any suggestion regarding this issue?

yeah I think that should work

is it not working?

Yes, it’s not working in cases where I have more than one (:ugen/something….) even though I can see that the forms have the specific metadata. Wouldn’t rule out a bug on my own hook code, but not sure what. Is postwalk a good way to do this? I wonder if immutability may be an issue here:

(defn ignore-ugen-kw-errors
  [body]
  (let [new-ch
        (->> body
             :children
             (walk/postwalk
              (fn [x]
                (if-let [ch (:children x)]
                  (let [ch* (->> ch
                                 (mapv (fn [c]
                                         (if (some-> c :children  first :k (namespace) (= PLUG_NS))
                                           (vary-meta c assoc :clj-kondo/ignore [:type-mismatch :invalid-arity])
                                           c))))]
                    (assoc x :children ch*))

                  x))))]
    (assoc body :children new-ch)))

Here’s an example: https://github.com/diegovdc/tiempaminos/blob/gc/guitar-processes/src/tieminos/scratch/make_synth_fn_hook.clj The first form will get a linter highlight, the other two will not.

We have a namespace at work that has all its functions generated at load time from macros. Kondo doesn't see the defn for these functions so it flags all uses as unresolved vars. Is there a way to tell Kondo not to complain about unresolved vars that come from a specific ns? I know I can flag each use of those fns (and we've done that in a few places but it's really tedious). I don't want to turn that linter off. It's a fixed set of functions, so I could add a dummy declare of everything at the top of that ns. That gets rid of the warning but what else am I losing if I do that?

âś… 1

This is usually something that can be solved using a hook

if you can let me know the shape of your code, then maybe I have more concrete ideas

Not sure what you mean about a hook to solve this...? When the ns loads, it is fully-populated with functions. But because they're created by a gigantic macro, that generates maybe a dozen defns for each invocation, Kondo cannot see those fns. So far, the only ways I can see to solve this: • explicitly add :config-in-call entries for every such fn in that ns—Kondo ends up with a massive list of calls to ignore unresolved-symbol on • explicitly add declare to the source (at the end of the ns) with every such fn listed—one source file has a long list of symbols in a declare • petition for a wildcard or whole ns version of :config-in-call 🙂 Bullet 2 is winning right now, but I'd love bullet 3.

Are they predictable? Eg is it like defrecord and produces 3 vars with predictable names? Or is this a hairy single macro call that's generating 50 vars that are semi unrelated? If the former, a hook seems appropriate.

@seancorfield right, so just write a macro hook that also generates those defn s

you can even do it inline in the code nowadays, let me look up the trick

Writing the hook would be even more work than what I'm doing!

> you can exclude warnings for all unresolved vars from namespace foo using: >

{:linters {:unresolved-var {:exclude [foo]}}}

The macro is over 100 lines long and I'd have to analyze it pretty deeply to even figure out which symbols it produces from a given database table name (it generates all the crud functions for 35 tables).

@borkdude does that exclude based on the calling ns or the defining ns?

aah ok, so it's dynamically created. I mean, you could write a script to statically generate that namespace and then clj-kondo would understand it too your question: the defining ns I hope, else it would be pretty useless

Heh, okay, that was what I was looking for and is basically bullet 3. Let me try that.

Yup, perfect! That was exactly what I needed! I just couldn't figure it out based on what I found in the docs. Thank you!

The solution:

:unresolved-var {:exclude [ws.billing-data.interface]}

🎉 1

Part of the "problem" with the various suggestions above (aside from the actual solution) is that the generate-crud macro includes multiple blocks like this:

~@(for [[k kd] aux-keys]
           (let [clj-k (str/replace (name k) "_" "-")
                 sym-k (symbol clj-k)]
             (list `defn (symbol (str clj-name "-by-" clj-k))
                   (str "Given the Database component and a "
                        kd ",\n"
                        "  return the matching " description
                        ", if any.")
                   {::generated true}
                   ['db sym-k]
                   (list `by-key 'db table sym-k k))))
based on sequences of symbols derived from Specs that are associated with database tables (so for table foo_bar there's a Spec :wsbilling/foo-bar with s/keys that we unpack and get the keys in that map to use to drive the macro, to generate all the crud functions. Saved us writing a LOT of boilerplate but not very amenable to Kondo and hooks 🙂

Yeah, for such a case you could still (re-)generate code at runtime and spit it out to disk, but not sure how much churn is in there

Right, but that still involves a (manual) process that is separate from just editing the code and I don't want to do that, esp. since it would be just to satisfy Kondo.

sure. some people find it more satisfying to have arity warnings etc work with those, but it's a trade-off everyone makes for themselves

Aye, and we've lived a long time without any of that (due to the unresolved var thing) and this codebase is meant to be in minimal maintenance mode. I'm just making a concerted effort to clean up all the legacy lint warnings now, to make my life easier 🙂

LLMs are pretty good at cleaning up those btw :)

just guide them to the clj-kondo repo and boom...

I've already turned off the missing docstring warning because it's just noise, now that we know no one is going to go back in and add all those missing docstrings.

sure. that's not even on by default

Right, but I enabled it to try to "encourage" my team mates to write docstrings since some of them pretty much never did. But now I have no team mates.

(and I always write docstrings)

off topic, now that we solved the issue. I recently learned that some people have the habit to never docstring private functions but always do it in comments instead. I thought that was interesting...

Weird. Any idea why?

My docstring was refused in an OSS repo for this reason

I guess to not risk that people will start relying on the behavior of the private function or so?

Interesting approach...

Could you create a macro that generates the dummy declares? I assume that wont work though for same reasons, runtime vs static code analysis … but maybe you can have a build task that generates the declares or better yet generates clj kondo configuration. If you update the namespace just run a build task to generate updated configuration much the way some libraries generate code for database schemas

Right, no, you can't generate the declare because Kondo still wouldn't see it, and I don't want to introduce any code-gen step because that makes things brittle and complex.

I manually added the declare list based on the Kondo warnings, and I'll chip away at that over the next few days I expect. It's tedious, but less so that annotating all the call sites 🙂

Fair but less complex than manually adding rules. Perhaps clj-kondo could create a call this function to generate rules, but this also introduces complexity and potential security issues around arbitrary code execution that could be exploited for other purposes. Although clojure is uniquely positioned perhaps to have a less arbitrary code execution it still creates complexity

What I sort of want is a variant of this for the whole ns, not just a specific function in an ns:

{:config-in-call {my-ns/ignore {:ignore [:unresolved-symbol]}}}
;; would be great to have wildcards or even just a whole ns:
{:config-in-call {my-ns {:ignore [:unresolved-symbol]}}}