lsp 2025-11-18

It seems one can't navigate to an inner class which is denoted with a dollar in the middle: Foo$Bar

Indeed, this was never implemented but always bug me, would you mind create a issue about it? it should not be that hard to parse that and try to find a class, or maybe we can solve somehow on kondo

@borkdude taking a look at that, I found that there is a one line fix in kondo that could make that work, but I wanna know if there was reason for it:

it seems kondo add to :java-class-definitions the :class with inner class separated by . instead of $

example: :class my_package.Foo.Bar instead of my_package.Foo$Bar , so when clojure-lsp compares it doesn't match

it's weird because java-class-usages does use Foo$Bar in its class field

I can do a replace-all . -> $ in clojure-lsp, but looks to me that this inconsistency should be fixed on kondo, WDYT

actually the replace is tricky as I don't know what is class and what is package: my.package.Foo.Bar , maybe only checking upper case letters

yeah I don't know why we did that. feel free to change it, I don't think it will be important to preserve that

👍 1

hum, I think we might not be outputing java-class-definitions for inner class in kondo too

not really:

$ clj -M:clj-kondo/dev --lint ~/dev/clojure-lsp/lib/test/fixtures/java_interop/Parent.java --config '{:output {:format :edn :analysis {:java-class-definitions true}}}' | jet -t ':analysis :java-class-definitions'

[{:class "my_class.Parent",
  :uri "file:/home/greg/dev/clojure-lsp/lib/test/fixtures/java_interop/Parent.java",
  :filename "/home/greg/dev/clojure-lsp/lib/test/fixtures/java_interop/Parent.java",
  :flags #{}}
 {:class "my_class.Parent.Child",
  :uri "file:/home/greg/dev/clojure-lsp/lib/test/fixtures/java_interop/Parent.java",
  :filename "/home/greg/dev/clojure-lsp/lib/test/fixtures/java_interop/Parent.java",
  :flags #{}}]

so it does report the inner class right? just not with dollar

and why no line/col number?

yeah, good question, I think we never managed to get those on kondo side

but would be nice to have them

otherwise navigation will always to top of file

which for inner class is not cool

especially for inner classes yes

I will check why it's not working in clojure-lsp side the inner class definition even replacing the $

you should only replace the last .

I guess it's ambiguous since foo.bar.Baz could be Baz in a package foo.bar or Baz could be an inner class of foo.bar

yeah, I did the other way around for now:

(string/replace (:class java-class-usage) "$" ".")
replacing the java-class-usage instead for quick check

so we really need to fix it in clj-kondo because of ambiguity

I don't know man. Don't you have AI tools for this? ;P

😂 1
😮 1

😂 yeah, I can try that

(ECA found it he)

so we may want to replace there

how do you know it's an inner class there?

that's what I'm looking right now

the normalize-inner-class-names looks really complicated. why isn't it just a matter of replacing the last dot with a dollar?

package names can have capital letters too, class names can be all lowercase

what function can I use to replace the last?

also, what about multiple inner classes

so relying on that isn't accurate

Foo$Bar$Baz

hum, if class names can be lower case so we have a problem indeed

they usually aren't but they can

can you get to the package name of the class? if you have that, then the rest is dollars

or the parent class

I think so

we use asm for .class and javaparser for .java

yes, your PR was about asm right

oh I see now

it was javaparser

so we need an additional fix for ASM I guess

I got the package, and made this:

(defn ^:private normalize-inner-class-names "Replace the . of full-class-name after package-name with $ Example: full-class-name: my.custom.package.Foo.Bar.Baz packcage-name: my.custom.package -> my.custom.package.Foo$Bar$Baz" [full-class-name package-name] (let [prefix (when-not (str/blank? package-name) (str package-name "."))] (if (and prefix (not (str/starts-with? full-class-name prefix))) ;; If the full-class-name doesn't start with the given package-name, ;; leave it unchanged. full-class-name (let [rest-name (if prefix (subs full-class-name (count prefix)) full-class-name) segments (str/split rest-name #"\.") [top & inners] segments] (if (empty? inners) ;; No inner classes to normalize. full-class-name ;; Rebuild name with $ between inner class segments. (str (or prefix "") top "$" (str/join "$" inners)))))))``` but looks complex yet

ASM seems to output as $ already

I'd say it was just javaparser bad choice

thanks, do you see any way to make simpler that code?

I think I do. If you know the package name, you can just "substract" that from the full class name, replace dots with $ and be done?

you don't first have to split the class name

ah of course, sounds simpler indeed, let me try

way better :)

(defn ^:private normalize-inner-class-names
  "Replace the . of full-class-name after package-name with $
   Example:
     full-class-name: my.custom.package.Foo.Bar.Baz
     packcage-name: my.custom.package
  
     -> my.custom.package.Foo$Bar$Baz"
  [full-class-name package-name]
  (let [prefix (if (string/blank? package-name)
                 ""
                 (str package-name "."))
        class-name (subs full-class-name (count prefix) (count full-class-name))
        class-name (str/replace class-name "." "$")]
    (str prefix class-name)))

hmm, I still can't navigate to the class

I threw away my .clj-kondo directory

I'll also throw away .lsp cache

nope, still no luck

so maybe something has to be changed on lsp as well?

yeah, I think the issue might be on clojure-lsp side, I'm still investigating

Hum, I think there is still something to fix on kondo, when linting a jar, example:

clj -M:clj-kondo/dev --lint ~/.m2/repository/org/benf/cfr/0.152/cfr-0.152.jar --config '{:output {:format :edn :analysis {:java-class-definitions true :java-member-definitions true :java-class-usages true}}}' | jet -t ':analysis :java-class-definitions'
I do not get the inner classes, like CfrDriver$Builder

it might be an ASM issue now, since this jar has .class

do you mean, without the package name?

no, a different issue, I'm checking but I think kondo is ignoring inner classes when looping the jar class files

could be. then we need visitInnerClass

hum, maybe

if it's not there yt

yeah, there is no visitInnerClass being used

I think we indeed need to implement visitInnerClass

will try to create a test for that

managed to create a test, the issue is when the .class is inside a jar, the .class alone works 🤯

will now debug it

well, I don't know all the consequences of removing that :)

I think that was added to prevent analyzing all kinds of generated classes from clojure core AOT

yeah, I imaged that

-rw-r--r--  2.0 unx     2251 b- defN 25-Jun-02 14:10 clojure/core$memfn.class
etc

-rw-r--r--  2.0 unx     1031 b- defN 25-Jun-02 14:10 clojure/uuid$fn__8546.class

perhaps we can just ignore clojure/ + $

but other AOT-ed clojure libs will have similar junk

yeah, kind hard

would be nice if we could somehow distinguish clojure generated $ classes than java ones

dollar + all lower case name (barring munging) would do it I guess

-rw-r--r--  2.0 unx     1216 b- defN 25-Jun-02 14:10 clojure/core$reduced_QMARK___inliner__5990.class

I mean _QMARK_

or maybe simpler: $ followed by a lowercase character

seems a safe heuristic

Seems better yeah

we would miss classes generated from deftype/protocols etc I think, but those are relatively rare

but defrecords classes would keep yet right?

sounds acceptable

we can check the difference by loading bb extract:java-members

and see how many files are added

or run lsp on clojure (which it does at startup) and see how much extra garbage that yields)

Ok! does that looks good?

(or (not (str/includes? nm "$"))
    (not (Character/isLowerCase (nth nm (inc (.indexOf nm "$"))))))

It works with clojure-lsp!

ah spoke too soon

I think the issue now is the decompilation with Cfr on lsp side 🫠

yeah you need to decompile the outer class

Ok, I think it's working, will push to clj-kondo and do more tests with lsp

1 windows test seems to be failing:

FAIL in (local-classes-test) (java_test.clj:88)
expected: (match? (m/in-any-order [{:class "foo.bar.AwesomeClass", :uri #"file:.*/corpus/java/classes/foo/bar/AwesomeClass.class", :filename #".*corpus/java/classes/foo/bar/AwesomeClass.class"} {:class "foo.bar.AwesomeClass$Foo", :uri #"file:.*/corpus/java/classes/foo/bar/AwesomeClass\$Foo.class", :filename #".*corpus/java/classes/foo/bar/AwesomeClass\$Foo.class"}]) awesome-class-defs)
  actual: ({:class "foo.bar.AwesomeClass$Foo",
  :uri
  "file:/D:/a/clj-kondo/clj-kondo/corpus/java/classes/foo/bar/AwesomeClass$Foo.class",
  :filename
  (mismatch
   (expected
    #".*corpus/java/classes/foo/bar/AwesomeClass\$Foo.class")
   (actual
    "D:\\a\\clj-kondo\\clj-kondo\\corpus\\java\\classes\\foo\\bar\\AwesomeClass$Foo.class")),
  :flags #{}}
 {:class "foo.bar.AwesomeClass",
  :uri
  "file:/D:/a/clj-kondo/clj-kondo/corpus/java/classes/foo/bar/AwesomeClass.class",
  :filename
  (mismatch
   (expected
    #".*corpus/java/classes/foo/bar/AwesomeClass.class")
   (actual
    "D:\\a\\clj-kondo\\clj-kondo\\corpus\\java\\classes\\foo\\bar\\AwesomeClass.class")),
  :flags #{:public}})

Yeah, I will just check for the class existence

something to improve in clj-kondo assert-submaps2 in the future is support use something like m/in-any-order from matcher-combinators

for now, it should work my last push

Not sure why one check is failing, doesn't related to the PR

let me look at the code

some questions. why was corpus/java/sources/foo/bar/AwesomeClass.java changed?

because its code was wrong

there was a method with a class inside it

I don't know how that passed before

you tried to compile it? ok

I see it now, that's weird indeed

yeah, it works with clojure-lsp, fixes the issue on clojure-lsp side

my suggestion here would be:

(or (not (str/includes? nm "$"))
                                                 (not (Character/isLowerCase ^Character (nth nm (inc (.indexOf nm "$"))))))
First take the index of "$". If it's negative, we're good. If it's positive, use .charAt to take the next character and check for lowercase. You should then also check if you're not beyond the length of the string for out of bounds exception or whatnot.

I mean, charAt will crash if you go beyond the length

actually nth will do that for you if you provide a not-found value, so let's do that

got it, what default-value?

not-found*

it could be any char

something like this?

(when-let [idx (str/index-of ...)]
  (not (Character/isLowerCase ^Character (nth nm (inc idx) \A)))
It's a bit ugly
(when-let [idx (str/index-of ...)]
  (when-let [^Character c (nth nm (inc idx) nil)
    (not (Character/isLowerCase c))

then when-let is cleaner

oh wait, it should return true when it doesn't include $ so you should also do that

(or (not (str/includes? nm "$"))
                                                 (when-let [idx (str/index-of nm "$")]
                                                   (when-let [^Character c (nth nm (inc idx) nil)]
                                                     (not (Character/isLowerCase c)))))
?

(not (some-> (str/index-of ...)
             inc
             (->> (nth nm nil))
             Character/isLowerCase))
hehe almost, the thread isn't right in nth

or we can get rid of the firs when-let because the or already checks that

(or (not (str/includes? nm "$"))
                                                 (when-let [^Character c (nth nm (inc (str/index-of nm "$")) nil)]
                                                   (not (Character/isLowerCase c))))

(let [idx (str/index-of nm "$")]
  (or (not idx)
    (when-let [^Character c (nth nm (inc idx) nil)]
      (not ...))

I think I'd prefer that

thanks so much. the only thing left checking is how much more garbage we get in the class definition/usage stuff in .clj-kondo/.cache

ah yeah, need to check that

i.e. if our filter for clojure classes is good enough

I think the bb idea was the best

bb extract:java-members

Iḿ getting this tho:

Execution error (NoSuchFileException) at sun.nio.fs.UnixException/translateToIOException (UnixException.java:92).
/tmp/java-cache/v1/java

Full report at:
/tmp/clojure-1439160252713424056.edn
Error while executing task: extract:java-members

let me take alook

oh I'm not getting it. do you have a tmp dir?

I can change that if it's a nixos thing

maybe paste the output of /tmp/clojure-1439160252713424056.edn in a gist

it just works over here. I'll be afk for a brief moment, but will be back

I'm bee afk too, feel free to check that if you can, otherwise I can take a look tomorrow, I believe this check would be better if done in your side because of this nix issue 🙏

oh ok, yeah that makes sense

@ericdallo I'm inspecting classes like this:

clj -M:clj-kondo/dev --lint "$(clojure -Spath)" --config '{:output {:format :edn} :analysis {:java-class-definitions true :java-class-usages true}}' > /tmp/out.edn

cat /tmp/out.edn | jet --from edn -t ':analysis :java-class-definitions' > /tmp/class.edn
In class.edn I find classes like:
"com.github.javaparser.GeneratedJavaParser$1"
com.github.javaparser.ParserConfiguration$1
also:
clojure.core$_LT__EQ___inliner__5641
clojure.core$Throwable__GT_map$fn__7563
clojure.lang.Util$3
clojure.core$_PLUS__SINGLEQUOTE_
I'll paste the class.edn file here so we can filter out all clojure.* names to inspect them more. But it seems as a rule of thumb: We can also ignore • inner classes starting with numbers • underscores (result from munging, probably not interesting for java interop?) • containing the string __inliner__ Perhaps some more

you don't need the bb task for this, that honestly only analyzed source code so it didn't catch anything interesting for the ASM filter

I see, sounds good

I can make a commit (since you pushed in a branch of the kondo repo anyway)

pushed. I think I got most of the junk now. can you review also the changed files to see if there's not any junk in there?

then I'll merge if CI passes

let me check

a lot of cache files were changed, was that expected?

besides that, looks good

yes, a lot of Foo.Bar were changed in Foo$Bar ;)

😅 1

Done in clojure-lsp!

the localtions are probably something we wanna improve next in clj-kondo, especially now with inner classes

awesome, confirming it works barring location.

but this is already great!

Is there a way to make clojure-lsp think that a squint project is a cljs project? Because I'm having a problem when trying to look at the clojure.core fns documentation in my editor (things like defn for example) it doesn't show anything, but in a normal cljs project it works correcly. But in the same project I can see docstrings and all other features of lsp, only things related to clojure.core stuff doesn't work for me. In the printscreen you can see: • Normal cljs project documentation over defn, working • Squint project documentation over defn, not working • Squint project documentation of a local defined function, working Repro: https://github.com/rafaeldelboni/squint-vite-react Version:

clojure-lsp --version
clojure-lsp 2025.08.25-14.21.46
clj-kondo 2025.07.28

Interesting, Clojure-lsp guess that by the extension, is there any repro I can try? I never tried squint before

thanks, will try it later tonight

❤️ 1

a workaround to this problem is probably to add a deps.edn with :paths ["src"]

just to fool clojure-lsp into thinking it belongs to the classpath

but would be nice if clojure-lsp recognized squint.edn as a deps file too

that's easy to recognize, @borkdude is there any classpath string command using squint? like bb print-deps --format classpath

no, but you can just treat it like a deps.edn for now

the only relevant part is :paths

yeah, same issue we had with bb, there is no easy way to simulate the classpath unless we have a one-line clojure command that returns a classpath string

deps.clj supports -Sdeps-file squint.edn

maybe the official clojure CLI does now too, let me check

yep:

-Sdeps EDN     Deps data or file to use as the last deps file to be merged

so you can just use that for squint

nice, will try

@borkdude this won't work for this case, since clojurescript is not available, so cljs.core/defn analysis don't exist. I believe the best here is have a npx squint classpath command or something, that would make clojure-lsp easier as well

otherwise we would need to read and get path of squint.edn , use the path to pass to clj -Sdeps '{:paths [...]}' and include the clojurescript lib with a hardcoded version, seems too many workarounds to live in clojure-lsp

eh no... like I said, you can now use clj -Sdeps squint.edn

and why include clojurescript? you don't need it for defn, clj-kondo has built-in stuff for defn etc.

oh it's about docstrings, then yeah. Maybe the user can then just use a deps.edn with a :dev alias with clojurescript and the squint path in it. I'm not going to add a premature classpath thing to squint, that just doesn't make sense, especially not since it's not really CLJS

not only docstrings, but analysis which affect more features than only docstrings I believe any build tool related to clojure should have a way to get a classpath, for now I'm trying clj -Sdeps squint.edn -Spath , so user need to include clojurescript dep in squint.edn in a dev alias right?

@rafaeldelboni is that enough for you?

Sounds ok to me, yes

I think that reduces the "Just works" behavior, as all new squint user would need to configure that to have a working IDE, but sounds like a good start

After this is done, worth adding a note about lsp support on squint readme, wdyt borkdude?

Squint edn doesnt support a dev alias but since its ignored its fine to add it. Wouldn’t it make sense to just add built in default analysis for some libs though? Nbb probably has the same issue with CLJS. Clj-kondo has built in support for .cljs and .clj files by just including some default libraries in its bundled analysis including the newest clojure and ClojureScript

Clojure dart users or any other new dialect would also benefit from that

Since lsp syncs with clojuredocs, wouldn't be the case for those cases (squint, nbb, cljdart) to just show data from there?

the problem is not docstrings/clojuredocs, but analysis

find definition won t work for example without fixing this issue

@borkdude I think we can fix especifically for clojure and clojurescript, but still sounds like a adhoc solution, especially what version to use, missing functions, or any extra lib that may exist (like cljd lib itself)

well find definition would not be a reason to add CLJS to squint deps, CLJS defn isnt squint defn

So that would be a bogus find definition

yeah I don't know what squint does, but the point is that I think the build tool should tell what lib is using, not LSP

like bb having lots of built-in libs and stuff that are printed via bb print-deps --format classpath and that works great now

Yes, for bb it makes sense. For squint, not so much,except that you can treat squint.edn (and nbb.edn) as deps.edn. Similar for Clojure CLR btw

I’ll figure something out to add to the README without changing either Clojure-LSP and squint, it’s doable

yeah I can see the difference, it's something to think a little bit more indeed

@rafaeldelboni can you try this (and perhaps Eric can double-check if I'm suggesting the right thing):

{:project-specs [{:project-path "squint.edn"
                  :classpath-cmd ["clojure" "-Spath" "-A:lsp"]}]}
and then in squint.edn:
{:paths ["src"]
 :aliases {:lsp {:extra-deps {org.clojure.clojurescript {:mvn/version "..."
I don't know what project path is for. Perhaps just this also works:
{:project-specs [{:classpath-cmd ["clojure" "-Spath" "-Sdeps" "squint.edn" "-A:lsp"]}]}
Note that the order of -Sdeps and -A:... is wrong in the original lsp docs. According to the clojure CLI -S stuff goes first. Sometimes it doesn't matter but sometimes it does, so better do it the official way

project-path is what will trigger that command, clojure-lsp will only trigger if that file is found

yeah, for a temporary solution that should work

Where do I add the project-specs config? .lsp/config.edn?

ok so in both cases you do need to add the -Sdeps squint.edn argument as well

it worked for me with a mix of both

{:project-specs [{:project-path "squint.edn"
                  :classpath-cmd ["clojure" "-Spath" "-Sdeps" "squint.edn" "-A:lsp"]}]}

I can't declares a project-specs without a project-path

and the other classpath wasn't getting the clojurescript into the paths

yeah I guess that's expected

the other classpath?

{:project-specs [{:project-path "squint.edn"
                  :classpath-cmd ["clojure" "-Spath" "-A:lsp"]}]}

this example

yes, I told later: you need -Sdeps squint.edn for both examples

yeah but it works I will update the repro repo

cool! maybe you can make a PR to squint's README where we mention this for LSP users

actually this would work for nbb users as well

is the lsp alias required? can't be dev? it's the standard lsp uses for all other project types

so with that we could have that on lsp, and user would just need to declare clojurescript in that profile

it can be dev but this would suggest that clojurescript is needed for squint development, which isn't true

tooling is development 😛

fair enough

I'll let @ericdallo suggest the simpler one with the dev alias. Since you still need the classpath-cmd, I don't see how that would be much simpler though

just {:project-paths "squint.edn"} doesn't work

yeah, I can add to clojure-lsp this project-path + classpath cmd to run the default aliases like done for other tooling, so users would just need to have that alias with dev profile, with clojurescript in there

sounds good enough for now

👍 1

@rafaeldelboni would you mind create a clojure-lsp issue for that please? to include squint.edn as a project-spec

👍 1
🎉 1

Done! available soon in #clojure-lsp-builds, I should release clojure-lsp soon too

out of curiosity my version is stuck long time on clojure-lsp 2025.08.25-14.21.46

in brew and archlinux

yep, it's the latest

is there something I'm missing?

I may do a release after me and borkdude fix https://clojurians.slack.com/archives/CPABC1H61/p1763467383675949

❤️ 2

I have some elisp code that I run when I'm switching work between different projects (mostly it just closes buffers, and clears some other state), and something about it is triggering this minibuffer prompt:

Server clojure-lsp:267077 exited (check corresponding stderr buffer for details). Do you want to restart it? (y or n) 
The mentioned stderr buffer is also gone at this point. Is there some way to not have this prompt pop up? (I don't think I really care if it 'restarts' or not, I'm happy to have it restart when I later open another project's file, or not, or whatever simple_smile).

the way lsp-mode works is to kill any left lsp servers running if all buffers related to it were kill But good question, never wondered about that, let me check

🙇 1

I think:

(setq lsp-restart 'ignore)
or auto-restart

Thank you, I will try that.

I appreciate the very quick response.

🚀 1

That did it! Just switched projects again, and didn't get the prompt. 👏

💙 1