Skip to content

Commit f5e9b9c

Browse files
committed
Merge branch 'wip-method-values'
2 parents 3beb5f5 + 2dd2977 commit f5e9b9c

16 files changed

Lines changed: 793 additions & 117 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ Changelog
22
========================================
33
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.
44
- - -
5+
* Release 1.4.0 on TODO
6+
* Added support for Clojure 1.12 qualified methods (Class/.method, Class/method, Class/new)
7+
* Added :method-value AST node for method values in value position
8+
* Added :param-tags support for overload disambiguation in method values and host calls
59
* Release 1.3.3 on 5 Jan 2026
610
* Bumped parent pom and dep versions
711
* Release 1.3.2 on 17 Jan 2025

build.clj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(ns build
2+
(:require
3+
[clojure.tools.build.api :as b]))
4+
5+
(def basis
6+
(b/create-basis {:project "deps.edn"}))
7+
8+
(defn compile-test-java [_]
9+
(b/javac {:src-dirs ["src/test/java"]
10+
:class-dir "target/test-classes"
11+
:basis basis}))

deps.edn

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{:deps {org.clojure/clojure {:mvn/version "1.12.4"}
2-
org.clojure/tools.analyzer {:mvn/version "1.2.1"}
2+
org.clojure/tools.analyzer {:mvn/version "1.2.2"}
33
org.clojure/tools.reader {:mvn/version "1.6.0"}
44
org.clojure/core.memoize {:mvn/version "1.2.273"}
55
org.ow2.asm/asm {:mvn/version "9.9.1"}}
6-
:paths ["src/main/clojure"]}
6+
:paths ["src/main/clojure" "src/test/clojure" "target/test-classes"]
7+
:aliases {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.6"}}
8+
:ns-default build}}}

pom.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<dependency>
2828
<groupId>org.clojure</groupId>
2929
<artifactId>tools.analyzer</artifactId>
30-
<version>1.2.1</version>
30+
<version>1.2.2</version>
3131
</dependency>
3232
<dependency>
3333
<groupId>org.clojure</groupId>
@@ -46,6 +46,10 @@
4646
</dependency>
4747
</dependencies>
4848

49+
<build>
50+
<testSourceDirectory>src/test/java</testSourceDirectory>
51+
</build>
52+
4953
<scm>
5054
<connection>scm:git:git://github.com/clojure/tools.analyzer.jvm.git</connection>
5155
<developerConnection>scm:git:git://github.com/clojure/tools.analyzer.jvm.git</developerConnection>

spec/ast-ref.edn

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,9 @@
176176
^:optional
177177
[:validated? "`true` if the method call could be resolved at compile time"]
178178
^:optional
179-
[:class "If :validated? the class or interface the method belongs to"]]}
179+
[:class "If :validated? the class or interface the method belongs to"]
180+
^:optional
181+
[:param-tags "A vector of type hints for overload disambiguation, from `^[Type ...]` metadata on the invocation form"]]}
180182
{:op :instance-field
181183
:doc "Node for an instance field access"
182184
:keys [[:form "`(.-field instance)`"]
@@ -266,6 +268,19 @@
266268
[:fixed-arity "The number of args this method takes"]
267269
^:children
268270
[:body "Synthetic :do node (with :body? `true`) representing the body of this method"]]}
271+
{:op :method-value
272+
:doc "Node for a qualified method reference in value position (Clojure 1.12+)"
273+
:keys [[:form "The original qualified method symbol, e.g. `String/valueOf`, `File/.getName`, `File/new`"]
274+
[:class "The resolved Class the method belongs to"]
275+
[:method "Symbol naming the method"]
276+
[:kind "One of :static, :instance, or :ctor"]
277+
^:optional
278+
[:param-tags "A vector of type hints for overload disambiguation, from `^[Type ...]` metadata"]
279+
[:methods "A vector of matching method/constructor reflective info maps"]
280+
^:optional
281+
[:field-overload "When :kind is :static and a static field of the same name exists, the field info map"]
282+
^:optional
283+
[:validated? "`true` if the method value could be resolved at compile time"]]}
269284
{:op :monitor-enter
270285
:doc "Node for a monitor-enter special-form statement"
271286
:keys [[:form "`(monitor-enter target)`"]
@@ -349,7 +364,9 @@
349364
^:children
350365
[:args "A vector of AST nodes representing the args to the method call"]
351366
^:optional
352-
[:validated? "`true` if the static method could be resolved at compile time"]]}
367+
[:validated? "`true` if the static method could be resolved at compile time"]
368+
^:optional
369+
[:param-tags "A vector of type hints for overload disambiguation, from `^[Type ...]` metadata on the invocation form"]]}
353370
{:op :static-field
354371
:doc "Node for a static field access"
355372
:keys [[:form "`Class/field`"]

src/main/clojure/clojure/tools/analyzer/jvm.clj

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
[box :refer [box]]
3535
[constant-lifter :refer [constant-lift]]
3636
[classify-invoke :refer [classify-invoke]]
37+
[process-method-value :refer [process-method-value]]
3738
[validate :refer [validate]]
3839
[infer-tag :refer [infer-tag]]
3940
[validate-loop-locals :refer [validate-loop-locals]]
@@ -90,8 +91,7 @@
9091
#'clojure.core/when-not
9192
#'clojure.core/while
9293
#'clojure.core/with-open
93-
#'clojure.core/with-out-str
94-
})
94+
#'clojure.core/with-out-str})
9595

9696
(def specials
9797
"Set of the special forms for clojure in the JVM"
@@ -127,13 +127,31 @@
127127
(let [sym-ns (namespace form)]
128128
(if-let [target (and sym-ns
129129
(not (resolve-ns (symbol sym-ns) env))
130-
(maybe-class-literal sym-ns))] ;; Class/field
131-
(let [opname (name form)]
132-
(if (and (= (count opname) 1)
133-
(Character/isDigit (char (first opname))))
134-
form ;; Array/<n>
135-
(with-meta (list '. target (symbol (str "-" opname))) ;; transform to (. Class -field)
136-
(meta form))))
130+
(maybe-class-literal sym-ns))]
131+
(let [opname (name form)
132+
opsym (symbol opname)]
133+
(cond
134+
;; Array/<n>, leave as is
135+
(and (= (count opname) 1)
136+
(Character/isDigit (char (first opname))))
137+
form
138+
139+
;; Class/.method or Class/new, leave as is to be parsed as :maybe-host-form -> :method-value
140+
(or (.startsWith ^String opname ".")
141+
(= "new" opname))
142+
form
143+
144+
;; Class/name where name is a static field, desugar to (. Class -name) as before
145+
;; But if :param-tags are present and methods with the same name exist, then leave as is to go through
146+
;; :method-value path
147+
(static-field target opsym)
148+
(if (and (param-tags-of form)
149+
(seq (filter :return-type (static-members target opsym))))
150+
form
151+
(with-meta (list '. target (symbol (str "-" opname)))
152+
(meta form)))
153+
154+
:else form))
137155
form)))
138156

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

148-
(let [op (symbol opname)]
149-
(with-meta (list '. target (if (zero? (count expr))
150-
op
151-
(list* op expr)))
152-
(meta form)))
166+
(cond
167+
;; (Class/new args), (Class/.method target args), (^[pt] Class/method args)
168+
;; -> leave as-is, will be analyzed as invoke of method-value
169+
(or (= "new" opname)
170+
(.startsWith ^String opname ".")
171+
(param-tags-of op))
172+
form
173+
174+
;; (Class/method args) -> (. Class (method args))
175+
:else
176+
(let [op-sym (symbol opname)]
177+
(with-meta (list '. target (if (seq expr)
178+
(list* op-sym expr)
179+
op-sym))
180+
(meta form))))
153181

154182
(cond
155183
(.startsWith opname ".") ; (.foo bar ..)
@@ -456,6 +484,7 @@
456484
#'box
457485

458486
#'analyze-host-expr
487+
#'process-method-value
459488
#'validate-loop-locals
460489
#'validate
461490
#'infer-tag

src/main/clojure/clojure/tools/analyzer/jvm/utils.clj

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,44 @@
390390
(conj p next)))) [] methods)
391391
methods)))
392392

393+
(defn param-tags-of [sym]
394+
(-> sym meta :param-tags))
395+
396+
(defn- tags-to-maybe-classes
397+
[tags]
398+
(mapv (fn [tag]
399+
(when-not (= '_ tag)
400+
(maybe-class tag)))
401+
tags))
402+
403+
(defn- signature-matches?
404+
[param-classes method]
405+
(let [method-params (:parameter-types method)]
406+
(and (= (count param-classes) (count method-params))
407+
(every? (fn [[pc mp]]
408+
(or (nil? pc) ;; nil is a wildcard
409+
(= pc (maybe-class mp))))
410+
(map vector param-classes method-params)))))
411+
412+
(defn- most-specific
413+
[methods]
414+
(map (fn [ms]
415+
(reduce (fn [a b]
416+
(if (.isAssignableFrom (maybe-class (:declaring-class a))
417+
(maybe-class (:declaring-class b)))
418+
b a))
419+
ms))
420+
(vals (group-by #(mapv maybe-class (:parameter-types %)) methods))))
421+
422+
(defn resolve-hinted-method
423+
"Given a class, method name and param-tags, resolves to the unique matching method.
424+
Returns nil if no match or if ambiguous."
425+
[methods param-tags]
426+
(let [param-classes (tags-to-maybe-classes param-tags)
427+
matching (most-specific (filter #(signature-matches? param-classes %) methods))]
428+
(when (= 1 (count matching))
429+
(first matching))))
430+
393431
(defn ns->relpath [s]
394432
(-> s str (s/replace \. \/) (s/replace \- \_) (str ".clj")))
395433

src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
(ns clojure.tools.analyzer.passes.jvm.analyze-host-expr
1010
(:require [clojure.tools.analyzer :as ana]
1111
[clojure.tools.analyzer.utils :refer [ctx source-info merge']]
12-
[clojure.tools.analyzer.jvm.utils :refer :all]))
12+
[clojure.tools.analyzer.jvm.utils :refer :all])
13+
(:import (clojure.lang AFunction)))
1314

1415
(defn maybe-static-field [[_ class sym]]
1516
(when-let [{:keys [flags type name]} (static-field class sym)]
@@ -142,8 +143,9 @@
142143
(defn analyze-host-expr
143144
"Performing some reflection, transforms :host-interop/:host-call/:host-field
144145
nodes in either: :static-field, :static-call, :instance-call, :instance-field
145-
or :host-interop nodes, and a :var/:maybe-class/:maybe-host-form node in a
146-
:const :class node, if necessary (class literals shadow Vars).
146+
or :host-interop nodes, a :var/:maybe-class/:maybe-host-form node in a
147+
:const :class node if necessary (class literals shadow Vars), and a
148+
:maybe-host-form node in a :method-value node for qualified methods.
147149
148150
A :host-interop node represents either an instance-field or a no-arg instance-method. "
149151
{:pass-info {:walk :post :depends #{}}}
@@ -190,9 +192,47 @@
190192
ast)
191193

192194
:maybe-host-form
193-
(if-let [the-class (maybe-array-class-sym (symbol (str (:class ast))
194-
(str (:field ast))))]
195-
(assoc (ana/analyze-const the-class env :class) :form form)
196-
ast)
197-
195+
(let [class-sym (:class ast)
196+
field-sym (:field ast)
197+
field-name (name field-sym)]
198+
(if-let [array-class (maybe-array-class-sym (symbol (str class-sym) field-name))]
199+
(assoc (ana/analyze-const array-class env :class) :form form)
200+
(if-let [the-class (maybe-class-literal class-sym)]
201+
(let [param-tags (param-tags-of form)
202+
kind (cond (.startsWith field-name ".") :instance
203+
(= "new" field-name) :ctor
204+
:else :static)
205+
method-name (if (= :instance kind)
206+
(symbol (subs field-name 1))
207+
field-sym)
208+
methods (case kind
209+
:ctor
210+
(members the-class (symbol (.getName ^Class the-class)))
211+
212+
:static
213+
(filter :return-type (static-members the-class method-name))
214+
215+
:instance
216+
(filter :return-type (instance-members the-class method-name)))
217+
field-info (when (= :static kind)
218+
(static-field the-class method-name))]
219+
;; field info but no methods shouldn't be possible, as we'd have desugared
220+
;; to a field syntax directly
221+
(assert (if field-info methods true))
222+
(if (seq methods)
223+
(merge
224+
{:op :method-value
225+
:form form
226+
:env env
227+
:class the-class
228+
:method method-name
229+
:kind kind
230+
:param-tags param-tags
231+
:methods (vec methods)
232+
:o-tag AFunction
233+
:tag (or tag AFunction)}
234+
(when field-info
235+
{:field-overload field-info}))
236+
ast))
237+
ast)))
198238
ast))

src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,38 @@
113113
tests thens))
114114
~switch-type ~test-type ~skip-check?))
115115

116+
(defmethod -emit-form :new
117+
[{:keys [class args param-tags]} opts]
118+
(if param-tags
119+
(let [sym (symbol (class->str (:val class)) "new")
120+
sym (vary-meta sym assoc :param-tags param-tags)]
121+
`(~sym ~@(mapv #(-emit-form* % opts) args)))
122+
`(new ~(-emit-form* class opts) ~@(mapv #(-emit-form* % opts) args))))
123+
116124
(defmethod -emit-form :static-field
117-
[{:keys [class field]} opts]
118-
(symbol (class->str class) (name field)))
125+
[{:keys [class field overloaded-field?]} opts]
126+
(if overloaded-field?
127+
`(. ~(class->sym class) ~(symbol (str "-" (name field))))
128+
(list (symbol (class->str class) (name field)))))
119129

120130
(defmethod -emit-form :static-call
121-
[{:keys [class method args]} opts]
122-
`(~(symbol (class->str class) (name method))
123-
~@(mapv #(-emit-form* % opts) args)))
131+
[{:keys [class method args param-tags]} opts]
132+
(let [sym (symbol (class->str class) (name method))
133+
sym (if param-tags (vary-meta sym assoc :param-tags param-tags) sym)]
134+
`(~sym ~@(mapv #(-emit-form* % opts) args))))
124135

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

129140
(defmethod -emit-form :instance-call
130-
[{:keys [instance method args]} opts]
131-
`(~(symbol (str "." (name method))) ~(-emit-form* instance opts)
132-
~@(mapv #(-emit-form* % opts) args)))
141+
[{:keys [instance method args class param-tags]} opts]
142+
(if param-tags
143+
(let [sym (symbol (class->str class) (str "." (name method)))
144+
sym (vary-meta sym assoc :param-tags param-tags)]
145+
`(~sym ~(-emit-form* instance opts) ~@(mapv #(-emit-form* % opts) args)))
146+
`(~(symbol (str "." (name method))) ~(-emit-form* instance opts)
147+
~@(mapv #(-emit-form* % opts) args))))
133148

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

165+
(defmethod -emit-form :method-value
166+
[{:keys [class method kind param-tags]} opts]
167+
(let [class-name (if (symbol? class) (name class) (.getName ^Class class))
168+
sym (case kind
169+
:static (symbol class-name (str method))
170+
:instance (symbol class-name (str "." method))
171+
:ctor (symbol class-name "new"))]
172+
(if param-tags
173+
(vary-meta sym assoc :param-tags param-tags)
174+
sym)))
175+
150176
(defmethod -emit-form :instance?
151177
[{:keys [class target]} opts]
152178
`(instance? ~class ~(-emit-form* target opts)))

src/main/clojure/clojure/tools/analyzer/passes/jvm/infer_tag.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
[annotate-tag :refer [annotate-tag]]
1717
[annotate-host-info :refer [annotate-host-info]]
1818
[analyze-host-expr :refer [analyze-host-expr]]
19-
[fix-case-test :refer [fix-case-test]]]))
19+
[fix-case-test :refer [fix-case-test]]
20+
[process-method-value :refer [process-method-value]]]))
2021

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

0 commit comments

Comments
 (0)