Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a292583
chore: add tests to paths
bronsa Feb 13, 2026
64bdc2f
desugar 1.12 qualified methods
bronsa Feb 13, 2026
891de29
util
bronsa Feb 13, 2026
bc55506
wip: propagate param-tags
bronsa Feb 13, 2026
709a603
analyze :maybe-host-form into new :method-value
bronsa Feb 13, 2026
cfb1f54
emit-form for method-value
bronsa Feb 13, 2026
596544e
feat: resolve-hinted-method
bronsa Feb 13, 2026
45d77e3
feat: handle validation of unresolveable method values
bronsa Feb 13, 2026
548ce69
feat: validate method values
bronsa Feb 13, 2026
9b5afc2
fix
bronsa Feb 13, 2026
548e331
feat: validate-call prefer param-tags
bronsa Feb 13, 2026
5ba49b2
chore: update ast-ref
bronsa Feb 13, 2026
675ac2c
fix: avoid clashes with x deftype
bronsa Feb 13, 2026
ade71c3
some tests
bronsa Feb 13, 2026
a87c462
chore: changelog
bronsa Feb 13, 2026
55d0293
fix for pre 1.12
bronsa Feb 13, 2026
cd839da
chore
bronsa Feb 13, 2026
d61a85c
don't try to fit it with `.`, just keep as invoke of method-values fo…
bronsa Feb 13, 2026
cfbc4b3
feat: emit-form preserves param-tags if needed
bronsa Feb 13, 2026
a9e4c8c
update tests
bronsa Feb 13, 2026
b8f5f17
chore: update docstring
bronsa Feb 13, 2026
45eaa61
feat: add process-method-value
bronsa Feb 13, 2026
9a8f0da
feat: infer-tag depend on process-method-value
bronsa Feb 13, 2026
40b7216
dead code
bronsa Feb 13, 2026
c472415
style
bronsa Feb 13, 2026
263a41d
feat: if field-overload, convert to method-value
bronsa Feb 13, 2026
2e3db85
stricter validate
bronsa Feb 13, 2026
84bc2a4
fix test
bronsa Feb 13, 2026
2e1723d
more tests
bronsa Feb 13, 2026
11ff2fc
fix: most-specific one
bronsa Feb 14, 2026
7a9260d
more tests
bronsa Feb 14, 2026
74c4af3
local helper
bronsa Feb 14, 2026
6d72063
wip
bronsa Feb 15, 2026
5bea2a1
fix: defensive wrapping for static-field
bronsa Feb 15, 2026
414d084
fix: defensive emission for static-field
bronsa Feb 15, 2026
f3c10ec
bump t.a
bronsa Feb 15, 2026
2dd2977
enable test
bronsa Feb 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ Changelog
========================================
Since tools.analyzer.jvm version are usually cut simultaneously with a tools.analyzer version, check also the tools.analyzer [CHANGELOG](https://github.com/clojure/tools.analyzer/blob/master/CHANGELOG.md) for changes on the corresponding version, since changes in that library will reflect on this one.
- - -
* Release 1.4.0 on TODO
* Added support for Clojure 1.12 qualified methods (Class/.method, Class/method, Class/new)
* Added :method-value AST node for method values in value position
* Added :param-tags support for overload disambiguation in method values and host calls
* Release 1.3.3 on 5 Jan 2026
* Bumped parent pom and dep versions
* Release 1.3.2 on 17 Jan 2025
Expand Down
11 changes: 11 additions & 0 deletions build.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
(ns build
(:require
[clojure.tools.build.api :as b]))

(def basis
(b/create-basis {:project "deps.edn"}))

(defn compile-test-java [_]
(b/javac {:src-dirs ["src/test/java"]
:class-dir "target/test-classes"
:basis basis}))
6 changes: 4 additions & 2 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{:deps {org.clojure/clojure {:mvn/version "1.12.4"}
org.clojure/tools.analyzer {:mvn/version "1.2.1"}
org.clojure/tools.analyzer {:mvn/version "1.2.2"}
org.clojure/tools.reader {:mvn/version "1.6.0"}
org.clojure/core.memoize {:mvn/version "1.2.273"}
org.ow2.asm/asm {:mvn/version "9.9.1"}}
:paths ["src/main/clojure"]}
:paths ["src/main/clojure" "src/test/clojure" "target/test-classes"]
:aliases {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.6"}}
:ns-default build}}}
6 changes: 5 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<dependency>
<groupId>org.clojure</groupId>
<artifactId>tools.analyzer</artifactId>
<version>1.2.1</version>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.clojure</groupId>
Expand All @@ -46,6 +46,10 @@
</dependency>
</dependencies>

<build>
<testSourceDirectory>src/test/java</testSourceDirectory>
</build>

<scm>
<connection>scm:git:git://github.com/clojure/tools.analyzer.jvm.git</connection>
<developerConnection>scm:git:git://github.com/clojure/tools.analyzer.jvm.git</developerConnection>
Expand Down
21 changes: 19 additions & 2 deletions spec/ast-ref.edn
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@
^:optional
[:validated? "`true` if the method call could be resolved at compile time"]
^:optional
[:class "If :validated? the class or interface the method belongs to"]]}
[:class "If :validated? the class or interface the method belongs to"]
^:optional
[:param-tags "A vector of type hints for overload disambiguation, from `^[Type ...]` metadata on the invocation form"]]}
{:op :instance-field
:doc "Node for an instance field access"
:keys [[:form "`(.-field instance)`"]
Expand Down Expand Up @@ -266,6 +268,19 @@
[:fixed-arity "The number of args this method takes"]
^:children
[:body "Synthetic :do node (with :body? `true`) representing the body of this method"]]}
{:op :method-value
:doc "Node for a qualified method reference in value position (Clojure 1.12+)"
:keys [[:form "The original qualified method symbol, e.g. `String/valueOf`, `File/.getName`, `File/new`"]
[:class "The resolved Class the method belongs to"]
[:method "Symbol naming the method"]
[:kind "One of :static, :instance, or :ctor"]
^:optional
[:param-tags "A vector of type hints for overload disambiguation, from `^[Type ...]` metadata"]
[:methods "A vector of matching method/constructor reflective info maps"]
^:optional
[:field-overload "When :kind is :static and a static field of the same name exists, the field info map"]
^:optional
[:validated? "`true` if the method value could be resolved at compile time"]]}
{:op :monitor-enter
:doc "Node for a monitor-enter special-form statement"
:keys [[:form "`(monitor-enter target)`"]
Expand Down Expand Up @@ -349,7 +364,9 @@
^:children
[:args "A vector of AST nodes representing the args to the method call"]
^:optional
[:validated? "`true` if the static method could be resolved at compile time"]]}
[:validated? "`true` if the static method could be resolved at compile time"]
^:optional
[:param-tags "A vector of type hints for overload disambiguation, from `^[Type ...]` metadata on the invocation form"]]}
{:op :static-field
:doc "Node for a static field access"
:keys [[:form "`Class/field`"]
Expand Down
59 changes: 44 additions & 15 deletions src/main/clojure/clojure/tools/analyzer/jvm.clj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
[box :refer [box]]
[constant-lifter :refer [constant-lift]]
[classify-invoke :refer [classify-invoke]]
[process-method-value :refer [process-method-value]]
[validate :refer [validate]]
[infer-tag :refer [infer-tag]]
[validate-loop-locals :refer [validate-loop-locals]]
Expand Down Expand Up @@ -90,8 +91,7 @@
#'clojure.core/when-not
#'clojure.core/while
#'clojure.core/with-open
#'clojure.core/with-out-str
})
#'clojure.core/with-out-str})

(def specials
"Set of the special forms for clojure in the JVM"
Expand Down Expand Up @@ -127,13 +127,31 @@
(let [sym-ns (namespace form)]
(if-let [target (and sym-ns
(not (resolve-ns (symbol sym-ns) env))
(maybe-class-literal sym-ns))] ;; Class/field
(let [opname (name form)]
(if (and (= (count opname) 1)
(Character/isDigit (char (first opname))))
form ;; Array/<n>
(with-meta (list '. target (symbol (str "-" opname))) ;; transform to (. Class -field)
(meta form))))
(maybe-class-literal sym-ns))]
(let [opname (name form)
opsym (symbol opname)]
(cond
;; Array/<n>, leave as is
(and (= (count opname) 1)
(Character/isDigit (char (first opname))))
form

;; Class/.method or Class/new, leave as is to be parsed as :maybe-host-form -> :method-value
(or (.startsWith ^String opname ".")
(= "new" opname))
form

;; Class/name where name is a static field, desugar to (. Class -name) as before
;; But if :param-tags are present and methods with the same name exist, then leave as is to go through
;; :method-value path
(static-field target opsym)
(if (and (param-tags-of form)
(seq (filter :return-type (static-members target opsym))))
form
(with-meta (list '. target (symbol (str "-" opname)))
(meta form)))

:else form))
form)))

(defn desugar-host-expr [form env]
Expand All @@ -143,13 +161,23 @@
opns (namespace op)]
(if-let [target (and opns
(not (resolve-ns (symbol opns) env))
(maybe-class-literal opns))] ; (class/field ..)
(maybe-class-literal opns))]

(let [op (symbol opname)]
(with-meta (list '. target (if (zero? (count expr))
op
(list* op expr)))
(meta form)))
(cond
;; (Class/new args), (Class/.method target args), (^[pt] Class/method args)
;; -> leave as-is, will be analyzed as invoke of method-value
(or (= "new" opname)
(.startsWith ^String opname ".")
(param-tags-of op))
form

;; (Class/method args) -> (. Class (method args))
:else
(let [op-sym (symbol opname)]
(with-meta (list '. target (if (seq expr)
(list* op-sym expr)
op-sym))
(meta form))))

(cond
(.startsWith opname ".") ; (.foo bar ..)
Expand Down Expand Up @@ -456,6 +484,7 @@
#'box

#'analyze-host-expr
#'process-method-value
#'validate-loop-locals
#'validate
#'infer-tag
Expand Down
38 changes: 38 additions & 0 deletions src/main/clojure/clojure/tools/analyzer/jvm/utils.clj
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,44 @@
(conj p next)))) [] methods)
methods)))

(defn param-tags-of [sym]
(-> sym meta :param-tags))

(defn- tags-to-maybe-classes
[tags]
(mapv (fn [tag]
(when-not (= '_ tag)
(maybe-class tag)))
tags))

(defn- signature-matches?
[param-classes method]
(let [method-params (:parameter-types method)]
(and (= (count param-classes) (count method-params))
(every? (fn [[pc mp]]
(or (nil? pc) ;; nil is a wildcard
(= pc (maybe-class mp))))
(map vector param-classes method-params)))))

(defn- most-specific
[methods]
(map (fn [ms]
(reduce (fn [a b]
(if (.isAssignableFrom (maybe-class (:declaring-class a))
(maybe-class (:declaring-class b)))
b a))
ms))
(vals (group-by #(mapv maybe-class (:parameter-types %)) methods))))

(defn resolve-hinted-method
"Given a class, method name and param-tags, resolves to the unique matching method.
Returns nil if no match or if ambiguous."
[methods param-tags]
(let [param-classes (tags-to-maybe-classes param-tags)
matching (most-specific (filter #(signature-matches? param-classes %) methods))]
(when (= 1 (count matching))
(first matching))))

(defn ns->relpath [s]
(-> s str (s/replace \. \/) (s/replace \- \_) (str ".clj")))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
(ns clojure.tools.analyzer.passes.jvm.analyze-host-expr
(:require [clojure.tools.analyzer :as ana]
[clojure.tools.analyzer.utils :refer [ctx source-info merge']]
[clojure.tools.analyzer.jvm.utils :refer :all]))
[clojure.tools.analyzer.jvm.utils :refer :all])
(:import (clojure.lang AFunction)))

(defn maybe-static-field [[_ class sym]]
(when-let [{:keys [flags type name]} (static-field class sym)]
Expand Down Expand Up @@ -142,8 +143,9 @@
(defn analyze-host-expr
"Performing some reflection, transforms :host-interop/:host-call/:host-field
nodes in either: :static-field, :static-call, :instance-call, :instance-field
or :host-interop nodes, and a :var/:maybe-class/:maybe-host-form node in a
:const :class node, if necessary (class literals shadow Vars).
or :host-interop nodes, a :var/:maybe-class/:maybe-host-form node in a
:const :class node if necessary (class literals shadow Vars), and a
:maybe-host-form node in a :method-value node for qualified methods.

A :host-interop node represents either an instance-field or a no-arg instance-method. "
{:pass-info {:walk :post :depends #{}}}
Expand Down Expand Up @@ -190,9 +192,47 @@
ast)

:maybe-host-form
(if-let [the-class (maybe-array-class-sym (symbol (str (:class ast))
(str (:field ast))))]
(assoc (ana/analyze-const the-class env :class) :form form)
ast)

(let [class-sym (:class ast)
field-sym (:field ast)
field-name (name field-sym)]
(if-let [array-class (maybe-array-class-sym (symbol (str class-sym) field-name))]
(assoc (ana/analyze-const array-class env :class) :form form)
(if-let [the-class (maybe-class-literal class-sym)]
(let [param-tags (param-tags-of form)
kind (cond (.startsWith field-name ".") :instance
(= "new" field-name) :ctor
:else :static)
method-name (if (= :instance kind)
(symbol (subs field-name 1))
field-sym)
methods (case kind
:ctor
(members the-class (symbol (.getName ^Class the-class)))

:static
(filter :return-type (static-members the-class method-name))

:instance
(filter :return-type (instance-members the-class method-name)))
field-info (when (= :static kind)
(static-field the-class method-name))]
;; field info but no methods shouldn't be possible, as we'd have desugared
;; to a field syntax directly
(assert (if field-info methods true))
(if (seq methods)
(merge
{:op :method-value
:form form
:env env
:class the-class
:method method-name
:kind kind
:param-tags param-tags
:methods (vec methods)
:o-tag AFunction
:tag (or tag AFunction)}
(when field-info
{:field-overload field-info}))
ast))
ast)))
ast))
42 changes: 34 additions & 8 deletions src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj
Original file line number Diff line number Diff line change
Expand Up @@ -113,23 +113,38 @@
tests thens))
~switch-type ~test-type ~skip-check?))

(defmethod -emit-form :new
[{:keys [class args param-tags]} opts]
(if param-tags
(let [sym (symbol (class->str (:val class)) "new")
sym (vary-meta sym assoc :param-tags param-tags)]
`(~sym ~@(mapv #(-emit-form* % opts) args)))
`(new ~(-emit-form* class opts) ~@(mapv #(-emit-form* % opts) args))))

(defmethod -emit-form :static-field
[{:keys [class field]} opts]
(symbol (class->str class) (name field)))
[{:keys [class field overloaded-field?]} opts]
(if overloaded-field?
`(. ~(class->sym class) ~(symbol (str "-" (name field))))
(list (symbol (class->str class) (name field)))))

(defmethod -emit-form :static-call
[{:keys [class method args]} opts]
`(~(symbol (class->str class) (name method))
~@(mapv #(-emit-form* % opts) args)))
[{:keys [class method args param-tags]} opts]
(let [sym (symbol (class->str class) (name method))
sym (if param-tags (vary-meta sym assoc :param-tags param-tags) sym)]
`(~sym ~@(mapv #(-emit-form* % opts) args))))

(defmethod -emit-form :instance-field
[{:keys [instance field]} opts]
`(~(symbol (str ".-" (name field))) ~(-emit-form* instance opts)))

(defmethod -emit-form :instance-call
[{:keys [instance method args]} opts]
`(~(symbol (str "." (name method))) ~(-emit-form* instance opts)
~@(mapv #(-emit-form* % opts) args)))
[{:keys [instance method args class param-tags]} opts]
(if param-tags
(let [sym (symbol (class->str class) (str "." (name method)))
sym (vary-meta sym assoc :param-tags param-tags)]
`(~sym ~(-emit-form* instance opts) ~@(mapv #(-emit-form* % opts) args)))
`(~(symbol (str "." (name method))) ~(-emit-form* instance opts)
~@(mapv #(-emit-form* % opts) args))))

(defmethod -emit-form :prim-invoke
[{:keys [fn args]} opts]
Expand All @@ -147,6 +162,17 @@
(list (-emit-form* keyword opts)
(-emit-form* target opts)))

(defmethod -emit-form :method-value
[{:keys [class method kind param-tags]} opts]
(let [class-name (if (symbol? class) (name class) (.getName ^Class class))
sym (case kind
:static (symbol class-name (str method))
:instance (symbol class-name (str "." method))
:ctor (symbol class-name "new"))]
(if param-tags
(vary-meta sym assoc :param-tags param-tags)
sym)))

(defmethod -emit-form :instance?
[{:keys [class target]} opts]
`(instance? ~class ~(-emit-form* target opts)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
[annotate-tag :refer [annotate-tag]]
[annotate-host-info :refer [annotate-host-info]]
[analyze-host-expr :refer [analyze-host-expr]]
[fix-case-test :refer [fix-case-test]]]))
[fix-case-test :refer [fix-case-test]]
[process-method-value :refer [process-method-value]]]))

(defmulti -infer-tag :op)
(defmethod -infer-tag :default [ast] ast)
Expand Down Expand Up @@ -269,7 +270,7 @@
Passes opts:
* :infer-tag/level If :global, infer-tag will perform Var tag
inference"
{:pass-info {:walk :post :depends #{#'annotate-tag #'annotate-host-info #'fix-case-test #'analyze-host-expr} :after #{#'trim}}}
{:pass-info {:walk :post :depends #{#'annotate-tag #'annotate-host-info #'fix-case-test #'analyze-host-expr #'process-method-value} :after #{#'trim}}}
[{:keys [tag form] :as ast}]
(let [tag (or tag (:tag (meta form)))
ast (-infer-tag ast)]
Expand Down
Loading