Skip to content

Instantly share code, notes, and snippets.

@pmonks
Last active November 13, 2023 19:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pmonks/223e60def27266e79fff47de734d060a to your computer and use it in GitHub Desktop.
Save pmonks/223e60def27266e79fff47de734d060a to your computer and use it in GitHub Desktop.
Pretty prints results from clojure.reflect/reflect on an object or class in an idiomatic Clojure fashion
;
; Copyright © 2023 Peter Monks
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
;
; SPDX-License-Identifier: Apache-2.0
;
(require '[clojure.string :as s])
(require '[clojure.reflect :as cr])
(defn object->class
"Converts any object into a java.lang.Class."
[obj]
(cond
(class? obj) obj
; Note: Treating symbols as Java classes makes it impossible to introspect on
; Clojure's symbol class itself.
; (symbol? obj) (let [ns (or (namespace obj) "java.lang")
; name (name obj)]
; (Class/forName (str ns "." name)))
:else (class obj)))
(defn type->string
"Converts a Java object or class (including things like primitive arrays) to a
human readable, idiomatic Clojure string."
[t]
(cr/typename (if (= java.lang.Class (class t))
t
(object->class t))))
(defn member-type->string
"Converts a member type (e.g. its :type or one of its :parameter-types) to a
human readable, idiomatic Clojure string."
[t]
(if (= "void" (str t))
"nil"
(s/replace (str t) "<>" "[]")))
(defn params->string
"Converts the parameter types of a member to a human readable, idiomatic
Clojure string."
[params]
(s/join " " (map member-type->string params)))
(defmulti member->string
"Returns a human-readable, idiomatic Clojure string representation of the
given member contained within the given class or object (e.g. returned from
clojure.reflect/reflect)."
(fn [_ m] (class m)))
(defmethod member->string clojure.reflect.Constructor
[x m]
(let [tn (type->string x)]
(when (= tn (str (:declaring-class m))) ; Constructors are not inherited in Java, yet they show up in the results of cr/reflect, so we filter them out.
(str (when-not (contains? (:flags m) :public) "<non-public> ")
"(" tn "."
(let [params (str " "
(params->string (:parameter-types m)))]
(when-not (s/blank? params) params))
") => " tn)))) ; Java constructors have an implicit return type of the type itself - just make it explicit as that's closer to how they're used in Clojure
(defmethod member->string clojure.reflect.Method
[x m]
(let [tn (type->string x)]
(str (when-not (contains? (:flags m) :public) "<non-public> ")
"("
(if (contains? (:flags m) :static)
(str tn "/")
".")
(:name m)
(let [params (str " "
(s/join " " (concat (when-not (contains? (:flags m) :static) [tn])
(map member-type->string (:parameter-types m)))))] ; We don't use params->string here as we want the collection
(when-not (s/blank? params) params))
") => " (member-type->string (:return-type m)))))
(defmethod member->string clojure.reflect.Field
[x m]
(let [tn (type->string x)]
(str
(when-not (contains? (:flags m) :public) "<non-public> ")
(if (contains? (:flags m) :static)
(str tn "/" (:name m) " => " (:type m))
(str "(.-" (:name m) " " tn ") => " (:type m))))))
(defn member-sort
"Function for use in sort-by with Java members."
[m]
(when m
(str (if (contains? (:flags m) :public) "0" "1")
"-"
(if (contains? (:flags m) :static) "0" "1")
"-"
(condp = (class m) ; Note: case doesn't work property with class literals - see https://stackoverflow.com/questions/12028944/clojure-case-statement-with-classes
clojure.reflect.Field "0"
clojure.reflect.Constructor "1"
clojure.reflect.Method "2")
"-"
(:name m)
"-"
(str (count (:parameter-types m)))
"-"
(params->string (:parameter-types m)))))
(defn java-members
"Returns a human-readable string representation of all of the Java members
(constructors, methods, fields) in the given class or object, including those
inherited from superclasses, as semi-idiomatic Clojure code. Defaults to
public members only, add {:public-only false} to get all."
[x & {public-only :public-only :or {public-only true}}]
(when x
(let [clazz (object->class x)]
(->> (cr/reflect clazz :ancestors true)
:members
(filter #(not (contains? (:flags %) :synthetic))) ; Remove synthetic (compiler-inserted) members
(filter #(or (not public-only) (contains? (:flags %) :public)))
(sort-by member-sort)
(map (partial member->string clazz))
distinct
(filter #(not (s/blank? %)))
(s/join "\n")))))
(defn print-java-members
"Prints to stdout a human-readable string representation of all of the Java
members (constructors, methods, fields) in the given class or object,
including those inherited from superclasses, as semi-idiomatic Clojure code.
Defaults to public members only, add {:public-only false} to get all."
[x & {public-only :public-only :or {public-only true}}]
(let [clazz (object->class x)]
(println "--------------------")
(println (str (when-not (= java.lang.Class (class x)) (str "\"" x "\" of type "))
(type->string clazz)
" has these"
(when public-only " public")
" Java members:\n"))
(println (java-members clazz {:public-only public-only}))))
@pmonks
Copy link
Author

pmonks commented Feb 1, 2023

Example output (on openjdk version "17.0.6" 2023-01-17):

user=> (print-java-members "foo")
--------------------
"foo" of type java.lang.String has these public Java members:

java.lang.String/CASE_INSENSITIVE_ORDER => java.util.Comparator
(java.lang.String/compare java.lang.CharSequence java.lang.CharSequence) => int
(java.lang.String/copyValueOf char[]) => java.lang.String
(java.lang.String/copyValueOf char[] int int) => java.lang.String
(java.lang.String/format java.lang.String java.lang.Object[]) => java.lang.String
(java.lang.String/format java.util.Locale java.lang.String java.lang.Object[]) => java.lang.String
(java.lang.String/join java.lang.CharSequence java.lang.CharSequence[]) => java.lang.String
(java.lang.String/join java.lang.CharSequence java.lang.Iterable) => java.lang.String
(java.lang.String/valueOf boolean) => java.lang.String
(java.lang.String/valueOf char) => java.lang.String
(java.lang.String/valueOf char[]) => java.lang.String
(java.lang.String/valueOf double) => java.lang.String
(java.lang.String/valueOf float) => java.lang.String
(java.lang.String/valueOf int) => java.lang.String
(java.lang.String/valueOf java.lang.Object) => java.lang.String
(java.lang.String/valueOf long) => java.lang.String
(java.lang.String/valueOf char[] int int) => java.lang.String
(java.lang.String.) => java.lang.String
(java.lang.String. byte[]) => java.lang.String
(java.lang.String. char[]) => java.lang.String
(java.lang.String. java.lang.String) => java.lang.String
(java.lang.String. java.lang.StringBuffer) => java.lang.String
(java.lang.String. java.lang.StringBuilder) => java.lang.String
(java.lang.String. byte[] int) => java.lang.String
(java.lang.String. byte[] java.lang.String) => java.lang.String
(java.lang.String. byte[] java.nio.charset.Charset) => java.lang.String
(java.lang.String. byte[] int int) => java.lang.String
(java.lang.String. char[] int int) => java.lang.String
(java.lang.String. int[] int int) => java.lang.String
(java.lang.String. byte[] int int int) => java.lang.String
(java.lang.String. byte[] int int java.lang.String) => java.lang.String
(java.lang.String. byte[] int int java.nio.charset.Charset) => java.lang.String
(.charAt java.lang.String int) => char
(.chars java.lang.String) => java.util.stream.IntStream
(.codePointAt java.lang.String int) => int
(.codePointBefore java.lang.String int) => int
(.codePointCount java.lang.String int int) => int
(.codePoints java.lang.String) => java.util.stream.IntStream
(.compareTo java.lang.String java.lang.Object) => int
(.compareTo java.lang.String java.lang.String) => int
(.compareToIgnoreCase java.lang.String java.lang.String) => int
(.concat java.lang.String java.lang.String) => java.lang.String
(.contains java.lang.String java.lang.CharSequence) => boolean
(.contentEquals java.lang.String java.lang.CharSequence) => boolean
(.contentEquals java.lang.String java.lang.StringBuffer) => boolean
(.describeConstable java.lang.String) => java.util.Optional
(.endsWith java.lang.String java.lang.String) => boolean
(.equals java.lang.String java.lang.Object) => boolean
(.equalsIgnoreCase java.lang.String java.lang.String) => boolean
(.formatted java.lang.String java.lang.Object[]) => java.lang.String
(.getBytes java.lang.String) => byte[]
(.getBytes java.lang.String java.lang.String) => byte[]
(.getBytes java.lang.String java.nio.charset.Charset) => byte[]
(.getBytes java.lang.String int int byte[] int) => nil
(.getChars java.lang.String int int char[] int) => nil
(.getClass java.lang.String) => java.lang.Class
(.hashCode java.lang.String) => int
(.indent java.lang.String int) => java.lang.String
(.indexOf java.lang.String int) => int
(.indexOf java.lang.String java.lang.String) => int
(.indexOf java.lang.String int int) => int
(.indexOf java.lang.String java.lang.String int) => int
(.intern java.lang.String) => java.lang.String
(.isBlank java.lang.String) => boolean
(.isEmpty java.lang.String) => boolean
(.lastIndexOf java.lang.String int) => int
(.lastIndexOf java.lang.String java.lang.String) => int
(.lastIndexOf java.lang.String int int) => int
(.lastIndexOf java.lang.String java.lang.String int) => int
(.length java.lang.String) => int
(.lines java.lang.String) => java.util.stream.Stream
(.matches java.lang.String java.lang.String) => boolean
(.notify java.lang.String) => nil
(.notifyAll java.lang.String) => nil
(.offsetByCodePoints java.lang.String int int) => int
(.regionMatches java.lang.String int java.lang.String int int) => boolean
(.regionMatches java.lang.String boolean int java.lang.String int int) => boolean
(.repeat java.lang.String int) => java.lang.String
(.replace java.lang.String char char) => java.lang.String
(.replace java.lang.String java.lang.CharSequence java.lang.CharSequence) => java.lang.String
(.replaceAll java.lang.String java.lang.String java.lang.String) => java.lang.String
(.replaceFirst java.lang.String java.lang.String java.lang.String) => java.lang.String
(.resolveConstantDesc java.lang.String java.lang.invoke.MethodHandles$Lookup) => java.lang.String
(.resolveConstantDesc java.lang.String java.lang.invoke.MethodHandles$Lookup) => java.lang.Object
(.split java.lang.String java.lang.String) => java.lang.String[]
(.split java.lang.String java.lang.String int) => java.lang.String[]
(.startsWith java.lang.String java.lang.String) => boolean
(.startsWith java.lang.String java.lang.String int) => boolean
(.strip java.lang.String) => java.lang.String
(.stripIndent java.lang.String) => java.lang.String
(.stripLeading java.lang.String) => java.lang.String
(.stripTrailing java.lang.String) => java.lang.String
(.subSequence java.lang.String int int) => java.lang.CharSequence
(.substring java.lang.String int) => java.lang.String
(.substring java.lang.String int int) => java.lang.String
(.toCharArray java.lang.String) => char[]
(.toLowerCase java.lang.String) => java.lang.String
(.toLowerCase java.lang.String java.util.Locale) => java.lang.String
(.toString java.lang.String) => java.lang.String
(.toUpperCase java.lang.String) => java.lang.String
(.toUpperCase java.lang.String java.util.Locale) => java.lang.String
(.transform java.lang.String java.util.function.Function) => java.lang.Object
(.translateEscapes java.lang.String) => java.lang.String
(.trim java.lang.String) => java.lang.String
(.wait java.lang.String) => nil
(.wait java.lang.String long) => nil
(.wait java.lang.String long int) => nil
nil
user=> (= (java-members "foo") (java-members java.lang.String))
true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment