Fork me on GitHub
#clojure
<
2024-05-31
>
namenu13:05:29

Hi, I want to scan my codebase and find every functions with specific metadata. Some of them are declared by macros. Is tools.reader good for this kind of job?

p-himik13:05:31

> Some of them are declared by macros This is the most problematic part. It means that tools.reader can't help as it doesn't do macroexpansion. And more than that - macroexpansion can work differently in different environments. If a macro is wrapped in anything conditional or if it uses any conditions based on e.g. environment variables, properties, previously defined things, in general you can't extract everything without also running the whole program.

namenu13:05:43

In this case, I'm only considering compile-time macro expansion and I wonder how some static analysis tools like clj-kondo or cljfmt works on this case. Do they run out of the box, right?

Ed13:05:07

You can do this from the repl with something like

(->> (mapcat (comp vals ns-map) (all-ns))
       (filter #(some-> % meta :file (.matches ".*scratch.clj"))))
which will find all the vars in loaded name spaces, post macro expansion. But clj-kondo doesn't really do macro expansion. It has some understanding of standard macros, like defn and will let you treat your custom macros in that same way, or it will allow you to write custom hooks to allow it to parse your custom macros. But it won't be able to work out arbitrary macro expansions and let you query the results.

p-himik13:05:50

There's no such thing as distinguished "compile-time" in Clojure. Running any code compiles it to classes - they just aren't stored on disk. Calling compile also runs the corresponding code. > how some static analysis tools like clj-kondo or cljfmt works on this case They don't. You cannot read some piece of code and see in a programmatic manner exactly what it does without actually running the code. All static analysis, while extremely valuable because most code is written in a kinda "static" way, is still the case of "best efforts". Even with a plain "statically" defined function, you can have things like this:

user=> (defn ^{:x (Boolean/valueOf (System/getenv "X"))} f [])
#'user/f
user=> (:x (meta #'f))
false
> which will find all the vars in loaded name spaces Exactly - the code has to be loaded, and if a namespace has top-level side effects, those will also be executed.

Ed13:05:03

https://github.com/clj-kondo/clj-kondo/blob/master/doc/hooks.md this might help you work out what you can do with clj-kondo

👍 1
clj-kondo 1
p-himik13:05:37

@U7JHWBCS0 Perhaps your actual problem can be solved in some other way. But that would require us knowing what that problem is.

namenu14:05:33

@U2FRKM4TW I have a bunch of callback functions which are plain defn or defhandler. They are binded to specific events and the event->callback mapping is defined in external edn. What I want to know is whether every callback is bounded to at least one event. IDE can't tell if the function is used or not because there might be no caller explicitly. And because of that every handers are marked as :ignore or sth to suppress unused warnings.

namenu14:05:52

@U0P0TMEFJ Thanks for the pointer. that might be a good start.

p-himik14:05:16

Your description doesn't make it obvious how finding functions with a specific metadata is useful. You can find all unused functions in whichever way you want, and then remove from that set all functions mentioned by that event->callback map. The remaining functions will all be truly unused. Unless, of course, there's some dynamic usage.

namenu14:05:06

Yes, you are correct. What I didn’t note is that defhandler expands to defn with metadata, and I came with the solution based on that.