1+ ; Copyright (c) Rich Hickey. All rights reserved.
2+ ; The use and distribution terms for this software are covered by the
3+ ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
4+ ; which can be found in the file epl-v10.html at the root of this distribution.
5+ ; By using this software in any fashion, you are agreeing to be bound by
6+ ; the terms of this license.
7+ ; You must not remove this notice, or any other, from this software.
8+ ; Authors: Fogus
9+
10+ (ns clojure.test-clojure.param-tags
11+ (:use clojure.test)
12+ (:require
13+ [clojure.string :as str]
14+ [clojure.reflect :as r]
15+ [clojure.test-helper :refer [should-not-reflect]])
16+ (:import
17+ (clojure.test SwissArmy ConcreteClass)
18+ (clojure.lang Tuple Compiler Compiler$CompilerException)
19+ (java.util Arrays UUID Locale)))
20+
21+ (set! *warn-on-reflection* true )
22+
23+ (deftest no-hints-with-param-tags
24+ (should-not-reflect
25+ (defn touc-no-reflect [s]
26+ (^[] String/.toUpperCase s)))
27+ (should-not-reflect
28+ (defn touc-no-reflectq [s]
29+ (^[] java.lang.String/.toUpperCase s)))
30+ (should-not-reflect
31+ (defn touc-no-reflect-arg-tags [s]
32+ (^[java.util.Locale] String/.toUpperCase s java.util.Locale/ENGLISH)))
33+ (should-not-reflect
34+ (defn no-overloads-no-reflect [v]
35+ (java.time.OffsetDateTime/.getYear v))))
36+
37+ (deftest no-param-tags-use-qualifier
38+ ; ; both Date and OffsetDateTime have .getYear - want to show here the qualifier is used
39+ (let [f (fn [^java.util.Date d] (java.time.OffsetDateTime/.getYear d))
40+ date (java.util.Date. 1714495523100 )]
41+ ; ; works when passed OffsetDateTime
42+ (is (= 2024 (f (-> date .toInstant (.atOffset java.time.ZoneOffset/UTC)))))
43+
44+ ; ; fails when passed Date, expects OffsetDateTime
45+ (is (thrown? ClassCastException
46+ (f date)))))
47+
48+ (deftest param-tags-in-invocation-positions
49+ (testing " qualified static method invocation"
50+ (is (= 3 (^[long] Math/abs -3 )))
51+ (is (= [1 2 ] (^[_ _] Tuple/create 1 2 )))
52+ (is (= " 42" (Long/toString 42 ))))
53+ (testing " qualified ctor invocation"
54+ (is (= (^[long long] UUID/new 1 2 ) #uuid " 00000000-0000-0001-0000-000000000002" ))
55+ (is (= (^[long long] java.util.UUID/new 1 2 ) #uuid " 00000000-0000-0001-0000-000000000002" ))
56+ (is (= " a" (^[String] String/new " a" ))))
57+ (testing " qualified instance method invocation"
58+ (is (= \A (String/.charAt " A" 0 )))
59+ (is (= " A" (^[java.util.Locale] String/.toUpperCase " a" java.util.Locale/ENGLISH)))
60+ (is (= " A" (^[Locale] String/.toUpperCase " a" java.util.Locale/ENGLISH)))
61+ (is (= 65 (aget (^[String] String/.getBytes " A" " US-ASCII" ) 0 )))
62+ (is (= " 42" (^[] Long/.toString 42 ))))
63+ (testing " string repr array type resolutions"
64+ (let [lary (long-array [1 2 3 4 99 100 ])
65+ oary (into-array [1 2 3 4 99 100 ])
66+ sary (into-array String [" a" " b" " c" ])]
67+ (is (= 4 (^[longs long] Arrays/binarySearch lary (long 99 ))))
68+ (is (= 4 (^[objects _] Arrays/binarySearch oary 99 )))
69+ (is (= 4 (^["[Ljava.lang.Object;" _] Arrays/binarySearch oary 99 )))
70+ (is (= 1 (^["[Ljava.lang.Object;" _] Arrays/binarySearch sary " b" )))))
71+ (testing " bad method names"
72+ (is (thrown? Exception (eval '(^[] java.lang.String/foo " a" ))))
73+ (is (thrown? Exception (eval '(^[] java.lang.String/.foo " a" ))))
74+ (is (thrown? Exception (eval '(^[] Math/new " a" ))))))
75+
76+
77+ ; ; Mapping of symbols returned from reflect call to :parameter-type used as arguments to .getDeclaredMethod,
78+ ; ; :arg-type used as arguments to the methods and constructors being tested, :arg-tag used as arg-tags
79+ ; ; to the methods and constructors being tested.
80+ (def reflected-parameter-types {'int {:parameter-type Integer/TYPE
81+ :arg-type " (int 42)"
82+ :arg-tag " int" }
83+ 'boolean {:parameter-type Boolean/TYPE
84+ :arg-type " true"
85+ :arg-tag " boolean" }
86+ 'long {:parameter-type Long/TYPE
87+ :arg-type " 42"
88+ :arg-tag " long" }
89+ 'long<> {:parameter-type (Class/forName " [J" )
90+ :arg-type " (long-array [1 2])"
91+ :arg-tag " long*" }
92+ 'int<><> {:parameter-type (Class/forName " [[I" )
93+ :arg-type " (make-array Integer/TYPE 1 2)"
94+ :arg-tag " int**" }
95+ 'java.lang.Object<> {:parameter-type (Class/forName " [Ljava.lang.Object;" )
96+ :arg-type " (into-array [1 2])"
97+ :arg-tag " \" [Ljava.lang.Object;\" " }
98+ 'java.lang.String<> {:parameter-type (Class/forName " [Ljava.lang.String;" )
99+ :arg-type " (into-array [\" a\" \" b\" ])"
100+ :arg-tag " \" [Ljava.lang.String;\" " }})
101+
102+ (defn is-static-method? [class method-name params]
103+ (let [method (.getDeclaredMethod ^Class class ^String (name method-name) ^" [Ljava.lang.Object;" params)]
104+ (java.lang.reflect.Modifier/isStatic (.getModifiers method))))
105+
106+ (defn get-methods
107+ " Reflect the class located at `path`, filter out the public members, add a :type
108+ of :constructor, :static, or :instance to each."
109+ [path]
110+ (let [reflected-class (r/reflect (resolve path))
111+ public (filter #(contains? (:flags %) :public ) (:members reflected-class))]
112+ (reduce (fn [res m]
113+ (let [class (-> m :declaring-class resolve)
114+ params (into-array Class (map #(-> % reflected-parameter-types :parameter-type ) (:parameter-types m)))]
115+ (cond
116+ (not (contains? m :return-type )) (conj res (assoc m :type :constructor ))
117+ (is-static-method? class (:name m) params) (conj res (assoc m :type :static ))
118+ :else (conj res (assoc m :type :instance )))))
119+ [] public)))
120+
121+ (defn exercise-constructor
122+ " Provided a map of data returned from a call to reflect representing a constructor.
123+ Construct a new instance of the class providing the appropriate arg-tags and return
124+ a map containing the new instance and expected target class"
125+ [{:keys [declaring-class parameter-types] :as m}]
126+ (let [target-class (-> declaring-class str Class/forName)
127+ args (str/join " " (map #(-> % reflected-parameter-types :arg-type ) parameter-types))
128+ arg-tags (str/join " " (map #(-> % reflected-parameter-types :arg-tag ) parameter-types))
129+ fun-call-str (read-string (str " (^[" arg-tags " ] " declaring-class " . " args " )" ))
130+ _ (should-not-reflect #(eval 'fun-call-str))
131+ new-instance (eval fun-call-str)]
132+ {:expected target-class :actual new-instance}))
133+
134+ (defn exercise-static-method
135+ " Provided a map of data returned from a call to reflect representing a static class method.
136+ Call the static method providing the appropriate arg-tags and return a map containing
137+ the actual and expected response."
138+ [{:keys [name declaring-class parameter-types]}]
139+ (let [class (str declaring-class)
140+ method (str name)
141+ args (str/join " " (map #(-> % reflected-parameter-types :arg-type ) parameter-types))
142+ arg-tags (str/join " " (map #(-> % reflected-parameter-types :arg-tag ) parameter-types))
143+ expected-response (str/join " -" parameter-types)
144+ fun-call-str (read-string (str " (^[" arg-tags " ] " class " /" method " " args " )" ))
145+ _ (should-not-reflect #(eval 'fun-call-str))
146+ response (eval fun-call-str)]
147+ {:expected expected-response :actual response}))
148+
149+ (defn exercise-instance-method
150+ " Provided a map of data returned from a call to reflect representing a class instance method.
151+ Call the method providing the appropriate arg-tags and return a map containing
152+ the actual and expected response."
153+ [{:keys [name declaring-class parameter-types]}]
154+ (let [method (str " ." name)
155+ args (str/join " " (map #(-> % reflected-parameter-types :arg-type ) parameter-types))
156+ arg-tags (str/join " " (map #(-> % reflected-parameter-types :arg-tag ) parameter-types))
157+ expected-response (str/join " -" parameter-types)
158+ fun-call-str (read-string (str " (^[" arg-tags " ] " declaring-class " /" method " " " (" declaring-class " .)" " " args " )" ))
159+ _ (should-not-reflect #(eval 'fun-call-str))
160+ response (eval fun-call-str)]
161+ {:expected expected-response :actual response}))
162+
163+ (deftest arg-tags-in-constructors-and-static-and-instance-methods
164+ (doseq [m (get-methods 'clojure.test.SwissArmy)]
165+ (case (:type m)
166+ :constructor (let [{:keys [expected actual]} (exercise-constructor m)]
167+ (is (instance? expected actual)))
168+ :static (let [{:keys [expected actual]} (exercise-static-method m)]
169+ (is (= expected actual)))
170+ :instance (let [{:keys [expected actual]} (exercise-instance-method m)]
171+ (is (= expected actual))))))
172+
173+ (defmacro arg-tags-called-in-macro
174+ [a-type b-type a b]
175+ `(^[~a-type ~b-type] SwissArmy/staticArityOverloadMethod ~a ~b))
176+
177+ (deftest arg-tags-in-macro
178+ (is (= " int-int" (arg-tags-called-in-macro int int 1 2 ))))
179+
180+ (deftest bridge-methods
181+ (testing " Allows correct intended usage."
182+ (let [concrete (ConcreteClass. )]
183+ (is (= 42 (^[Integer] ConcreteClass/.stampWidgets concrete (int 99 ))))))
184+ (testing " Will not call bridge method."
185+ (is (thrown? Compiler$CompilerException
186+ (eval '(let [concrete (clojure.test.ConcreteClass. )]
187+ (^[Object] ConcreteClass/.stampWidgets concrete (int 99 ))))))))
188+
189+
190+ (deftest incorrect-arity-invocation-error-messages
191+
192+ (testing " Invocation with param-tags having incorrect number of args"
193+ (let [e (try
194+ (eval '(^[long] Math/abs -1 -2 -3 ))
195+ (catch Compiler$CompilerException e (str " -> " (.getMessage (.getCause e)))))]
196+ (is (not (nil? (re-find #"expected 1.*received 3" e))) " Error message was expected to indicate 1 argument was expected but 2 were provided" ))))
0 commit comments