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
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
yes
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 checkso we really need to fix it in clj-kondo because of ambiguity
yes
nope
I don't know man. Don't you have AI tools for this? ;P
๐ yeah, I can try that
found it
(ECA found it he)
https://github.com/clj-kondo/clj-kondo/blob/dd45054e3eb8d9aa76a7da0e42826144dbd957c6/src/clj_kondo/impl/analysis/java.clj#L229 returns with the .
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
reviewplease https://github.com/clj-kondo/clj-kondo/pull/2657
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
maybe we should be using this? https://asm.ow2.io/javadoc/org/objectweb/asm/ClassVisitor.html#visitInnerClass(java.lang.String,java.lang.String,java.lang.String,int)
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 yetASM seems to output as $ already
I'd say it was just javaparser bad choice
ok
good work
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)))indeed :)
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$Builderit 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
thanks
managed to create a test, the issue is when the .class is inside a jar, the .class alone works ๐คฏ
Hmmhmhm
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.classetc
perhaps we can just ignore clojure/ + $
I guess
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.classI mean _QMARK_
yeah
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?
yeah
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
reviewplease https://github.com/clj-kondo/clj-kondo/pull/2658
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
@borkdude is anything missing from me https://github.com/clj-kondo/clj-kondo/pull/2658?
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 nthor 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
pushed!
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 idea?
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-memberslet 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 moreyou 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)
please!
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 ;)
merged
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!
nice!
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.28Interesting, 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
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 mergedso 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 wayproject-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?
yes
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
ah ok
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
there you go https://github.com/rafaeldelboni/squint-vite-react/blob/main/.lsp/config.edn https://github.com/rafaeldelboni/squint-vite-react/blob/main/squint.edn this works
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
@rafaeldelboni would you mind create a clojure-lsp issue for that please? to include squint.edn as a project-spec
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?
ah ok
I may do a release after me and borkdude fix https://clojurians.slack.com/archives/CPABC1H61/p1763467383675949
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
I think:
(setq lsp-restart 'ignore)
or auto-restartThank you, I will try that.
I appreciate the very quick response.
That did it! Just switched projects again, and didn't get the prompt. ๐