Ok I have another follow on question from the above: I am wanting to make a defprotocol macro which produces a protocol and generates another var alongside. Here is a minimal example of what I am trying to do:
(ns hooks.io.julienvincent.example
(:refer-clojure :exclude [defprotocol])
(:require
[clj-kondo.hooks-api :as api]))
(defn defprotocol [{:keys [node]}]
(let [[_ name-node & rest-children] (:children node)
protocol-node (api/list-node
(into [(api/token-node 'defprotocol)
name-node]
rest-children))
name-sym (api/sexpr name-node)
additional-node-name (symbol (str "?" (name name-sym)))
additional-node (api/list-node [(api/token-node 'def)
(api/token-node additional-node-name)
(api/token-node 'nil)])]
{:node (api/list-node
[(api/token-node 'do)
;; Protocol node first otherwise we lose references
protocol-node
(with-meta additional-node (meta name-node))
;; Generate fake usage to prevent clojure-lsp from reporting
;; unused-var warnings
(with-meta (api/token-node additional-node-name) (meta name-node))])}))
Usage looking like:
(ns io.julienvincent.kondo-test
(:require
[io.julienvincent.example :as example]))
(example/defprotocol Example
(foo [x]))
;; The base protocol is here
(reify Example
(foo [_] 1))
;; This is the extra node I produced
(println ?Example)
---
The problem I am having is with references. I can't find a way to 'combine' references for both the protocol and the additional var. If I go-to-references from the macro protocol definition then it will only find references for the first node exported in the clj-kondo hook (the protocol in this case) and won't find the following nodes (such as the var ?Example in this case).
If I go-to-definition from usages of the var ?Example it does correctly take me to the definition, just not the other way around.
Is this just a core limitation of the systems at play or is there perhaps a way to do what I want here? i.e is there a way to ensure ?Example is included in references to my Example macro definition in addition to the protocol.So I attempted to recreate your success with AI and got codex to fix the above for me: https://github.com/julienvincent/clojure-lsp/commit/fef6e0bf691b8090a3650644ca0a69dc3b01f84e The changes were much more extensive, and I didn't read any of it (I have no familiarity with the code-base anyway), but this does allow me to at least try out the user-experience of the solution locally. I can confirm that the approach of substring renaming works pretty well - at least for my limited testing and at least for my use-case. I gave instruction to attempt a rename using the existing mechanism (but for all discovered references) and then fall back to a substring replacement if no text was changed at a particular usage location. Do with this what you want :)
Cool, interesting, will take a look soon, the good thing is that the rename is well tested
Yea it is from what I saw. Also that implementation seems entirely contained to the rename operation and mostly contains additions it seems. Based on that I would imagine a real implementation to be quite feasible
Back at this, the issue is well described and clear, thank you @m401! Actually it was so clear, I gave a try just to see how good ECA would solve this with a simple prompt of "solve the issue #2176", and sonnet-4.5 solved in 2mins with https://github.com/clojure-lsp/clojure-lsp/commit/484b4eac682a1c9df1f611ca30607151aae0cb72 😅🫠 kinda scary he The good thing was the solution change was well localized LMK if nightly works the same for you @m401! The print shows the result when finding references from the definition, showing the 2 places
Wow that's actually insane! Guess we are all out of a job ;) I saw the issue get closed and I'm already on it to see if this works for me! Will report back shortly
The solution was kinda easy, after you understand the problem, and I think that's the thing LLM suites really well, understanding and giving you context about a problem
Yea, and it found the convenient find-[all]-elements-under-cursor existing fn. That's cool
exactly hehe
aand of course the macos-aarch64 nightly job is the slowest to complete 😭
Ok, I can confirm that the issue as described is fixed! One caveat being that rename does not work. I suppose this is expected. I was assuming when I raised this that it would "just work" once references work, but now that I think about it I assume this must be a completely different mechanism.
So.. is there a way to get renaming to work as well? I guess that isn't powered by clj-kondo
ah yeah, especially because there are 2 var-definitions there... can you give a example of a renaming? just to make sure I understand the desired result
Sure, to keep the same example as before - renaming the definition should rename all the references. Kind of how renaming (defrecord Point) to (defrecord Point2) will rename ->Point2 and map->Point2. But I don't see how this will be possible, because the name of Example1 and Example2 (from my issue example) are determined by the hook. So clojure-lsp would need to.. re-eval the hook before renaming??
exactly
I can't see a way to solve that because of that
is the defrecord scenario special-cased in clojure-lsp?
no, the issue is the custom naming, like Example for definition but the references are ?Example and ?Example2
Yea but defrecord var names also don't match the source?
ah, there is special handling for defrecord yes, just for map-> and ->
we look especificaly for those
Perhaps we can explore how this might look in an ideal future. There would need to be some interaction with clj-kondo I presume, in a way where the special casing for defrecord could be described entirely from kondo
but that would mean kondo would need to understand the implications of renaming something.. which feels out of scope for what its doing
Without knowing anything about the internals of clojure-lsp, would it be feasible for clojure-lsp to re-run the hook and correlate new var names based on reported index?
yeah, or maybe we could fallback for something like: renaming "Example" -> "ExampleFoo" we would get the references and kinda replace the Example -> ExampleFoo , resulting in ?ExampleFoo2
not easy to re-run the hooks, they happen after the code was changed, after the rename
yea so it would need to be a two-phase rename, which feels stupid
Your solution sounds like it would work for most cases
I guess, we could explore that
I'm sure someone could write a funky hook where that would not work
please create a new one to help keep track
yes, indeed
Ok! I'll get to it in a few
Issue tracked here: https://github.com/clojure-lsp/clojure-lsp/issues/2183
FYI here is another related issue: https://github.com/clojure-lsp/clojure-lsp/issues/2192 Again, I got AI to fix this in my local fork so I can try out the proposed solution. The solution it produced looks pretty good, with very minimal changes
I decided to open a PR with the patch
@m401 does that happens to unused-private-var as well? Will take a look later, thanks
I would suspect this is a limitation in clojure-lsp @ericdallo is that correct? As clj-kondo does have all the information.
It's probably an LSP issue. If you look at the exported analysis data, the var + location should be there and this is all LSP should need
try it out and see if the data is correct
Reading
So, in the analysis the defined-by says def, not my macro. I'm not sure if that's relevant?
"var-definitions": [
{
"end-row": 6,
"name-end-col": 26,
"name-end-row": 5,
"name-row": 5,
"ns": "io.julienvincent.kondo-test",
"name": "?Example",
"defined-by": "clojure.core/def",
"filename": "clj-kondo-test/io/julienvincent/kondo_test.clj",
"col": 1,
"name-col": 19,
"defined-by->lint-as": "clojure.core/def",
"end-col": 13,
"row": 5
},is lsp just using the location information?
it's defined by def since your macro expands into defining the var using def
I simplified the example to have my macro just produce two identacal (do (def ?Example nil) (def ?Example2 nil)) nodes, both pointing to the same location and only the first one has reference (via lsp). The clj-kondo analysis looks identical for both
anyway, the location is there and this is all lsp needs. take it up with Eric :)
Ok! I will wait for his maybe reply here :)
clojure-lsp resolves that by ns, name and location if both have the same it should work we don't do custom stuff for that, let me test your repro
If you give me 10 mins I can prepare a repro repo for you
that will certainly help!
https://github.com/julienvincent/clojure-lsp-references-repro - here you go! If you go to the io.julienvincent.kondo-test ns you can see that references to Example only go to the first var
thanks, cloning it
ok, let me check if I understand the problem because I suspect this is expected: I believe this is enough for the discussion, you have:
1 (ns foo)
2
3 (defmacro defthings [_name]) ;; find-references should return line 5 only, as it's the only defthings usage.
4
5 (defthings Example) ;; find-references in Example should return lines 5 and 7, find-definition goes to itself, line 5
6
7 (println ?Example) ;; find-references in ?Example should return lines 5 and 7, find-definition goes to line 5
isn't that expected?if you want to find ferences of the defthings in line 3 and return line 7, then this is pretty uncommon, not sure if we support even changing the locs, as the names won't even match
Not quite the problem:
The macro (defthings Example) produces two vars: ?Example and ?Example2. If you look at the clj-kondo hook you can see we define these two nodes.
Only the first node reported by the hook is included in references to the macro call (defthings Example). The (println ?Example2) reference is 'lost' when searching from the macro call
I.e performing a find-references from (defthings Example) finds only (println ?Example) and not (println ?Example2)
ah sorry, I misread the hook
I see the problem, let me deep dive to understand why happens
BTW, there is a quick way to test analysis, if you use emacs or calva, lsp-clojure-cursor-info it shows analysis, semantic tokens etc of cur cursor, pretty useful to debug hooks and so
Neovim all the way 🤘
haha it should be easy to have the same in neovim, as it's a server command
I know people who use emacs because it's a better vim. I can't attest since I don't know vim at all ;)
@borkdude Do you want to start a war? Here? Now!?? :P
let's take it outside
😂
there is room for any editor, even the bad and ugly ones
you mean emacs right?
My duckling is beautiful to me
neovim starts fast which is a great feature
Starts fast, dies slow
I believe I found the root cause, it's because during find-references feature, we check for the https://github.com/clojure-lsp/clojure-lsp/blob/f555e4b89a1a3aa8aa9f66809f0d5e4dc034c61d/lib/src/clojure_lsp/queries.clj#L540 with https://github.com/clojure-lsp/clojure-lsp/blob/f555e4b89a1a3aa8aa9f66809f0d5e4dc034c61d/lib/src/clojure_lsp/queries.clj#L536 for defrecord,deftype (map->, ->, etc)
not sure the best way to fix that but would be certainly a new feature, like kondo pass some kind of analysis and lsp use that, not sure we want that
this becomes clear when you see the macro expansion, there is nothing that co-relates the ?Example with ?Example2, they are just different vars being defined by the same macro
What scenario is that filter accounting for? Is it just a safety net, or are there good reasons to do this check in the first place? I.e why is references pointing to same location not enough
that's a very good question :)
from git blame, we added in 2022 to exclude declare when calculating unused-public-vars, but it's not clear if that's still a issue, and probably there are other ways to filter now that we have :defined-by 'declare
... and waiting to see if anyone complainsI'm complaining! :D
yeah, removing that check doesn't break tests which is bad, I can't see easily now a case where not checking the name will cause issues
actually some tests fail, let me check
Hum, that filter by name is quite important, is actually the only reliable way to know a var-usage somehwere in the code is related to that definition, because the ns and name must match, otherwise how you could tell only by location?
Example:
(defthings Example) var-def:
{:name-row 5 :name-col 12 :name-end-row 5 :name-end-col 19
:row 5 :col 1 :end-row 5 :end-col 20
:external? false
:ns io.julienvincent.references
:name ?Example2
:defined-by clojure.core/def
:defined-by->lint-as clojure.core/def
:callstack [{:ns clojure.core :name def} {:ns clojure.core :name do}]
:uri "..."
:meta {}
:bucket :var-definitions}
?Example var-usage:
{:row 7 :col 10 :end-row 7 :end-col 18
:name-row 7 :name-col 10 :name-end-row 7 :name-end-col 18
:external? false
:name ?Example
:from io.julienvincent.references
:to io.julienvincent.references
:context {}
:uri "..."
:bucket :var-usages}
?Example2 var-usage:
{:row 8 :col 10 :end-row 8 :end-col 19
:name-row 8 :name-col 10 :name-end-row 8 :name-end-col 19
:external? false
:name ?Example2
:from io.julienvincent.references
:to io.julienvincent.references
:context {}
:uri "..."
:bucket :var-usages}
I can't see a way to co-relate those if not using the ns and name when finding references, the location on usages, say nothing to solve this problemdoesn't clj-kondo already tell you which namespace and name it is? I don't understand the problem
you have a var def for Example2 and a usage of Example2, what's the problem?
ah you are right, the problem is: we have 2 var-defs for the same position! I actually mispasted:
(defthings Example) var defs:
{:row 5 :col 1 :end-row 5 :end-col 20
:name-row 5 :name-col 12 :name-end-row 5 :name-end-col 19
:ns io.julienvincent.references
:name ?Example
:external? false
:defined-by clojure.core/def
:defined-by->lint-as clojure.core/def
:meta {}
:callstack [{:ns clojure.core :name def} {:ns clojure.core :name do}]
:uri "..."
:bucket :var-definitions}
{:row 5 :col 1 :end-row 5 :end-col 20
:name-row 5 :name-col 12 :name-end-row 5 :name-end-col 19
:external? false
:ns io.julienvincent.references
:name ?Example2
:defined-by clojure.core/def
:defined-by->lint-as clojure.core/def
:callstack [{:ns clojure.core :name def} {:ns clojure.core :name do}]
:uri "..."
:meta {}
:bucket :var-definitions}
so this is the different thing that's affecting clojure-lsplet me check
perhaps you do a group-by on location or so? or index-by or whatever, which assumes 1 var per location
yep we do a distinct-by uri name row col: https://github.com/clojure-lsp/clojure-lsp/blob/f555e4b89a1a3aa8aa9f66809f0d5e4dc034c61d/lib/src/clojure_lsp/queries.clj#L546
maybe it's better to use group-by or so then?
is :name the var name?
in that case it shouldn't be a problem since the name is different?
I think the problem is not in the references, but the var-defnition that comes https://github.com/clojure-lsp/clojure-lsp/blob/f555e4b89a1a3aa8aa9f66809f0d5e4dc034c61d/lib/src/clojure_lsp/queries.clj#L535
the problem is https://github.com/clojure-lsp/clojure-lsp/blob/f555e4b89a1a3aa8aa9f66809f0d5e4dc034c61d/lib/src/clojure_lsp/queries.clj#L643-L648, like how we could know which var-definition from cursor we should look you know
the find-references is working, it's finding the references of the given var-definition, the problem is, when you have 2 var-defs with same exact position, which one should we use, or should we consider to use both? that sounds weird
does it matter, since they are at the same location, the editor will bring you to the macro call?
yeah, we know the location will be the macro call, but the var-def in mmemory will have name ?Example not ?Example2
the point is that when one use the find-references feature, we first find the definition of it to then find the references
I think we could refactor the find-definition-from-cursor to return multiple elements, but there are 375 usages of that :) we would need to create some new or think in a better way
you can use the name of the initiating position?
yeah, we could try to check for that
anyway, not trivial fix, we would need to take care there, create tests for clojure-lsp first to repro that and then try to fix it, It's still not entirely sure what parts of the codebase would be required to change becase those features are kind related for this problem, for example fixing the var-definition would not be enough, as it would just return the other var-def and then on find-references we would exclude the current selected one which is wrong, which let me think that maybe we would need to change find-references code as well. @m401 please create a issue, will require more deep diving
doesn't LLMs solve all problems nowadays? 30% more productivity
ha! for sake of curiosity I can give ECA a try to see what's the outcome :p
(this was a joke btw ;P, but worth a shot ;))
creating a clojure test that repro the problem would help for sure LLM find a way (maybe not the best he)
hehe
it's kinda bizarre how it arrived at same conclusion of us in 1min after providing just the hook + usage and the issue:
Now I understand the problem. When the clj-kondo hook generates multiple definitions (like `?Example` and `?Example2`) from the same source location, they share the same `name-row` and `name-col` (since both use `(meta name-node)` for the name token).
When finding references for `?Example`, the current code:
1. Converts the var-usage to a var-definition with `{:ns ... :name '?Example :bucket :var-definitions}`
2. Searches for usages matching that specific name
The fix should find all var-definitions that share the same source position (`name-row`/`name-col`) and include all their names in the search.
Let me check if there's more context around how elements are found at a cursor position:This doesn't make sense to me. I thought you were trying to find var references for Example2
yes, it was my provided repro to it, the final decision was this in the plan mode:
The issue is clear: when clj-kondo hooks generate multiple var-definitions from the same source location (sharing `name-row`/`name-col`), find-references only returns usages for the specific var name being queried, not all co-located vars.
**Chosen Approach**: Modify `find-references :var-usages` to:
1. Look up the var-definition for the given var-usage
2. Find all other var-definitions in the same URI that share the same `name-row` and `name-col`
3. Collect all names from these co-located definitions
4. Find references for all of them
**Why this approach:**
- It respects the hook's intent: when multiple definitions share a source position, they're semantically related
- It's a minimal, targeted change to the existing find-references dispatch
- It follows the existing pattern used for `defrecord`/`deftype` (where `var-definition-names` already handles multiple names like `->RecordName` and `map->RecordName`)
**Alternative considered**: Modifying `var-definition-names` to also return co-located var names. However, this would require the var-definition to carry information about its position and require access to the db/analysis, which breaks the current pure function design.
I'm asking for preview file changes to understand how's gonna solve it, but it seems to be a brutal approachThis thread is making my week
I'm out for lunch but I'll write this up into an issue when I get back regardless :)
Let's first agree on the problem. Macro foo defines two vars: example1 and example2. When a user is on a usage of example2 and searches for references, you only want to see references of example2, not example1. Correct? So what lsp normally does: it goes from usage to definition and then to references. But because of locations overlapping, it somehow doesn't find example2.
yes, makes sense
we want the find-references from definition in the code to find both example1 and example2
that is not what I wrote
Yea @borkdude I'm not following your description. Eric's comment describes the problem unless you ask trying to describe something else?
hum, I do think he wants to find references from the definition
Yes, find-references from definition is the problem from my perspective. Find references / find definition from Example2 should just point back to the definition - which it already does correctly
yeah that sounds more accurate
Yeah ok. Two different scenarios then with potentially different outcomes. 1. Find references with cursor at (my-macro Example). It should find Example1 and Example2 references. 2. Find references with cursor at Example2. It should only find Example2 references?
yes, for me only 1 is what we want
For 2. It should go back to definition no? Not other references of the definition
yes, 2 already works
This is scenario 2. My cursor was at the 3rd usage
Normal operation of lsp find-references of a usage just finds the definition afaik. Not at pc to confirm
no, it doesn't just find the definition, it finds the references
Either way, the only operation I care about from a usage is going back to the definition - which already works
Eric was mentioning that find references from a usage goes via the definition, which is why I understood that as the problem. It is also a problem, just not your original problem
Makes sense, agreed
Issue created here: https://github.com/clojure-lsp/clojure-lsp/issues/2176
thanks, I needed to left but will check it soon