From a292583ed083f8e138755f95b8d0291fe084a84e Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 10:43:37 +0000 Subject: [PATCH 01/37] chore: add tests to paths --- deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.edn b/deps.edn index 1c88934..48d8474 100644 --- a/deps.edn +++ b/deps.edn @@ -3,4 +3,4 @@ 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"]} From 64bdc2ffde1c749823b8da57b7c61cc92f10f76f Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:00:54 +0000 Subject: [PATCH 02/37] desugar 1.12 qualified methods --- .../clojure/clojure/tools/analyzer/jvm.clj | 77 ++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm.clj b/src/main/clojure/clojure/tools/analyzer/jvm.clj index a63eb78..41f59b9 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm.clj @@ -90,8 +90,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" @@ -127,13 +126,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/ - (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/, 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 (meta 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] @@ -143,25 +160,49 @@ opns (namespace op)] (if-let [target (and opns (not (resolve-ns (symbol opns) env)) - (maybe-class-literal opns))] ; (class/field ..) - - (let [op (symbol opname)] - (with-meta (list '. target (if (zero? (count expr)) - op - (list* op expr))) - (meta form))) + (maybe-class-literal opns))] + + (let [param-tags (:param-tags (meta op)) + form-meta (cond-> (meta form) + param-tags (assoc :param-tags param-tags))] + (cond + ;; (Class/new args) -> (new Class args) + (= "new" opname) + (with-meta (list* 'new target expr) + form-meta) + + ;; (Class/.method target args) -> (. ^Class (do target) (method rest-args)) + (.startsWith ^String opname ".") + (if (seq expr) + (let [method-sym (symbol (subs opname 1)) + [target-arg & args] expr] + (with-meta (list '. (with-meta (list 'do target-arg) + {:tag target}) + (if (seq args) + (list* method-sym args) + method-sym)) + form-meta)) + 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)) + form-meta)))) (cond (.startsWith opname ".") ; (.foo bar ..) (let [[target & args] expr target (if-let [target (maybe-class-literal target)] (with-meta (list 'do target) - {:tag 'java.lang.Class}) + {:tag 'java.lang.Class}) target) args (list* (symbol (subs opname 1)) args)] (with-meta (list '. target (if (= 1 (count args)) ;; we don't know if (.foo bar) is (first args) args)) ;; a method call or a field access - (meta form))) + (meta form))) (.endsWith opname ".") ;; (class. ..) (with-meta (list* 'new (symbol (subs opname 0 (dec (count opname)))) expr) From 891de2950823cc57bbad10d2fbf12c7086267e02 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:03:00 +0000 Subject: [PATCH 03/37] util --- src/main/clojure/clojure/tools/analyzer/jvm.clj | 2 +- src/main/clojure/clojure/tools/analyzer/jvm/utils.clj | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm.clj b/src/main/clojure/clojure/tools/analyzer/jvm.clj index 41f59b9..2cfbb71 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm.clj @@ -144,7 +144,7 @@ ;; 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 (meta form)) + (if (and (param-tags-of form) (seq (filter :return-type (static-members target opsym)))) form (with-meta (list '. target (symbol (str "-" opname))) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj b/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj index a159c4e..0dcab5b 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj @@ -390,6 +390,9 @@ (conj p next)))) [] methods) methods))) +(defn param-tags-of [sym] + (-> sym meta :param-tags)) + (defn ns->relpath [s] (-> s str (s/replace \. \/) (s/replace \- \_) (str ".clj"))) From bc55506dad54872a485dacb454389d814f1bc089 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:05:36 +0000 Subject: [PATCH 04/37] wip: propagate param-tags --- .../tools/analyzer/passes/jvm/analyze_host_expr.clj | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj index edc566a..9836709 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj @@ -8,8 +8,9 @@ (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.utils :refer [ctx source-info merge' param-tags-of]] + [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)] @@ -165,8 +166,12 @@ (case op :host-call - (analyze-host-call target-type (:method ast) - (:args ast) target class? env) + (let [result (analyze-host-call target-type (:method ast) + (:args ast) target class? env) + param-tags (param-tags-of form)] + (if param-tags + (assoc result :param-tags param-tags) + result)) :host-field (analyze-host-field target-type (:field ast) From 709a603c54cd90a77fede9d3b0e0203b0016d5c8 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:17:21 +0000 Subject: [PATCH 05/37] analyze :maybe-host-form into new :method-value --- .../analyzer/passes/jvm/analyze_host_expr.clj | 52 ++++++++++++++++--- .../tools/analyzer/passes/jvm/validate.clj | 2 +- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj index 9836709..f063509 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj @@ -8,7 +8,7 @@ (ns clojure.tools.analyzer.passes.jvm.analyze-host-expr (:require [clojure.tools.analyzer :as ana] - [clojure.tools.analyzer.utils :refer [ctx source-info merge' param-tags-of]] + [clojure.tools.analyzer.utils :refer [ctx source-info merge']] [clojure.tools.analyzer.jvm.utils :refer :all]) (:import (clojure.lang AFunction))) @@ -195,9 +195,49 @@ 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 (or (param-tags-of form) + (when (coll? form) + (param-tags-of (first 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)) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj index 0e939a8..fe6eeb4 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj @@ -15,7 +15,7 @@ [infer-tag :refer [infer-tag]] [analyze-host-expr :refer [analyze-host-expr]]] [clojure.tools.analyzer.utils :refer [arglist-for-arity source-info resolve-sym resolve-ns merge']] - [clojure.tools.analyzer.jvm.utils :as u :refer [tag-match? try-best-match]]) + [clojure.tools.analyzer.jvm.utils :as u :refer [tag-match? try-best-match resolve-hinted-method]]) (:import (clojure.lang IFn ExceptionInfo))) (defmulti -validate :op) From cfb1f54fb1579637058ccbc6fbe522cd88c97c1b Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:18:23 +0000 Subject: [PATCH 06/37] emit-form for method-value --- .../clojure/tools/analyzer/passes/jvm/emit_form.clj | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj index 3bacb9b..9b64c3f 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj @@ -147,6 +147,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))) From 596544e2613bf628f04755ea7fe6dbd5a41689e8 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:21:58 +0000 Subject: [PATCH 07/37] feat: resolve-hinted-method --- .../clojure/tools/analyzer/jvm/utils.clj | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj b/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj index 0dcab5b..3b47361 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj @@ -393,6 +393,32 @@ (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 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." + [class method-name param-tags] + (let [param-classes (tags-to-maybe-classes param-tags) + methods (members class method-name) + matching (filter #(signature-matches? param-classes %) methods)] + (when (= 1 (count matching)) + (first matching)))) + (defn ns->relpath [s] (-> s str (s/replace \. \/) (s/replace \- \_) (str ".clj"))) From 45d77e31da905c15b13f6ce1be2000bef23c4fb7 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:24:18 +0000 Subject: [PATCH 08/37] feat: handle validation of unresolveable method values --- .../tools/analyzer/passes/jvm/validate.clj | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj index fe6eeb4..b5d84d4 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj @@ -37,14 +37,22 @@ [{:keys [class field form env] :as ast}] (if-let [handle (-> (env/deref-env) :passes-opts :validate/unresolvable-symbol-handler)] (handle class field ast) - (if (resolve-ns class env) - (throw (ex-info (str "No such var: " class) - (merge {:form form} + (if-let [resolved-class (maybe-class-literal class)] + (throw (ex-info (str "Cannot find method or field " field " for class " + (.getName ^Class resolved-class)) + (merge {:class resolved-class + :field field + :form form} (source-info env)))) - (throw (ex-info (str "No such namespace: " class) - (merge {:ns class - :form form} - (source-info env))))))) + (if (resolve-ns class env) + (throw (ex-info (str "No such var: " class) + (merge {:form form} + (source-info env)))) + (throw (ex-info (str "No such namespace: " class) + (merge {:ns class + :form form} + (source-info env)))))))) + (defmethod -validate :set! [{:keys [target form env] :as ast}] From 548ce697a92eb0d31b3b67f772f14f46d784dbf3 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:27:13 +0000 Subject: [PATCH 09/37] feat: validate method values --- .../clojure/tools/analyzer/jvm/utils.clj | 3 +-- .../tools/analyzer/passes/jvm/validate.clj | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj b/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj index 3b47361..6dd481c 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj @@ -412,9 +412,8 @@ (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." - [class method-name param-tags] + [methods param-tags] (let [param-classes (tags-to-maybe-classes param-tags) - methods (members class method-name) matching (filter #(signature-matches? param-classes %) methods)] (when (= 1 (count matching)) (first matching)))) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj index b5d84d4..04552a8 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj @@ -53,6 +53,25 @@ :form form} (source-info env)))))))) +(defmethod -validate :method-value + [{:keys [class method kind param-tags methods env] :as ast}] + (let [class (u/maybe-class class)] + (if param-tags + (if-let [m (resolve-hinted-method methods param-tags)] + (assoc ast + :class class + :methods [m] + :validated? true) + (throw (ex-info (str "param-tags " (pr-str param-tags) + " insufficient to resolve " (name kind) " method " + method " in class " (.getName ^Class class)) + (merge {:class class + :method method + :param-tags param-tags} + (source-info env))))) + (assoc ast + :class class + :validated? true)))) (defmethod -validate :set! [{:keys [target form env] :as ast}] From 9b5afc2bc5edbf2420305cf11ce0f7f7825730ea Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:36:10 +0000 Subject: [PATCH 10/37] fix --- src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj index 04552a8..c467448 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj @@ -37,7 +37,7 @@ [{:keys [class field form env] :as ast}] (if-let [handle (-> (env/deref-env) :passes-opts :validate/unresolvable-symbol-handler)] (handle class field ast) - (if-let [resolved-class (maybe-class-literal class)] + (if-let [resolved-class (u/maybe-class-literal class)] (throw (ex-info (str "Cannot find method or field " field " for class " (.getName ^Class resolved-class)) (merge {:class resolved-class From 548e331e5cf7d00a0ee0c45beb161440e5527353 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:36:33 +0000 Subject: [PATCH 11/37] feat: validate-call prefer param-tags --- .../tools/analyzer/passes/jvm/validate.clj | 73 ++++++++++--------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj index c467448..df3f911 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj @@ -111,44 +111,51 @@ :args (mapv (fn [a] (prewalk a cleanup)) args)} (source-info (:env ast))))))))))) -(defn validate-call [{:keys [class instance method args tag env op] :as ast}] +(defn- found-method [ast args tag instance? instance m] + (let [ret-tag (:return-type m) + arg-tags (mapv u/maybe-class (:parameter-types m)) + args (mapv (fn [arg tag] (assoc arg :tag tag)) args arg-tags) + class (u/maybe-class (:declaring-class m))] + (merge' ast + {:method (:name m) + :validated? true + :class class + :o-tag ret-tag + :tag (or tag ret-tag) + :args args} + (if instance? + {:instance (assoc instance :tag class)})))) + +(defn validate-call [{:keys [class instance method args tag env op param-tags] :as ast}] (let [argc (count args) instance? (= :instance-call op) f (if instance? u/instance-methods u/static-methods) tags (mapv :tag args)] (if-let [matching-methods (seq (f class method argc))] - (let [[m & rest :as matching] (try-best-match tags matching-methods)] - (if m - (let [all-ret-equals? (apply = (mapv :return-type matching))] - (if (or (empty? rest) - (and all-ret-equals? ;; if the method signature is the same just pick the first one - (apply = (mapv #(mapv u/maybe-class (:parameter-types %)) matching)))) - (let [ret-tag (:return-type m) - arg-tags (mapv u/maybe-class (:parameter-types m)) - args (mapv (fn [arg tag] (assoc arg :tag tag)) args arg-tags) - class (u/maybe-class (:declaring-class m))] - (merge' ast - {:method (:name m) - :validated? true - :class class - :o-tag ret-tag - :tag (or tag ret-tag) - :args args} - (if instance? - {:instance (assoc instance :tag class)}))) - (if all-ret-equals? - (let [ret-tag (:return-type m)] - (assoc ast - :o-tag Object - :tag (or tag ret-tag))) - ast))) - (if instance? - (assoc (dissoc ast :class) :tag Object :o-tag Object) - (throw (ex-info (str "No matching method: " method " for class: " class " and given signature") - (merge {:method method - :class class - :args (mapv (fn [a] (prewalk a cleanup)) args)} - (source-info env))))))) + ;; try resolving via param-tags first + (if-let [hinted-method (and param-tags + (resolve-hinted-method matching-methods param-tags))] + (found-method ast args tag instance? instance hinted-method) + (let [[m & rest :as matching] (try-best-match tags matching-methods)] + (if m + (let [all-ret-equals? (apply = (mapv :return-type matching))] + (if (or (empty? rest) + (and all-ret-equals? ;; if the method signature is the same just pick the first one + (apply = (mapv #(mapv u/maybe-class (:parameter-types %)) matching)))) + (found-method ast args tag instance? instance m) + (if all-ret-equals? + (let [ret-tag (:return-type m)] + (assoc ast + :o-tag Object + :tag (or tag ret-tag))) + ast))) + (if instance? + (assoc (dissoc ast :class) :tag Object :o-tag Object) + (throw (ex-info (str "No matching method: " method " for class: " class " and given signature") + (merge {:method method + :class class + :args (mapv (fn [a] (prewalk a cleanup)) args)} + (source-info env)))))))) (if instance? (assoc (dissoc ast :class) :tag Object :o-tag Object) (throw (ex-info (str "No matching method: " method " for class: " class " and arity: " argc) From 5ba49b2fc55f4e22fdae6b2d2e825e3e66f677b2 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:36:53 +0000 Subject: [PATCH 12/37] chore: update ast-ref --- spec/ast-ref.edn | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/spec/ast-ref.edn b/spec/ast-ref.edn index f267ab1..b084e22 100644 --- a/spec/ast-ref.edn +++ b/spec/ast-ref.edn @@ -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)`"] @@ -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)`"] @@ -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`"] From 675ac2c0bb8fa3ebed543edff208994d58362374 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:39:38 +0000 Subject: [PATCH 13/37] fix: avoid clashes with x deftype --- src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj index a0b88da..cf61ac2 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj @@ -96,9 +96,9 @@ (is (.startsWith (name (:name chunk)) "chunk")) (is (= clojure.lang.IChunk (:tag chunk))))) -(def ^:dynamic x) +(def ^:dynamic *test-dynamic*) (deftest set!-dynamic-var - (is (ast1 (set! x 1)))) + (is (ast1 (set! *test-dynamic* 1)))) (deftest analyze-proxy (is (ast1 (proxy [Object] [])))) From ade71c38f8f9e0cb81c9b22c427e94fcd5a66afc Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 12:15:06 +0000 Subject: [PATCH 14/37] some tests --- .../clojure/tools/analyzer/jvm/core_test.clj | 77 +++++++++- .../tools/analyzer/jvm/passes_test.clj | 137 +++++++++++++++++- 2 files changed, 211 insertions(+), 3 deletions(-) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj index cf61ac2..46010bf 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj @@ -6,7 +6,8 @@ [clojure.tools.analyzer.passes.elide-meta :refer [elides elide-meta]] [clojure.tools.analyzer.ast :refer [postwalk]] [clojure.tools.reader :as r] - [clojure.test :refer [deftest is]])) + [clojure.test :refer [deftest is]]) + (:import (java.io File))) (defprotocol p (f [_])) (defn f1 [^long x]) @@ -115,3 +116,77 @@ (deftest array_class (is (ana (r/read-string "(fn [^{:tag int/2} x] (instance? int/2 x))")))) + +(deftest macroexpander-qualified-methods-test + (is (= (list '. Integer (symbol "-MAX_VALUE")) + (mexpand Integer/MAX_VALUE))) + + (is (= 'String/1 (mexpand String/1))) + + (is (= 'String/.length (mexpand String/.length))) + (is (= 'Integer/.intValue (mexpand Integer/.intValue))) + + (is (= 'String/new (mexpand String/new))) + + (is (= 'String/valueOf (mexpand String/valueOf))) + (is (= 'Integer/parseInt (mexpand Integer/parseInt))) + + (let [expanded (mexpand (String/new "hello"))] + (is (= 'new (first expanded))) + (is (= java.lang.String (second expanded)))) + + (let [expanded (mexpand (String/.substring "hello" 1 3))] + (is (= '. (first expanded))) + (is (= '(do "hello") (second expanded))) + (is (= String (:tag (meta (second expanded))))) + (is (= 'substring (first (nth expanded 2))))) + + (let [expanded (mexpand (String/.length "hello"))] + (is (= '. (first expanded))) + (is (= 'length (nth expanded 2)))) + + (let [expanded (mexpand (Integer/parseInt "2"))] + (is (= '. (first expanded))) + (is (= java.lang.Integer (second expanded))))) + +(deftest analyzer-qualified-methods-test + (let [a (ast1 File/.getName)] + (is (= :method-value (:op a))) + (is (= :instance (:kind a))) + (is (= 'getName (:method a))) + (is (= java.io.File (:class a)))) + + (let [a (ast1 String/valueOf)] + (is (= :method-value (:op a))) + (is (= :static (:kind a))) + (is (= 'valueOf (:method a))) + (is (= String (:class a)))) + + (let [a (ast1 File/new)] + (is (= :method-value (:op a))) + (is (= :ctor (:kind a))) + (is (= java.io.File (:class a)))) + + (let [a (ast1 Integer/MAX_VALUE)] + (is (= :static-field (:op a))) + (is (= Integer (:class a)))) + + (let [a (ana (r/read-string "String/1"))] + (is (= :const (:op a))) + (is (= :class (:type a))) + (is (.isArray ^Class (:val a)))) + + (let [a (ast1 (File/new "."))] + (is (= :new (:op a)))) + + (let [a (ast1 (String/.length "hello"))] + (is (= :instance-call (:op a))) + (is (= 'length (:method a)))) + + (let [a (ast1 (String/.substring "hello" 1 3))] + (is (= :instance-call (:op a))) + (is (= 'substring (:method a)))) + + (let [a (ast1 (Integer/parseInt "7"))] + (is (= :static-call (:op a))) + (is (= 'parseInt (:method a))))) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj index a83d16c..20e25f8 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj @@ -8,7 +8,8 @@ [clojure.set :as set] [clojure.tools.analyzer.passes.add-binding-atom :refer [add-binding-atom]] [clojure.tools.analyzer.passes.collect-closed-overs :refer [collect-closed-overs]] - [clojure.tools.analyzer.jvm.core-test :refer [ast ast1 e f f1]] + [clojure.tools.reader :as r] + [clojure.tools.analyzer.jvm.core-test :refer [ast ast1 ana e f f1]] [clojure.tools.analyzer.passes.jvm.emit-form :refer [emit-form emit-hygienic-form]] [clojure.tools.analyzer.passes.jvm.validate :as v] @@ -22,7 +23,8 @@ [clojure.tools.analyzer.passes.jvm.classify-invoke :refer [classify-invoke]]) (:import (clojure.lang Keyword Var Symbol AFunction PersistentVector PersistentArrayMap PersistentHashSet ISeq) - java.util.regex.Pattern)) + java.util.regex.Pattern + (java.io File))) (defn validate [ast] (env/with-env (ana.jvm/global-env) @@ -161,3 +163,134 @@ {:passes-opts (merge ana.jvm/default-passes-opts {:validate/wrong-tag-handler (fn [t ast] {t nil})})}))) + +(deftest method-value-emit-form-test + (is (= 'java.io.File/.getName (emit-form (ast1 File/.getName)))) + + (is (= 'java.lang.String/valueOf (emit-form (ast1 String/valueOf)))) + + (is (= 'java.io.File/new (emit-form (ast1 File/new)))) + + (let [emitted (emit-form (ana (r/read-string "^[long] String/valueOf")))] + (is (= 'java.lang.String/valueOf emitted)) + (is (= '[long] (:param-tags (meta emitted))))) + + (let [emitted (emit-form (ana (r/read-string "^[int int] String/.substring")))] + (is (= 'java.lang.String/.substring emitted)) + (is (= '[int int] (:param-tags (meta emitted)))))) + +(deftest method-value-validate-test + (let [a (ast1 File/.getName)] + (is (= :method-value (:op a))) + (is (:validated? a)) + (is (= java.io.File (:class a)))) + + (let [a (ast1 String/valueOf)] + (is (= :method-value (:op a))) + (is (:validated? a)) + (is (pos? (count (:methods a))))) + + (let [a (ast1 File/new)] + (is (= :method-value (:op a))) + (is (:validated? a)) + (is (= :ctor (:kind a)))) + + (let [a (ana (r/read-string "^[long] String/valueOf"))] + (is (= :method-value (:op a))) + (is (:validated? a)) + (is (= 1 (count (:methods a))))) + + (let [a (ana (r/read-string "^[int int] String/.substring"))] + (is (= :method-value (:op a))) + (is (:validated? a)) + (is (= 1 (count (:methods a)))))) + +(deftest method-value-kinds-test + (let [a (ast1 File/.isDirectory)] + (is (= :instance (:kind a))) + (is (= 'isDirectory (:method a)))) + + (let [a (ast1 Character/isDigit)] + (is (= :method-value (:op a))) + (is (= :static (:kind a))) + (is (= 'isDigit (:method a))) + (is (< 1 (count (:methods a))))) + + (let [a (ast1 String/new)] + (is (= :ctor (:kind a))) + (is (= String (:class a)))) + + (let [a (ast1 File/.getName)] + (is (= AFunction (:o-tag a))))) + +(deftest method-value-field-overload-test + (let [a (ast1 Integer/MAX_VALUE)] + (is (= :static-field (:op a)))) + + (let [a (ast1 Boolean/TRUE)] + (is (= :static-field (:op a))))) + +(deftest qualified-method-invocation-test + (let [a (ast1 (File/new "."))] + (is (= :new (:op a)))) + + (let [a (ast1 (String/.length "hello"))] + (is (= :instance-call (:op a))) + (is (= 'length (:method a))) + (is (:validated? a))) + + (let [a (ast1 (String/.substring "hello" 1 3))] + (is (= :instance-call (:op a))) + (is (= 'substring (:method a))) + (is (= 2 (count (:args a))))) + + (let [a (ast1 (Integer/parseInt "7"))] + (is (= :static-call (:op a))) + (is (= 'parseInt (:method a))) + (is (:validated? a))) + + (let [a (ast1 (File/.isDirectory (File. ".")))] + (is (= :instance-call (:op a))) + (is (= 'isDirectory (:method a))))) + +(deftest param-tags-invocation-test + (let [a (ana (r/read-string "(^[long] String/valueOf 42)"))] + (is (= :static-call (:op a))) + (is (:validated? a)) + (is (= '[long] (:param-tags a)))) + + (let [a (ana (r/read-string "(^[int int] String/.substring \"hello\" 1 3)"))] + (is (= :instance-call (:op a))) + (is (:validated? a)) + (is (= '[int int] (:param-tags a)))) + + (let [a (ana (r/read-string "(^[int _] String/.substring \"hello\" 1 3)"))] + (is (= :instance-call (:op a))) + (is (:validated? a)) + (is (= '[int _] (:param-tags a)))) + + (let [a (ana (r/read-string "^[int/1] java.util.Arrays/sort"))] + (is (= :method-value (:op a))) + (is (= :static (:kind a))) + (is (= 1 (count (:methods a)))))) + +(deftest existing-interop-unchanged-test + (let [a (ast1 (.length "hello"))] + (is (= :instance-call (:op a))) + (is (:validated? a))) + + (let [a (ast1 (String. "foo"))] + (is (= :new (:op a))) + (is (:validated? a))) + + (is (= Void/TYPE (:tag (ast1 (.println System/out "foo"))))) + + (let [a (ast1 (Integer/parseInt "7"))] + (is (= :static-call (:op a))) + (is (:validated? a))) + + (let [a (ast1 Integer/MAX_VALUE)] + (is (= :static-field (:op a)))) + + (let [a (ast1 Boolean/TYPE)] + (is (= :static-field (:op a))))) From a87c46293c0f114d96f12c215504b9a8b3a268df Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:43:38 +0000 Subject: [PATCH 15/37] chore: changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bee26..bb71ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 55d0293fb50c5121e48d791115be2c3f52fea648 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 11:56:20 +0000 Subject: [PATCH 16/37] fix for pre 1.12 --- src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj index 46010bf..0d90af7 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj @@ -121,8 +121,6 @@ (is (= (list '. Integer (symbol "-MAX_VALUE")) (mexpand Integer/MAX_VALUE))) - (is (= 'String/1 (mexpand String/1))) - (is (= 'String/.length (mexpand String/.length))) (is (= 'Integer/.intValue (mexpand Integer/.intValue))) From cd839dacf2cc2b3786dca7f6c95ea3d4ab8a77ca Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 12:21:05 +0000 Subject: [PATCH 17/37] chore --- src/main/clojure/clojure/tools/analyzer/jvm.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm.clj b/src/main/clojure/clojure/tools/analyzer/jvm.clj index 2cfbb71..f9c6d4e 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm.clj @@ -162,7 +162,7 @@ (not (resolve-ns (symbol opns) env)) (maybe-class-literal opns))] - (let [param-tags (:param-tags (meta op)) + (let [param-tags (param-tags-of op) form-meta (cond-> (meta form) param-tags (assoc :param-tags param-tags))] (cond From d61a85cc2288bd51d51eaec405f851aaf182381a Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 22:11:29 +0000 Subject: [PATCH 18/37] don't try to fit it with `.`, just keep as invoke of method-values for now --- .../clojure/clojure/tools/analyzer/jvm.clj | 48 +++++++------------ .../analyzer/passes/jvm/analyze_host_expr.clj | 8 +--- 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm.clj b/src/main/clojure/clojure/tools/analyzer/jvm.clj index f9c6d4e..61bd3d4 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm.clj @@ -162,47 +162,33 @@ (not (resolve-ns (symbol opns) env)) (maybe-class-literal opns))] - (let [param-tags (param-tags-of op) - form-meta (cond-> (meta form) - param-tags (assoc :param-tags param-tags))] - (cond - ;; (Class/new args) -> (new Class args) - (= "new" opname) - (with-meta (list* 'new target expr) - form-meta) - - ;; (Class/.method target args) -> (. ^Class (do target) (method rest-args)) - (.startsWith ^String opname ".") - (if (seq expr) - (let [method-sym (symbol (subs opname 1)) - [target-arg & args] expr] - (with-meta (list '. (with-meta (list 'do target-arg) - {:tag target}) - (if (seq args) - (list* method-sym args) - method-sym)) - form-meta)) - 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)) - form-meta)))) + (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 ..) (let [[target & args] expr target (if-let [target (maybe-class-literal target)] (with-meta (list 'do target) - {:tag 'java.lang.Class}) + {:tag 'java.lang.Class}) target) args (list* (symbol (subs opname 1)) args)] (with-meta (list '. target (if (= 1 (count args)) ;; we don't know if (.foo bar) is (first args) args)) ;; a method call or a field access - (meta form))) + (meta form))) (.endsWith opname ".") ;; (class. ..) (with-meta (list* 'new (symbol (subs opname 0 (dec (count opname)))) expr) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj index f063509..ea44eb3 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj @@ -166,12 +166,8 @@ (case op :host-call - (let [result (analyze-host-call target-type (:method ast) - (:args ast) target class? env) - param-tags (param-tags-of form)] - (if param-tags - (assoc result :param-tags param-tags) - result)) + (analyze-host-call target-type (:method ast) + (:args ast) target class? env) :host-field (analyze-host-field target-type (:field ast) From cfbc4b3a0bd1f5b5b26b25f4a319c1a02b65d0b5 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 22:29:17 +0000 Subject: [PATCH 19/37] feat: emit-form preserves param-tags if needed --- .../tools/analyzer/passes/jvm/emit_form.clj | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj index 9b64c3f..161f035 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj @@ -113,23 +113,36 @@ 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))) (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] From a9e4c8c34b8270641ae3f077b632d906349dcf7a Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 22:29:38 +0000 Subject: [PATCH 20/37] update tests --- .../clojure/tools/analyzer/jvm/core_test.clj | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj index 0d90af7..81f8cfd 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj @@ -129,19 +129,10 @@ (is (= 'String/valueOf (mexpand String/valueOf))) (is (= 'Integer/parseInt (mexpand Integer/parseInt))) - (let [expanded (mexpand (String/new "hello"))] - (is (= 'new (first expanded))) - (is (= java.lang.String (second expanded)))) - - (let [expanded (mexpand (String/.substring "hello" 1 3))] - (is (= '. (first expanded))) - (is (= '(do "hello") (second expanded))) - (is (= String (:tag (meta (second expanded))))) - (is (= 'substring (first (nth expanded 2))))) - - (let [expanded (mexpand (String/.length "hello"))] - (is (= '. (first expanded))) - (is (= 'length (nth expanded 2)))) + (is (= '(String/new "hello") (mexpand (String/new "hello")))) + (is (= '(String/.substring "hello" 1 3) (mexpand (String/.substring "hello" 1 3)))) + (is (= '(String/.length "hello") (mexpand (String/.length "hello")))) + (is (= '(Integer/parseInt "2") (mexpand (^[int] Integer/parseInt "2")))) (let [expanded (mexpand (Integer/parseInt "2"))] (is (= '. (first expanded))) From b8f5f17d9a78c8b0d50f4bca46f0b2682c031b3e Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 22:58:00 +0000 Subject: [PATCH 21/37] chore: update docstring --- .../clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj index ea44eb3..4b25c05 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj @@ -143,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 #{}}} From 45eaa6169a36d7bf796fa726f10fbf3293fb9d72 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 23:07:57 +0000 Subject: [PATCH 22/37] feat: add process-method-value --- .../passes/jvm/process_method_value.clj | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj new file mode 100644 index 0000000..97258a4 --- /dev/null +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj @@ -0,0 +1,52 @@ +;; Copyright (c) Nicola Mometto, Rich Hickey & contributors. +;; The use and distribution terms for this software are covered by the +;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +;; which can be found in the file epl-v10.html at the root of this distribution. +;; By using this software in any fashion, you are agreeing to be bound by +;; the terms of this license. +;; You must not remove this notice, or any other, from this software. + +(ns clojure.tools.analyzer.passes.jvm.process-method-value + (:require [clojure.tools.analyzer.utils :refer [source-info]] + [clojure.tools.analyzer.passes.jvm.analyze-host-expr :refer [analyze-host-expr]])) + +(defn process-method-value + "Transforms :invoke nodes whose :fn is a :method-value into the + corresponding :instance-call, :static-call, or :new node. " + {:pass-info {:walk :post :depends #{#'analyze-host-expr}}} + [{:keys [op] :as ast}] + (if (and (= :invoke op) + (= :method-value (:op (:fn ast)))) + (let [{:keys [args env]} ast + {:keys [class method kind param-tags]} (:fn ast)] + (when (and (= :instance kind) (empty? args)) + (throw (ex-info (str "Qualified instance method " (.getName ^Class class) "/." method + " must have a target") + (merge {:class class :method method} + (source-info env))))) + (merge (dissoc ast :fn :args) + (case kind + :instance + {:op :instance-call + :method method + :class class + :instance (first args) + :args (vec (rest args)) + :children [:instance :args]} + + :static + {:op :static-call + :method method + :class class + :args args + :children [:args]} + + :ctor + {:op :new + :class {:op :const :type :class :val class + :form class :env env} + :args args + :children [:class :args]}) + (when param-tags + {:param-tags param-tags}))) + ast)) From 9a8f0da10b616482e68d4ee4b1cb3d1dba5e1a15 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 23:08:15 +0000 Subject: [PATCH 23/37] feat: infer-tag depend on process-method-value --- src/main/clojure/clojure/tools/analyzer/jvm.clj | 2 ++ .../clojure/clojure/tools/analyzer/passes/jvm/infer_tag.clj | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm.clj b/src/main/clojure/clojure/tools/analyzer/jvm.clj index 61bd3d4..19fce45 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm.clj @@ -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]] @@ -483,6 +484,7 @@ #'box #'analyze-host-expr + #'process-method-value #'validate-loop-locals #'validate #'infer-tag diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/infer_tag.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/infer_tag.clj index cb601d4..ef8253c 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/infer_tag.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/infer_tag.clj @@ -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) @@ -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)] From 40b7216c9030000401a4c8603f9e403b66de6a24 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 23:14:33 +0000 Subject: [PATCH 24/37] dead code --- .../clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj index 4b25c05..edb1925 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/analyze_host_expr.clj @@ -198,9 +198,7 @@ (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 (or (param-tags-of form) - (when (coll? form) - (param-tags-of (first form)))) + (let [param-tags (param-tags-of form) kind (cond (.startsWith field-name ".") :instance (= "new" field-name) :ctor :else :static) From c4724152497f6a79fb5e92dd38c8e34e9cac3ebe Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 23:25:10 +0000 Subject: [PATCH 25/37] style --- .../tools/analyzer/passes/jvm/process_method_value.clj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj index 97258a4..a00f63f 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj @@ -14,11 +14,10 @@ "Transforms :invoke nodes whose :fn is a :method-value into the corresponding :instance-call, :static-call, or :new node. " {:pass-info {:walk :post :depends #{#'analyze-host-expr}}} - [{:keys [op] :as ast}] + [{:keys [op args env] :as ast}] (if (and (= :invoke op) (= :method-value (:op (:fn ast)))) - (let [{:keys [args env]} ast - {:keys [class method kind param-tags]} (:fn ast)] + (let [{:keys [class method kind param-tags]} (:fn ast)] (when (and (= :instance kind) (empty? args)) (throw (ex-info (str "Qualified instance method " (.getName ^Class class) "/." method " must have a target") From 263a41d7cfa1b53647a4c81cfb5e7ae5cae650b0 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 23:38:11 +0000 Subject: [PATCH 26/37] feat: if field-overload, convert to method-value --- .../passes/jvm/process_method_value.clj | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj index a00f63f..3ceb857 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/process_method_value.clj @@ -12,13 +12,20 @@ (defn process-method-value "Transforms :invoke nodes whose :fn is a :method-value into the - corresponding :instance-call, :static-call, or :new node. " + corresponding :instance-call, :static-call, or :new node. + Also converts value-position :method-value nodes with a + :field-overload (and no :param-tags) into :static-field nodes." {:pass-info {:walk :post :depends #{#'analyze-host-expr}}} [{:keys [op args env] :as ast}] - (if (and (= :invoke op) - (= :method-value (:op (:fn ast)))) - (let [{:keys [class method kind param-tags]} (:fn ast)] - (when (and (= :instance kind) (empty? args)) + (cond + (and (= :invoke op) + (= :method-value (:op (:fn ast)))) + (let [{:keys [class method kind param-tags methods]} (:fn ast) + instance? (= :instance kind) + call-args (if instance? (vec (rest args)) args) + argc (count call-args) + methods (seq (filter #(= argc (count (:parameter-types %))) methods))] + (when (and instance? (empty? args)) (throw (ex-info (str "Qualified instance method " (.getName ^Class class) "/." method " must have a target") (merge {:class class :method method} @@ -30,22 +37,38 @@ :method method :class class :instance (first args) - :args (vec (rest args)) + :args call-args :children [:instance :args]} :static {:op :static-call :method method :class class - :args args + :args call-args :children [:args]} :ctor {:op :new :class {:op :const :type :class :val class :form class :env env} - :args args + :args call-args :children [:class :args]}) (when param-tags - {:param-tags param-tags}))) - ast)) + {:param-tags param-tags}) + (when methods + {:methods (vec methods)}))) + + (and (= :method-value op) + (:field-overload ast) + (not (:param-tags ast))) + (let [{:keys [flags type name]} (:field-overload ast)] + {:op :static-field + :assignable? (not (:final flags)) + :class (:class ast) + :field name + :form (:form ast) + :env env + :o-tag type + :tag (or (:tag ast) type)}) + + :else ast)) From 2e3db8577ed4d803142037f7567e7a82faf3cad5 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 23:48:28 +0000 Subject: [PATCH 27/37] stricter validate --- .../tools/analyzer/passes/jvm/validate.clj | 124 +++++++++--------- 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj index df3f911..77cfde3 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj @@ -53,22 +53,41 @@ :form form} (source-info env)))))))) +(defn- resolve-method-by-param-tags [methods param-tags ^Class class desc env] + (or (resolve-hinted-method methods param-tags) + (throw (ex-info (str "param-tags " (pr-str param-tags) + " insufficient to resolve " desc + " in class " (.getName class)) + (merge {:class class :param-tags param-tags} + (source-info env)))))) + +(defn- tag-args-from-method [ast m] + (let [arg-tags (mapv u/maybe-class (:parameter-types m))] + (assoc ast + :args (mapv (fn [arg tag] (assoc arg :tag tag)) (:args ast) arg-tags) + :validated? true))) + +(defn- found-method [ast tag instance? m] + (let [ret-tag (:return-type m) + class (u/maybe-class (:declaring-class m))] + (merge' (-> ast (tag-args-from-method m)) + {:method (:name m) + :class class + :o-tag ret-tag + :tag (or tag ret-tag)} + (when instance? + {:instance (assoc (:instance ast) :tag class)})))) + (defmethod -validate :method-value [{:keys [class method kind param-tags methods env] :as ast}] (let [class (u/maybe-class class)] (if param-tags - (if-let [m (resolve-hinted-method methods param-tags)] + (let [m (resolve-method-by-param-tags methods param-tags class + (str (name kind) " method " method) env)] (assoc ast :class class :methods [m] - :validated? true) - (throw (ex-info (str "param-tags " (pr-str param-tags) - " insufficient to resolve " (name kind) " method " - method " in class " (.getName ^Class class)) - (merge {:class class - :method method - :param-tags param-tags} - (source-info env))))) + :validated? true)) (assoc ast :class class :validated? true)))) @@ -83,7 +102,7 @@ ast) (defmethod -validate :new - [{:keys [args] :as ast}] + [{:keys [args param-tags methods] :as ast}] (if (:validated? ast) ast (if-not (= :class (-> ast :class :type)) @@ -91,58 +110,43 @@ (merge {:class (:form (:class ast)) :ast ast} (source-info (:env ast))))) - (let [^Class class (-> ast :class :val) - c-name (symbol (.getName class)) - argc (count args) - tags (mapv :tag args)] - (let [[ctor & rest] (->> (filter #(= (count (:parameter-types %)) argc) - (u/members class c-name)) - (try-best-match tags))] - (if ctor - (if (empty? rest) - (let [arg-tags (mapv u/maybe-class (:parameter-types ctor)) - args (mapv (fn [arg tag] (assoc arg :tag tag)) args arg-tags)] - (assoc ast - :args args - :validated? true)) - ast) - (throw (ex-info (str "no ctor found for ctor of class: " class " and given signature") - (merge {:class class - :args (mapv (fn [a] (prewalk a cleanup)) args)} - (source-info (:env ast))))))))))) - -(defn- found-method [ast args tag instance? instance m] - (let [ret-tag (:return-type m) - arg-tags (mapv u/maybe-class (:parameter-types m)) - args (mapv (fn [arg tag] (assoc arg :tag tag)) args arg-tags) - class (u/maybe-class (:declaring-class m))] - (merge' ast - {:method (:name m) - :validated? true - :class class - :o-tag ret-tag - :tag (or tag ret-tag) - :args args} - (if instance? - {:instance (assoc instance :tag class)})))) + (let [^Class class (-> ast :class :val)] + (if param-tags + (-> ast (tag-args-from-method (resolve-method-by-param-tags methods param-tags class "constructor" (:env ast)))) + (let [c-name (symbol (.getName class)) + argc (count args) + tags (mapv :tag args) + [ctor & rest] (->> (filter #(= (count (:parameter-types %)) argc) + (u/members class c-name)) + (try-best-match tags))] + (if ctor + (if (empty? rest) + (-> ast (tag-args-from-method ctor)) + ast) + (throw (ex-info (str "no ctor found for ctor of class: " class " and given signature") + (merge {:class class + :args (mapv (fn [a] (prewalk a cleanup)) args)} + (source-info (:env ast)))))))))))) -(defn validate-call [{:keys [class instance method args tag env op param-tags] :as ast}] +(defn validate-call [{:keys [class instance method args tag env op param-tags methods] :as ast}] (let [argc (count args) instance? (= :instance-call op) - f (if instance? u/instance-methods u/static-methods) tags (mapv :tag args)] - (if-let [matching-methods (seq (f class method argc))] - ;; try resolving via param-tags first - (if-let [hinted-method (and param-tags - (resolve-hinted-method matching-methods param-tags))] - (found-method ast args tag instance? instance hinted-method) + (if param-tags + (-> ast + (found-method tag instance? + (resolve-method-by-param-tags methods param-tags class + (str (if instance? "instance" "static") " method " method) + env))) + (if-let [matching-methods (seq ((if instance? u/instance-methods u/static-methods) + class method argc))] (let [[m & rest :as matching] (try-best-match tags matching-methods)] (if m (let [all-ret-equals? (apply = (mapv :return-type matching))] (if (or (empty? rest) (and all-ret-equals? ;; if the method signature is the same just pick the first one (apply = (mapv #(mapv u/maybe-class (:parameter-types %)) matching)))) - (found-method ast args tag instance? instance m) + (-> ast (found-method tag instance? m)) (if all-ret-equals? (let [ret-tag (:return-type m)] (assoc ast @@ -155,14 +159,14 @@ (merge {:method method :class class :args (mapv (fn [a] (prewalk a cleanup)) args)} - (source-info env)))))))) - (if instance? - (assoc (dissoc ast :class) :tag Object :o-tag Object) - (throw (ex-info (str "No matching method: " method " for class: " class " and arity: " argc) - (merge {:method method - :class class - :argc argc} - (source-info env)))))))) + (source-info env))))))) + (if instance? + (assoc (dissoc ast :class) :tag Object :o-tag Object) + (throw (ex-info (str "No matching method: " method " for class: " class " and arity: " argc) + (merge {:method method + :class class + :argc argc} + (source-info env))))))))) (defmethod -validate :static-call [ast] From 84bc2a4315aa976a006512452849d90adbfc8e3b Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 23:56:42 +0000 Subject: [PATCH 28/37] fix test --- src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj index 81f8cfd..d735e1c 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/core_test.clj @@ -132,7 +132,7 @@ (is (= '(String/new "hello") (mexpand (String/new "hello")))) (is (= '(String/.substring "hello" 1 3) (mexpand (String/.substring "hello" 1 3)))) (is (= '(String/.length "hello") (mexpand (String/.length "hello")))) - (is (= '(Integer/parseInt "2") (mexpand (^[int] Integer/parseInt "2")))) + (is (= '(Integer/parseInt "2") (mexpand (^{:param-tags [String]} Integer/parseInt "2")))) (let [expanded (mexpand (Integer/parseInt "2"))] (is (= '. (first expanded))) From 2e1723d82915fc6d007d1dde7f6729bd01b9d3a8 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Fri, 13 Feb 2026 23:59:12 +0000 Subject: [PATCH 29/37] more tests --- .../tools/analyzer/jvm/passes_test.clj | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj index 20e25f8..f053b56 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj @@ -21,10 +21,11 @@ [clojure.tools.analyzer.passes.jvm.fix-case-test :refer [fix-case-test]] [clojure.tools.analyzer.passes.jvm.analyze-host-expr :refer [analyze-host-expr]] [clojure.tools.analyzer.passes.jvm.classify-invoke :refer [classify-invoke]]) - (:import (clojure.lang Keyword Var Symbol AFunction + (:import (clojure.lang Keyword Var Symbol AFunction ExceptionInfo PersistentVector PersistentArrayMap PersistentHashSet ISeq) java.util.regex.Pattern - (java.io File))) + (java.io File) + (java.util UUID Arrays))) (defn validate [ast] (env/with-env (ana.jvm/global-env) @@ -294,3 +295,74 @@ (let [a (ast1 Boolean/TYPE)] (is (= :static-field (:op a))))) + +(deftest bad-method-names-test + (is (thrown? ExceptionInfo (ast1 String/foo))) + (is (thrown? ExceptionInfo (ast1 String/.foo))) + (is (thrown? ExceptionInfo (ast1 Math/new)))) + +(deftest param-tags-method-signature-selection-test + (let [a (ana (r/read-string "^[double] Math/abs"))] + (is (= :method-value (:op a))) + (is (= 1 (count (:methods a)))) + (is (:validated? a))) + + (let [a (ana (r/read-string "^[float] Math/abs"))] + (is (= :method-value (:op a))) + (is (= 1 (count (:methods a)))) + (is (:validated? a))) + + (let [a (ana (r/read-string "^[long] Math/abs"))] + (is (= :method-value (:op a))) + (is (= 1 (count (:methods a)))) + (is (:validated? a))) + + (let [a (ana (r/read-string "^[int] Math/abs"))] + (is (= :method-value (:op a))) + (is (= 1 (count (:methods a)))) + (is (:validated? a)))) + +(deftest param-tags-constructor-invocation-test + (let [a (ana (r/read-string "(^[long long] java.util.UUID/new 1 2)"))] + (is (= :new (:op a))) + (is (:validated? a)) + (is (= '[long long] (:param-tags a)))) + + (let [a (ana (r/read-string "(^[String] String/new \"a\")"))] + (is (= :new (:op a))) + (is (:validated? a)) + (is (= '[String] (:param-tags a))))) + +(deftest param-tags-no-arg-invocation-test + (let [a (ana (r/read-string "(^[] String/.toUpperCase \"hello\")"))] + (is (= :instance-call (:op a))) + (is (:validated? a)) + (is (= '[] (:param-tags a)))) + + (let [a (ana (r/read-string "(^[] Long/.toString 42)"))] + (is (= :instance-call (:op a))) + (is (:validated? a)) + (is (= '[] (:param-tags a))))) + +(deftest param-tags-wildcard-test + (let [a (ana (r/read-string "(^[_ _] String/.substring \"hello\" 1 3)"))] + (is (= :instance-call (:op a))) + (is (:validated? a)) + (is (= '[_ _] (:param-tags a))))) + +(deftest param-tags-array-types-test + (let [a (ana (r/read-string "^[long/1 long] java.util.Arrays/binarySearch"))] + (is (= :method-value (:op a))) + (is (= 1 (count (:methods a)))) + (is (:validated? a))) + + (let [a (ana (r/read-string "^[Object/1 _] java.util.Arrays/binarySearch"))] + (is (= :method-value (:op a))) + (is (= 1 (count (:methods a)))) + (is (:validated? a)))) + +(deftest bad-param-tags-test + (is (thrown? ExceptionInfo (ana (r/read-string "^[String String] Math/abs")))) + (is (thrown? ExceptionInfo (ana (r/read-string "(^[] String/foo \"a\")")))) + (is (thrown? ExceptionInfo (ana (r/read-string "(^[] String/.foo \"a\")")))) + (is (thrown? ExceptionInfo (ana (r/read-string "(^[String String String] java.util.UUID/new 1 2 3)"))))) From 11ff2fc115a0ad7c4a710b98507756e91e3798bf Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Sat, 14 Feb 2026 00:09:37 +0000 Subject: [PATCH 30/37] fix: most-specific one --- .../clojure/clojure/tools/analyzer/jvm/utils.clj | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj b/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj index 6dd481c..d699faa 100644 --- a/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj +++ b/src/main/clojure/clojure/tools/analyzer/jvm/utils.clj @@ -409,12 +409,22 @@ (= 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 (filter #(signature-matches? param-classes %) methods)] + matching (most-specific (filter #(signature-matches? param-classes %) methods))] (when (= 1 (count matching)) (first matching)))) From 7a9260d24a079b443b58e9cb1d60341b61bb8db3 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Sat, 14 Feb 2026 00:35:12 +0000 Subject: [PATCH 31/37] more tests --- pom.xml | 4 +++ .../tools/analyzer/jvm/passes_test.clj | 32 +++++++++++++++++-- .../jvm/test/FieldMethodOverload.java | 13 ++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/test/java/clojure/tools/analyzer/jvm/test/FieldMethodOverload.java diff --git a/pom.xml b/pom.xml index 6afaf30..0e8e1e1 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,10 @@ + + src/test/java + + scm:git:git://github.com/clojure/tools.analyzer.jvm.git scm:git:git://github.com/clojure/tools.analyzer.jvm.git diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj index f053b56..792b60c 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj @@ -25,7 +25,8 @@ PersistentVector PersistentArrayMap PersistentHashSet ISeq) java.util.regex.Pattern (java.io File) - (java.util UUID Arrays))) + (java.util UUID Arrays) + clojure.tools.analyzer.jvm.test.FieldMethodOverload)) (defn validate [ast] (env/with-env (ana.jvm/global-env) @@ -199,12 +200,14 @@ (let [a (ana (r/read-string "^[long] String/valueOf"))] (is (= :method-value (:op a))) (is (:validated? a)) - (is (= 1 (count (:methods a))))) + (is (= 1 (count (:methods a)))) + (is (= '[long] (-> a :methods first :parameter-types)))) (let [a (ana (r/read-string "^[int int] String/.substring"))] (is (= :method-value (:op a))) (is (:validated? a)) - (is (= 1 (count (:methods a)))))) + (is (= 1 (count (:methods a)))) + (is (= '[int int] (-> a :methods first :parameter-types))))) (deftest method-value-kinds-test (let [a (ast1 File/.isDirectory)] @@ -305,21 +308,25 @@ (let [a (ana (r/read-string "^[double] Math/abs"))] (is (= :method-value (:op a))) (is (= 1 (count (:methods a)))) + (is (= '[double] (-> a :methods first :parameter-types))) (is (:validated? a))) (let [a (ana (r/read-string "^[float] Math/abs"))] (is (= :method-value (:op a))) (is (= 1 (count (:methods a)))) + (is (= '[float] (-> a :methods first :parameter-types))) (is (:validated? a))) (let [a (ana (r/read-string "^[long] Math/abs"))] (is (= :method-value (:op a))) (is (= 1 (count (:methods a)))) + (is (= '[long] (-> a :methods first :parameter-types))) (is (:validated? a))) (let [a (ana (r/read-string "^[int] Math/abs"))] (is (= :method-value (:op a))) (is (= 1 (count (:methods a)))) + (is (= '[int] (-> a :methods first :parameter-types))) (is (:validated? a)))) (deftest param-tags-constructor-invocation-test @@ -354,11 +361,13 @@ (let [a (ana (r/read-string "^[long/1 long] java.util.Arrays/binarySearch"))] (is (= :method-value (:op a))) (is (= 1 (count (:methods a)))) + (is (= '[long<> long] (-> a :methods first :parameter-types))) (is (:validated? a))) (let [a (ana (r/read-string "^[Object/1 _] java.util.Arrays/binarySearch"))] (is (= :method-value (:op a))) (is (= 1 (count (:methods a)))) + (is (= '[java.lang.Object<> java.lang.Object] (-> a :methods first :parameter-types))) (is (:validated? a)))) (deftest bad-param-tags-test @@ -366,3 +375,20 @@ (is (thrown? ExceptionInfo (ana (r/read-string "(^[] String/foo \"a\")")))) (is (thrown? ExceptionInfo (ana (r/read-string "(^[] String/.foo \"a\")")))) (is (thrown? ExceptionInfo (ana (r/read-string "(^[String String String] java.util.UUID/new 1 2 3)"))))) + +(deftest field-method-overload-test + (let [a (ast1 clojure.tools.analyzer.jvm.test.FieldMethodOverload/doppelganger)] + (is (= :static-field (:op a)))) + + (let [a (ana (r/read-string "^[] clojure.tools.analyzer.jvm.test.FieldMethodOverload/doppelganger"))] + (is (= :method-value (:op a))) + (is (= 1 (count (:methods a)))) + (is (= '[] (-> a :methods first :parameter-types)))) + + (let [a (ast1 (clojure.tools.analyzer.jvm.test.FieldMethodOverload/doppelganger))] + (is (= :static-call (:op a))) + (is (:validated? a))) + + (let [a (ast1 (clojure.tools.analyzer.jvm.test.FieldMethodOverload/doppelganger (int 1) (int 2)))] + (is (= :static-call (:op a))) + (is (:validated? a)))) diff --git a/src/test/java/clojure/tools/analyzer/jvm/test/FieldMethodOverload.java b/src/test/java/clojure/tools/analyzer/jvm/test/FieldMethodOverload.java new file mode 100644 index 0000000..c7097b7 --- /dev/null +++ b/src/test/java/clojure/tools/analyzer/jvm/test/FieldMethodOverload.java @@ -0,0 +1,13 @@ +package clojure.tools.analyzer.jvm.test; + +public class FieldMethodOverload { + public static final String doppelganger = "static-field"; + + public static String doppelganger() { + return ""; + } + + public static String doppelganger(int a, int b) { + return "int-int"; + } +} From 74c4af37dd04a0a947d7dbe28029bb51d57f23da Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Sat, 14 Feb 2026 00:36:51 +0000 Subject: [PATCH 32/37] local helper --- build.clj | 6 ++++++ deps.edn | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 build.clj diff --git a/build.clj b/build.clj new file mode 100644 index 0000000..c662baf --- /dev/null +++ b/build.clj @@ -0,0 +1,6 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(defn compile-test-java [_] + (b/javac {:src-dirs ["src/test/java"] + :class-dir "target/test-classes"})) diff --git a/deps.edn b/deps.edn index 48d8474..3befb50 100644 --- a/deps.edn +++ b/deps.edn @@ -3,4 +3,6 @@ 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" "src/test/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}}} From 6d72063afd513729e5d4fd89cadf191f66c86b73 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Sun, 15 Feb 2026 18:22:48 +0000 Subject: [PATCH 33/37] wip --- build.clj | 9 +++- .../tools/analyzer/jvm/passes_test.clj | 38 +++++++++++++++++ .../clojure/tools/analyzer/jvm/test/Foo.java | 41 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/test/java/clojure/tools/analyzer/jvm/test/Foo.java diff --git a/build.clj b/build.clj index c662baf..544a9df 100644 --- a/build.clj +++ b/build.clj @@ -1,6 +1,11 @@ (ns build - (:require [clojure.tools.build.api :as b])) + (: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"})) + :class-dir "target/test-classes" + :basis basis})) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj index 792b60c..efc2318 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj @@ -392,3 +392,41 @@ (let [a (ast1 (clojure.tools.analyzer.jvm.test.FieldMethodOverload/doppelganger (int 1) (int 2)))] (is (= :static-call (:op a))) (is (:validated? a)))) + +#_(deftest static-field-method-bonanza + (doseq [x '[clojure.tools.analyzer.jvm.test.Foo/bar + (clojure.tools.analyzer.jvm.test.Foo/bar) + ((clojure.tools.analyzer.jvm.test.Foo/bar)) + (. clojure.tools.analyzer.jvm.test.Foo -bar) + ((. clojure.tools.analyzer.jvm.test.Foo -bar)) + (clojure.tools.analyzer.jvm.test.Foo/bar 1) + ((clojure.tools.analyzer.jvm.test.Foo/bar) 1) + (. clojure.tools.analyzer.jvm.test.Foo -bar 1) + ((. clojure.tools.analyzer.jvm.test.Foo -bar) 1) + (((. clojure.tools.analyzer.jvm.test.Foo -bar)) 1) + clojure.tools.analyzer.jvm.test.Foo/baz + (clojure.tools.analyzer.jvm.test.Foo/baz) + ((clojure.tools.analyzer.jvm.test.Foo/baz)) + (. clojure.tools.analyzer.jvm.test.Foo -baz) + ((. clojure.tools.analyzer.jvm.test.Foo -baz)) + (clojure.tools.analyzer.jvm.test.Foo/baz 1) + ((clojure.tools.analyzer.jvm.test.Foo/baz) 1) + (. clojure.tools.analyzer.jvm.test.Foo -baz 1) + ((. clojure.tools.analyzer.jvm.test.Foo -baz) 1) + clojure.tools.analyzer.jvm.test.Foo/qux + (clojure.tools.analyzer.jvm.test.Foo/qux) + ((clojure.tools.analyzer.jvm.test.Foo/qux)) + (. clojure.tools.analyzer.jvm.test.Foo -qux) + ((. clojure.tools.analyzer.jvm.test.Foo -qux)) + (clojure.tools.analyzer.jvm.test.Foo/qux 1) + ((clojure.tools.analyzer.jvm.test.Foo/qux) 1) + (. clojure.tools.analyzer.jvm.test.Foo -qux 1) + ((. clojure.tools.analyzer.jvm.test.Foo -qux) 1)]] + (let [=? (fn [a b] + (if (.startsWith (pr-str a) "#function") + (= (a 1) (b 1)) + (= a b)))] + (is (=? (try (eval x) (catch Exception _ ::exception)) + (try (eval (emit-form (ana x))) + (catch Exception _ ::exception))) + (str "bad " x))))) diff --git a/src/test/java/clojure/tools/analyzer/jvm/test/Foo.java b/src/test/java/clojure/tools/analyzer/jvm/test/Foo.java new file mode 100644 index 0000000..f54b4f8 --- /dev/null +++ b/src/test/java/clojure/tools/analyzer/jvm/test/Foo.java @@ -0,0 +1,41 @@ +package clojure.tools.analyzer.jvm.test; + +import clojure.lang.AFn; +import clojure.lang.IFn; + +public class Foo { + + public static final IFn bar = new AFn() { + public Object invoke() { + return "bar"; + } + public Object invoke(Object x) { + return "bar" + x.toString(); + } + }; + + public static final IFn baz = new AFn() { + public Object invoke() { + return "baz"; + } + public Object invoke(Object x) { + return "baz" + x.toString(); + } + }; + + public static String bar() { + return "bar()"; + } + public static String bar(Object x) { + return "bar()" + x.toString(); + } + + + public static String qux() { + return "qux()"; + } + public static String qux(Object x) { + return "qux()" + x.toString(); + } + +} From 5bea2a1fb847ada5075469c2c07de8885de6063f Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Sun, 15 Feb 2026 18:36:43 +0000 Subject: [PATCH 34/37] fix: defensive wrapping for static-field --- .../clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj index 161f035..a2a3545 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj @@ -123,7 +123,7 @@ (defmethod -emit-form :static-field [{:keys [class field]} opts] - (symbol (class->str class) (name field))) + `(~(symbol (class->str class) (name field)))) (defmethod -emit-form :static-call [{:keys [class method args param-tags]} opts] From 414d0843a412162503b83609946470b2e798ff8a Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Sun, 15 Feb 2026 19:06:24 +0000 Subject: [PATCH 35/37] fix: defensive emission for static-field --- .../clojure/tools/analyzer/passes/jvm/emit_form.clj | 6 ++++-- .../clojure/tools/analyzer/passes/jvm/validate.clj | 11 ++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj index a2a3545..713f12a 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/emit_form.clj @@ -122,8 +122,10 @@ `(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 param-tags]} opts] diff --git a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj index 77cfde3..6ce35bc 100644 --- a/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj +++ b/src/main/clojure/clojure/tools/analyzer/passes/jvm/validate.clj @@ -175,10 +175,15 @@ (validate-call (assoc ast :class (u/maybe-class (:class ast)))))) (defmethod -validate :static-field - [ast] - (if (:validated? ast) + [{:keys [class validated? field] :as ast}] + (if validated? ast - (assoc ast :class (u/maybe-class (:class ast))))) + (let [class (u/maybe-class class) + overloaded-field? (boolean (some :return-type (u/static-members class field)))] + (assoc ast + :overloaded-field? overloaded-field? + :class class + :validated? true)))) (defmethod -validate :instance-call [{:keys [class validated? instance] :as ast}] From f3c10ec225d35a24659758ede8d5007fbe1df589 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Sun, 15 Feb 2026 19:13:02 +0000 Subject: [PATCH 36/37] bump t.a --- deps.edn | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deps.edn b/deps.edn index 3befb50..cae28ed 100644 --- a/deps.edn +++ b/deps.edn @@ -1,5 +1,5 @@ {: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"}} diff --git a/pom.xml b/pom.xml index 0e8e1e1..18b99c5 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.clojure tools.analyzer - 1.2.1 + 1.2.2 org.clojure From 2dd29779418ead77422a78aa130ac389a4d4c528 Mon Sep 17 00:00:00 2001 From: Nicola Mometto Date: Sun, 15 Feb 2026 19:13:41 +0000 Subject: [PATCH 37/37] enable test --- .../tools/analyzer/jvm/passes_test.clj | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj index efc2318..da195e4 100644 --- a/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj +++ b/src/test/clojure/clojure/tools/analyzer/jvm/passes_test.clj @@ -393,40 +393,40 @@ (is (= :static-call (:op a))) (is (:validated? a)))) -#_(deftest static-field-method-bonanza - (doseq [x '[clojure.tools.analyzer.jvm.test.Foo/bar - (clojure.tools.analyzer.jvm.test.Foo/bar) - ((clojure.tools.analyzer.jvm.test.Foo/bar)) - (. clojure.tools.analyzer.jvm.test.Foo -bar) - ((. clojure.tools.analyzer.jvm.test.Foo -bar)) - (clojure.tools.analyzer.jvm.test.Foo/bar 1) - ((clojure.tools.analyzer.jvm.test.Foo/bar) 1) - (. clojure.tools.analyzer.jvm.test.Foo -bar 1) - ((. clojure.tools.analyzer.jvm.test.Foo -bar) 1) - (((. clojure.tools.analyzer.jvm.test.Foo -bar)) 1) - clojure.tools.analyzer.jvm.test.Foo/baz - (clojure.tools.analyzer.jvm.test.Foo/baz) - ((clojure.tools.analyzer.jvm.test.Foo/baz)) - (. clojure.tools.analyzer.jvm.test.Foo -baz) - ((. clojure.tools.analyzer.jvm.test.Foo -baz)) - (clojure.tools.analyzer.jvm.test.Foo/baz 1) - ((clojure.tools.analyzer.jvm.test.Foo/baz) 1) - (. clojure.tools.analyzer.jvm.test.Foo -baz 1) - ((. clojure.tools.analyzer.jvm.test.Foo -baz) 1) - clojure.tools.analyzer.jvm.test.Foo/qux - (clojure.tools.analyzer.jvm.test.Foo/qux) - ((clojure.tools.analyzer.jvm.test.Foo/qux)) - (. clojure.tools.analyzer.jvm.test.Foo -qux) - ((. clojure.tools.analyzer.jvm.test.Foo -qux)) - (clojure.tools.analyzer.jvm.test.Foo/qux 1) - ((clojure.tools.analyzer.jvm.test.Foo/qux) 1) - (. clojure.tools.analyzer.jvm.test.Foo -qux 1) - ((. clojure.tools.analyzer.jvm.test.Foo -qux) 1)]] - (let [=? (fn [a b] - (if (.startsWith (pr-str a) "#function") - (= (a 1) (b 1)) - (= a b)))] - (is (=? (try (eval x) (catch Exception _ ::exception)) - (try (eval (emit-form (ana x))) - (catch Exception _ ::exception))) - (str "bad " x))))) +(deftest static-field-method-bonanza + (doseq [x '[clojure.tools.analyzer.jvm.test.Foo/bar + (clojure.tools.analyzer.jvm.test.Foo/bar) + ((clojure.tools.analyzer.jvm.test.Foo/bar)) + (. clojure.tools.analyzer.jvm.test.Foo -bar) + ((. clojure.tools.analyzer.jvm.test.Foo -bar)) + (clojure.tools.analyzer.jvm.test.Foo/bar 1) + ((clojure.tools.analyzer.jvm.test.Foo/bar) 1) + (. clojure.tools.analyzer.jvm.test.Foo -bar 1) + ((. clojure.tools.analyzer.jvm.test.Foo -bar) 1) + (((. clojure.tools.analyzer.jvm.test.Foo -bar)) 1) + clojure.tools.analyzer.jvm.test.Foo/baz + (clojure.tools.analyzer.jvm.test.Foo/baz) + ((clojure.tools.analyzer.jvm.test.Foo/baz)) + (. clojure.tools.analyzer.jvm.test.Foo -baz) + ((. clojure.tools.analyzer.jvm.test.Foo -baz)) + (clojure.tools.analyzer.jvm.test.Foo/baz 1) + ((clojure.tools.analyzer.jvm.test.Foo/baz) 1) + (. clojure.tools.analyzer.jvm.test.Foo -baz 1) + ((. clojure.tools.analyzer.jvm.test.Foo -baz) 1) + clojure.tools.analyzer.jvm.test.Foo/qux + (clojure.tools.analyzer.jvm.test.Foo/qux) + ((clojure.tools.analyzer.jvm.test.Foo/qux)) + (. clojure.tools.analyzer.jvm.test.Foo -qux) + ((. clojure.tools.analyzer.jvm.test.Foo -qux)) + (clojure.tools.analyzer.jvm.test.Foo/qux 1) + ((clojure.tools.analyzer.jvm.test.Foo/qux) 1) + (. clojure.tools.analyzer.jvm.test.Foo -qux 1) + ((. clojure.tools.analyzer.jvm.test.Foo -qux) 1)]] + (let [=? (fn [a b] + (if (.contains (str (class a)) "invoke") + (= (a 1) (b 1)) + (= a b)))] + (is (=? (try (eval x) (catch Exception _ ::exception)) + (try (eval (emit-form (ana x))) + (catch Exception _ ::exception))) + (str "bad " x)))))