clj-kondo 2025-05-13

Is this kondo warning a bit off? I expected the third line to get red squigglies.

✅ 1

if you repro this on the command line, do you see the same?

Looks like the command line is giving me the same:

$ clj-kondo --lint src/err.clj
src/err.clj:4:6: error: clojure.core/group-by is called with 3 args but expects 2
linting took 7ms, errors: 1, warnings: 0
$ bat src/err.clj
───────┬───────────────────────────────────────────────────────────────────────────────────────────
       │ File: src/err.clj
───────┼───────────────────────────────────────────────────────────────────────────────────────────
   1   │ (ns err)
   2   │
   3   │ (->> (map inc '(1 2 3))
   4   │      (group-by identity)
   5   │      (-> identity))
───────┴───────────────────────────────────────────────────────────────────────────────────────────

$ cat src/err.clj
(ns err)

(->> (map inc '(1 2 3))
     (group-by identity)
     (-> identity))

The error seems correct?

$ clj -M /tmp/dude.clj
Execution error (ArityException) at dude/eval144 (dude.clj:4).
Wrong number of args (3) passed to: clojure.core/group-by

the call to group-by has an incorrect amount of arguments so I'd expect squiggles somewhere around group-by

🤔 1

clojure itself also points at line 4

You are indeed correct.

(-> '(->> (map inc '(1 2 3))
          (group-by identity)
          (-> identity))
    macroexpand-1)
;; => (-> identity (group-by identity (map inc '(1 2 3))))

(-> '(->> (map inc '(1 2 3))
          (group-by identity)
          (-> identity))
    macroexpand-1 macroexpand-1)
;; => (group-by identity identity (map inc '(1 2 3)))
I confused myself with the macroexpansion!

it's confusing indeed :)

History / Implementation Question: Why doesn't clj-kondo use tools.analyzer? It seems like there's a lot of conceptual overlap, so I'm wondering if there were issues with performance, limitations in the analyzer API, or some other reasons why clj-kondo "does its own thing". I'm looking at doing my own code analysis, so my question is really "hmm, which of these codebases should I study / understand / copy for my project" =D

the reason is that clj-kondo only does static analysis without access to a runtime

Ahhh, gotcha. yeah that makes a lot of sense.

I recently worked on an analyzer that is similar to tools.analyzer but simpler. It was derived from the RCF project by hyperfiddle, it's easy to understand. I recommend looking at that code if you want something like tools.analyzer but lighter. The code I worked on is now part of #clerk which uses it for dependency analysis.

Thanks, I'll take a look. Yeah, I don't necessarily have any issues with tools.analyzer. I'm more just trying to get an overview of the space of options and what would make sense for my project.

SCI also has an analyzer but doesn't use the tools.analyzer like approach with producing an AST of maps first, because I wanted it to behave as fast as possible without much overhead

Yeah, I took a look through SCI and got the sense that it was written with perf as a primary consideration

If I had known tools analyzer a bit bitter I might have gone more with that approach but I suspect that producing an AST first takes more startup time. Now it's just all a single pass

What are the aims of your project?

Can you recommend any resources for background on this kind of thing? I've found a lot on the level of, e.g., Crafting Interpreters, which is like "how to make your own language 101" but I haven't found as much at the intermediate level discussing tradeoffs of different approaches.

I'm designing a little programming language for geometry / CAD. It has variables and constraints as well as standard expression-oriented programming. So the tricky part is having the variables "flow through" to where they can get collected and solved numerically to then do the final evaluation. So the classic tree-walking approach won't work because I can't just resolve bindings and evaluate forms in isolation --- there is always a sort of global context.

e.g., you might have a body like:

(constraint (= 5 (+ ?x 1)))
(constraint (= ?y (* ?x 2)))
(line (point 1 2) (point ?x ?y))

Alex Miller (Clojure team) 2025-05-13T15:39:37.248519Z

have you looked at https://github.com/mentat-collective/emmy ?

👍 1

haha yeah, I was just perusing through their autodiff docs.

I'd like to do something self-contained, though, don't think I'll be able to make entirely what I want as a Clojure library. So having a go at using Clojure to implement my own language.

i think following through to the end of Crafting Interpreters will teach you all about this stuff

👍 1

there are some other good "how to design a programming language" books too, it's a pretty healthy space

should kondo warn about an import of a class defined by a Clojure namespace (e.g. by deftype) without a require of that same namespace? Similar to the existing warning Unresolved namespace _. Are you missing a require? For example, this code gives no warnings but will still run as long as you happen to require bar before requiring foo, which can easily happen by accident in a large project

;;; foo.clj
(ns foo
  (:import (bar Bar)))

(defn bar [] (Bar.))

;;; bar.clj
(ns bar)

(deftype Bar [])

Both defrecord and deftype contain a :load-ns option that causes the namespace to be loaded on import. But maybe you shouldn't be importing the class at all and use the constructor functions instead.

user=> (deftype Dude [])
user.Dude
user=> (->Dude)
#object[user.Dude 0x62ddd21b "user.Dude@62ddd21b"]

I don't think that's right?

user=> (require 'foo)
Execution error (ClassNotFoundException) at java.net.URLClassLoader/findClass (URLClassLoader.java:349).
bar.Bar

constructor functions work, but importing a Clojure-defined class shouldn't be a footgun

> I don't think that's right? what is not right?

> causes the namespace to be loaded on import

if you use the :load-ns option

how can that possibly work if Clojure hasn't loaded the namespace that contains the deftype?

it does in the aot case with gen-class

@mgardner2 we could add a lint rule but it could get tedious if you have to add a :require for an :import every time. most people will load the namespace somewhere and the compiler gives a good warning, right?

kind of— it gives you a ClassNotFoundException but only if you happen to require things in the "wrong" way. And you have to know that you need to require in addition to the import, which is not obvious to Clojure newbies. We just ran into this at work, where a performance-testing CI workflow failed due to this even though all the others ran fine

assuming something somewhere else is loading the file that needs to be loaded so the type you are using is defined seems pretty brittle

yes, so exactly the kind of thing I would want kondo to warn me about

➕ 1

alright, issue welcome then!

I'll create one and paste the link here. Might not get to it until later today