Fork me on GitHub

@borkdude Picking up from #CFFTD7R6Z, my question is this: why should my library, or anyone else’s library for that matter, be expected to maintain configuration for your tool?


It seems like an odd design decision if, in fact, I am understanding that is the design.


I never said that anyones library should, just that a library can include configuration so everyone using your library gets "correct" linting, navigation, renaming, etc.


Right, but why does Kondo try to lint something it doesn’t understand?


kondo doesn't know that it doesn't understand something.


Does it know the difference between a macro call and a plain function call? (Edited to make the question positive instead of the negative.)


Often clj-kondo knows that it's inside a macro, but it can also function with incomplete knowledge of a project. You can switch off certain things in certain calls (macros, but also functions). Given that projects usually only have a handful of macros, it's often not so tedious to provide configuration as you might think. For meander this may be different, but I haven't even tried. Often macros are very similar to others, you can configure those with {:lint-as {foo/with-foo clojure.core/let} Switching off all warnings in those macros is a bit of a waste if you can easily improve the experience by doing this. I have tried the "disable when inside a macro" approach, but it wasn't that great either, as you can easily get false negatives. Note that providing a config for your macros not only improves lint warnings, but also navigation, renaming, and other IDE-like feature since the analysis that clj-kondo spits out is used by tools like clojure-lsp. clj-kondo also has a :hooks option where you can provide a programmatic transformation of the expression as the user wrote it to another expression that then gets linted/analyzed in turn. This is the most flexible option and should work for all macros. Only as a last resort would I recommend to "disable everything inside this macro".


> This is the most flexible option and should work for all macros. There is even a :macroexpand option where you can provide an (almost) literal version of your macro as you wrote it in clojure. But meander is a complex beast so I think for meander it would be a less direct and way more simplified version.


A relatively simple example for a macro you can see here: The exported config is in resources/clj-kondo.exports


Does Kondo analyze on a per-file basis?


Note that clj-kondo isn't unique here, all static analyzers in clojure have this problem, Cursive users have the same issues and AFAIK clj-kondo is way more flexible in its support for macros


but it does keep state of other analyzed files on disk around to look up later


Does Kondo work inside of a project REPL?


IOW if I am connected to my project can Kondo lint with respect to that environment?


99% of the reason I keep going back to Emacs/Cider is because Cider utilizes var meta data.


That question can mean a number of different things, but if you mean: does clojure interact with a Clojure runtime, that runs the project: no. Clj-kondo never executes any of your code. Could it be enriched by looking into a runtime? Yes, possibly, but it doesn't right now. Note that clj-kondo also works for ClojureScript and ClojureDart, without needing to know anything about their runtimes.


Yeah, that’s fine and all. I was asking because it would be nice if it did.


clj-kondo also uses var metadata, but statically


Does it resolve dependencies?


Because I would be fine to do something like

(defmacro do-try-to-lint-this
  {:kondo ,,,}


I used {:style/indent ,,,} all the time like this.


clj-kondo itself doesn't resolve dependencies, but you can feed it a classpath:

clj-kondo --lint $(clojure -Spath)
some tools like clojure-lsp do this automatically for you. By feeding it a classpath, it copies exported configs to the local .clj-kondo directory, if you provide more options:
clj-kondo --lint $(clojure -Spath) --dependencies --copy-configs
If you would put in your library:
{:linters {:unresolved-symbol {:exclude [(meander.epsilon/match)]}}}
or so, then this would have the same effect. clj-kondo is designed to not have to pollute your code with all kinds of annotations, therefore it's recommended to put it in a config file


Yeah. I don’t see that as “polluting” however.


clj-kondo does support #_:clj-kondo/ignore annotations and namespace metadata configuration, but this isn't "exported" to other users


I see that as colocating concerns.


It kinda reminds me of the splitting of css/html/js and how much more lucid working with React is.


the concern of using project local configuration and configuration that you want to export to other users are separate concerns. what goes in your .clj-kondo/config.edn applies to your whole project, but often this contains stuff that you don't want to share with other users.


I get that you don't want to put any energy in something you don't use yourself. But it's possible for other people to contribute a config for meander here as well:


So basically this is a route I could go if I want meander users to not have issues with kondo in their project?


If I don’t do that, they have to configure their project.


I’m trying not to feel unhappy about this.


It’s a similar unhappy feeling I have with spec.


This is supposed to spark joy. 😛


I don’t think this is a good design.


And I want to be clear that I’m not trying to be critical because I think there’s a lot of good in Kondo but I disagree that these should be the options.


What kondo does is very similar to other tools. e.g. typescript or graalvm native-image


Clojure is not those languages.


libraries can either bundle their types or their types can be made available in another shared repo


Put another way, those languages do not have macros and so the problem I am specifically bothered by does not exist there.


this is true, but the mechanism is similar: providing stuff that enhances the UX or functionality as configuration


Right, which brings in my other complaint (and that is not just about Kondo).


(afk for a bit)


Configuration over conventions is really getting old.


Have you or do you know of anyone that has considered putting together an interface to sanely work the numerous configuration formats out there?


I know this is OT for this channel, however, Kondo happens to be one of the “last straws” for me in this never ending deluge of Clojure configuration bullshit.


Clojure, unlike JavaScript, has a vanishingly small user base and I think if we’re going to have all these tools that require configuration, it would be nice to have a humane interface for working with them.


Back on topic, I think because of the nature of macros, Kondo should try to go out of its way to avoid linting ones it is unfamiliar with unless otherwise instructed to do so. I also think I would like the option to “pollute” my var meta data with Kondo configuration so that I do not have to add/commit/maintain a separate file.


For my specific annoyances, I would accept the latter as a solution to the former.


This is, after all, what meta data is intended for.


I'll consider it, thanks for the feedback. Would you mind posting an issue about it, so it can be upvoted by other people? This is how features like this are prioritized.


Yep. Looks like you have a lot of issues though.


interesting ideas here and a good conversation, thank you both


also interested in being able to specify this on the var, I’d prefer it to an external file


I don't think this will result into not having to check in extra files though, as clj-kondo needs to read some configuration at the moment it encounters a macro call to know what to do with it, so I think the best that can be done is when analyzing that var with exported metadata config, that would get written to a file inside your .clj-kondo directory as an extra part of the configuration. The net benefit of this is marginal


That sounds like a design problem. Kondo doesn’t technically need a file to do what it does.


> Looks like you have a lot of issues though These are mostly feature request


Cider doesn’t need me to maintain files when I put :style/indent on something.


cider is a server process, clj-kondo is not


That’s just an implementation detail.


Either way, there should be a way for Kondo to cache information like that without requiring an end user to maintain a file.


clj-kondo can be run inside a server process, but in general it can be used as a tool that you can just throw code at. for it to know stuff about other namespaces, it goes through an on-disk configuration + cache. It is an implementation detail, but it is how it is.


I mean, since Kondo is writing these files anyway, it seems totally fine for it to write that meta data out too.


you're assuming that clj-kondo will scan your whole project first, that's not how it's designed. you can read about that here:


I guess I’m not understanding the technical constraints that would require me to maintain a file if Kondo has access to the meta data on the var.


No, I was assuming what you mentioned previously about being able to hand it a class path.


Obviously on a single file what I’m proposing wouldn’t work if the config was in another file.


Please read "design decisions" in I hope that will help you understand better what trade-offs were made, even though you might not agree with some of them.


If I hand Kondo a class path, it’ll consider the ns dependencies?


Maybe I misunderstood something.


See design decision number 2 in, it's about that


You mean “Design principles”?


OK. So if I’m reading that correctly it is in agreement with what I’m saying. > The cache directory should only be used to enhance linting, not as a reliance.


if you hand clj-kondo a classpath, it wil extract information, like arity info, etc, so you get more feedback, but lint configuration (treat this macro as that macro, etc) should live in .clj-kondo config since you won't rely on having to lint the classpath at all


To be clear, I am saying if Kondo is considering a group of files and an ordered analysis, it should be able to utilized any information extracted from those files for the purposes of analysis regardless of ordering.


Because this kind of analysis is commutative.


You can analyze the files in any order to produce a state of information suitable for any file in that set.


if you would analyze all the files in one go, yes. but this is not what's happening when you use clj-kondo via flycheck in emacs for example


Again, commutative operation.


You can continually update a stream of files, etc. doesn’t matter.


yes, that is how it works


K so I don’t see why I need to maintain a file.


You’re basically describing a commutative reduction function over some state value.


well, you yourself would maybe not have to maintain a file, but clj-kondo has to persist the extracted information somewhere, right, where do you think that would be, on the moon?


That state value could be whatever.


That’s not my problem.


You can put the data wherever you want, why should I have to worry about that? I see some .clj-kondo files added to my project, as a passive user of the tool I shouldn’t have to think about that. K, but if Kondo now creates an issue for people that use my library such that the only remedy I have is to create a file and check that in, I don’t think that is acceptable at all.


Asking me to put some meta data on my var is a perfectly acceptable solution, however, because that’s in the same space as code I’m maintaining.


That's not your problem at all. People can configure linting for meander they way they like it in their own projects.


Just add a part to your README like this: and go home.


But if you want to provide a better experience, then you or someone else would have to check in some files with a configuration


But again, you can just tell clj-kondo users to off and invent their own config


Then I get to be the dick.


On a scale of 1 to 10 is what I’m asking difficult?


This is why this exists: Projects that don't want to participate, can ask their users to contribute a configuration over there. It's not on you


Or does it go against a design proposition or something.


Consider the situation Kondo is creating by virtue of how it is designed and how those design considerations propagate to everyone else.


Many Kondo users are passive users. They end up using Kondo because tooling just pulls it in.


Library authors have the ability to export a shared configuration, but this is not mandatory at all. This was a much requested feature by many people who were happy that it is now possible and many people are using that feature. You can see a list of that here: But you can still configure libraries in anyway you like it outside of the scope of that library. If you're not satisfied with clj-kondo, I'm not begging you to use it. Please find a better tool that suits your needs.


Yeah, I get all this. My tl;dr is I want to put config on a var via meta as I think that is easer to maintain for a maintainer. It’s also super easy for me to remember and get in the habit of as I am with :style/indent. It is also more REPL friendly as things can be updated on the fly in the same spot.


I don’t care about single file shit.


Because most people using Clojure, let alone Kondo, are using it on a project basis with a REPL running, etc.


If I can then dump that config to disk from a Kondo command or something, I’d probably be open to maintaining that file as part of an automatic pre commit hook.


> If I can then dump that config to disk from a Kondo command or something, I’d probably be open to maintaining that file as part of an automatic pre commit hook. Now we're talking. I was assuming you didn't want to commit any file at all.


I don’t want to maintain a file.


Talking through this, that is ultimately my contention.


I am fine learning the minimal amount of Kondo configuration for the purposes of being able to stick that on a var and press a button which bakes the file for me.


By maintain here I just mean that I don’t want to have to manually update/edit it myself.


Because I often write/delete macros etc. I want my editor to respond to those changes without requiring file edits and I want to be able to build those files (since I kinda have to be nice to my users) automatically.


I can write an issue and even send you a patch or two if you want to collaborate on this.


Not opposed to that. Again, I think most of what Kondo does out of the box apart from its handling of macros is solid.


Sure. Please be as detailed as possible in your issue and propose a few concrete things like:

(ns my-namespace.match)

(defmacro my-let
  {:clj-kondo/config '{:lint-as {my-namespace/my-let clojure.core/let}}} 
The above still feels like too much boilerplate, maybe. But there are many ways to configure the behavior of a macro, this is what probably entails most of the work, finding out good ways to express all the various things you could do, if you didn't want to have it as boilerplatey
(ns my-namespace.match)

(defmacro my-let
  {:clj-kondo/config '{:hooks {:analyze-call {my-namespace/my-let hooks/my-let}}} 


The other part of the work is scanning this metadata, then spitting it out to an "extracted config file" which then gets automatically picked up by clj-kondo


This feature request is similar, but it's not the same:


The request there is to override the config for linting that particular call


FWIW I use the attr-map? style of meta data all the time, more than ^ and it doesn’t feel like boilerplate.


I also used the attr-map? above


OK. Well, I will jot down an issue. I can link this one too.


To be clear, I’d like to work together on this if you’re open to it.


So that I can experiment with this on my own, when I open that issue (or here is fine too), can you point me in the direction of where I might look to make patches in the code?


I agree that whatever the solution is, it should be lightweight. The only way to get a feel for it is to try it out.


I can give more detailed information when I'm not about to go to bed 😅 like tomorrow. But a few preliminary pointers: There is analyze-defn in impl/analyzer.clj which extracts metadata and keeps this around. This metadata should be searched for "exported" configuration. This exported configuration we should then spit out to .clj-kondo/<org>/<lib>/config.edn (maybe with an extra _extracted suffix on there, to not overwrite other stuff. The two-level directory + config.edn is a convention that is already used by libraries that export config and this gets automatically read and merged with any user config.


I think we'd have to use the namespace name + also the .clj(s)(c) extension since there may be multiple files for the same namespace


maybe _extracted/<namespace.clj> would be a good two level structure to spit out to


so say that you're scanning namespace foo/bar/baz.cljc, you can then spit out the found config to:

and it would be automatically picked up


and this would then also happen when you're scanning dependencies, so there would be no need for you to check in any files into source control either even


I'm off to bed now, thanks


Thanks for the information! I will likely post the issue tomorrow. Also, thanks for hearing me out. I’m glad this discussion turned into an opportunity to try and find a resolution.

👍 1

fwiw, I am an active user of clj-kondo. I have never been bothered by clj-kondo config files. But this is probably because I am an active user of clj-kondo! I'll follow this issue with interest to learn more about a different perspective and ideas.


Interesting discussion indeed, looking forward to checking out the feature request. One thing I had in mind while reading through was macros producing other macros and funky stuff like that. With a bit of work one can support those using config files and hooks, I'm wondering how this approach could deal with those.


one downside for me is I use fuzzy search (`projectile-find-file`) to open buffers normally. Having multiple files with the same name makes this worse.


@U5H74UNSF what "same names" are you referring to specifically?


eg viewer.cljc and viewer.clj_kondo in clerk. Same name, different extension, makes fuzzy file search slightly more complex.


I was just going to say, .clj_kondo is possible to you have a different name.


though I now realize the kondo file name is fully under my control, right?


but when using the :macroexpand hook it's often easier to have the same name, so syntax-quotes resolve to the same stuff

👍 1

but not a hard requirement


when you use clojure-lsp, I don't find myself needing projectile-find-file a lot


right. Well that weakens my point. Still I value locality and would prefer if I could configure kondo from var metadata I think. Will see when I can play with it.


it's an illusion to think that everything can be configured locally, but I want to be open to the experiment


I think macros are best served with hook code, which can't be inline-configured anyway, but I think from noprompt's angle, he wants a way to quickly shut up clj-kondo near the macro. I know there are better ways, but someone new to the tool might not want to invest that time (even if it's just once for a macro that will be used for years to come)


and macros aren't the only thing to configure


what prevents hook code from being inlined?


I think that would mean that the file and its dependencies would need to be loaded.


theoretically it's possible to do that, but I think it would become rather messy and error-prone to extract that code into a file that clj-kondo can run, without running into syntax-quote issues, etc


it is possible to inline that code as a string, for testing purposes, but it's not really nice to work like that


Yeah, that’s a mixed bag.


On the one hand, yes, you could load code and say to users “if you do this be warned I’m going to load your code and that could spell side effects, etc.”


On the other you could just not load the code. 😛


@U06MDAPTP it is often possible to copy a macro almost verbatim and use it with a :macroexpand hook. The reason that clj-kondo doesn't just load this code from source is that in practice, macros can depend on other dependencies, rely on cljs.analyzer , etc. While clj-kondo just wants to have a pure function from s-expr to s-expr or rewrite-clj node to rewrite-clj node. The macro code runs in an interpreter that can't even touch your file system.


Yeah that makes sense.


Another reason is that macro-expansions can often be much simpler for clj-kondo: they don't have to expand into something that works, just something that makes sense to the linter


wondering if there should also be a setting for the use my macro code as-is for analysis case


I have considered that, but I think it would lead to more questions and errors than I can handle ;)


For simple rewriting macros that’s probably fine. For macros which are embedded languages that won’t work too well.


It's of course worth an experiment.


but lint-as should be nicely settable using var meta at least, correct?


The thing is for something like Meander, in order to provide the analyzer with the most information I would need to write a hook.


But I’m not focusing on that case ATM.


@U5H74UNSF yes, :lint-as and :config-in-call

👍 1

(defmacro my-let
  {:clj-kondo/export {:lint-as clojure.core/let}}
  [...] ...)

(defmacro meatch
  {:clj-kondo/export {:config-in-call {:linters {:unresolved-symbol {:exclude [#"\?.*"]}}}}}
  [...] ...)
or so?


(the regex doesn't work for unresolved symbol, but I think it would be nice to be able to suppress ?foo symbols as they are often indicating a local in meander-like macros)


I'm not sure if we should just spell out the full config. Maybe that would be less confusing that coming up with a shorter one.

(defmacro my-let
  {:clj-kondo/export {:lint-as {my-ns/my-let clojure.core/let}}}
  [...] ...)
but then there is no need to attach this config to a var at all, you could just float it anywhere in your namespace:
(ns foobar
{:clj-kondo/export {:lint-as {my-ns/my-let clojure.core/let}}})
(defmacro my-let
  [...] ...)

👍 1

in the first case you wouldn’t need to repeat the name if you declare it locally, and you get locality


@U5H74UNSF what syntax would you propose for lint-as attached to a var?


(defmacro my-let
  {:clj-kondo/lint-as clojure.core/let}
  [...] ...)


maybe this


yeah, maybe. my thought was that we'd need the word export, but if you are the macro author, you "own" it so there's no reason you shouldn't export that config to others


(defmacro match
  {:clj-kondo/config {:linters {:unresolved-symbol {:level :off}}}}
  [...] ...)
(defmacro match
  {:clj-kondo/disable [:unresolved-symbol]}
  [...] ...)


(defmacro match
  {:clj-kondo/config {:linters {:unresolved-symbol {:exclude [dude]}}}})
  (when-not (= 'dude x) x)

(match dude)


makes sense I guess


however it would be kind of confusing since kondo already supports:

(ns foo
  {:clj-kondo/config {:linters ...}})
which is not exported config, but config which only applies to the scope of the namespace.


and we have this feature request which is similar for a call:


We could also do this. We already have

#_:clj-kondo/ignore (+ 1 2 :three)
We chose "uneval" here (or whatever it may be called) since 1) not every value takes metadata in clojure so you wouldn't be able to put this annotation on numbers, etc otherwise and 2) to not transfer linter instructions/metadata to production code Along the same line we could do:
#_{:clj-kondo/export {:lint-as {my-namespace/with-let clojure.core/let}}} 


although putting it on the var feels more natural. I just don't want to cause any confusion with stuff that's already there


(defmacro my-let
  {:clj-kondo/export {:lint-as clojure.core/let}
  [...] ...)
would mean {:lint-as {my-ns/my-let clojure.core/let}} .Maybe that's the least ambigious and come closest to the other one



(defmacro my-let
  {:clj-kondo/export {:linters {:unresolved-symbol {:exclude [#"^\?"]}}}}
  [...] ...)


would mean {:config-in-call {my-ns/my-let {:linters ...}}}


or maybe:

{:clj-kondo/export {:lint-as {?var clojure.core/let}}}
no different syntax, but you get the fq var symbol interpolated for ?var (inspired by meander 😆 )

😹 2

{:clj-kondo/export {:config-in-call {?var ...}}}


{:clj-kondo/export {:linters {:discouraged-var {?var {:message "Don't call this function ever again, unless you want to heat up the room via CPU"}}}}}


there are many places in which a var name can occur so supporting a "short" syntax where you can avoid the name of the var is error-prone both for the user and also to interpret that by clj-kondo. The ?var interpolation probably matches better the existing documentation, etc.


Kind of a prototype:


FYI I probably won’t get a chance to capture anything on an issue today. I’ve been busy with work today and I gotta take my son to an appointment here in a bit and pretty much be a dad after that for the rest of the day.


no problem!


@borkdude I wanted to let you know that I haven’t forgotten about putting together the issue. Work has been a bit intense the past couple weeks and I’ve been mentally tapped at the end of the day. I still need to get familiar of what would make sense for var configuration but I could see

(defmacro my-macro
  {:clj-kondo/lint-as `case
   :clj-kondo.hooks/analyze-call my-project.hooks/analyze-my-macro}}
  [,,,] ,,,)
and then have kondo expand that to
 {:lint-as {my-project/my-macro clojure.core/case}
  :hooks {:analyze-call {my-project/my-macro my-project.hooks/analyze-my-macro}}
Note, this example is only for illustration. The idea is to tweak the configuration so it would make sense for a var.


In terms of getting to “quickly shutting up”, I think having

{:linters {:exclude #{my-project/my-macro}}
or something like this for saying “do not lint this var and ignore all other configuration”.



{:clj-kondo/exclude true}
on the var meta e.g.
(defmacro ^:clj-kondo/exclude my-macro ,,,)
would thus expand to
{:clj-kondo/config {:linters {:exclude #{my-project/my-macro}}}


true/`false` corresponds to wether or not the var name should go in the exclusion set.


yep, sounds good, looking forward to the issue! there are certain linters that still makes sense in macros, e.g. ones like {:a 1 :b} : missing key for :b, etc.


I think what’s described in the issue will help in adopting clj-kondo. I also felt a lot of startup pain with clj-kondo for some projects. It was feeling too overwhelming for me to continue (and I didn’t). Thank you @U06MDAPTP for bringing it up and thank you @borkdude for patiently listening


Sure! I had another idea for meander-like macros: what if you could say that clj-kondo would take the first unresolved symbol as a binding and the rest of the similar unresolved symbols as binding references? This would make navigation to locals and renaming also work within meander/match etc


(defmacro match {:clj-kondo/config {:infer-locals true}}
  [ .. ] ..)
(match {:foo 1 :bar 2} {:foo ?hello (unresolved, treat as local) :bar ?dude (unresolved, treat as local)} [?hello (local ref) ?dude (local ref)])


Making some progress:


That’s looking great! 🙂


it's going into the right direction now... hopefully can merge tomorrow after writing some tests..


Man, that’s great news. Whenever you get it ready I’ll start testing it out!


you can already test it out today, depending on how you want to use it. apart from the tests it's ready I think (apart from some other non-macro stuff I also want to add)


I probably won’t get a chance until the weekend because $job. I can probably look at it either Saturday or Sunday.


Merged on master


@borkdude awesome, thank you!


clj-kondo new version released


Hi, clj-kondo reports ->x as an redefined var. It happens once I create some deftype and then a function prefixed with -> . It is a bug, right?


from (doc deftype) > Given (deftype TypeName ...), a factory function called ->TypeName > will be defined, taking positional parameters for the fields


so deftype creates a ->x and then you clobber that when redefining it with defn ->x. The warning is valid and warning you against what you are doing

👍 1

I’m doing this on purpose, I’m changing the factory in cljdart. Okay, if it’s intentional, I’ll hide it. Thanks for the info.

Sam Ritchie20:03:47

Try an alter-var-root instead of a defn


I think that’s a bit much. it’s totally fine to want to do this (or name your function differently since there’s an expectation around ->x. ) . Just suppress the warning.

#_{:clj-kondo/ignore [:redefined-var]}
(defn ->x [])

👍 2