Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Java class and method resolution to info middleware. #40

Merged
merged 1 commit into from
Apr 25, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
language: clojure
script: lein2 with-profile dev test
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests all pass. The :dev dependencies just weren't being added to the classpath for the Travis CI build.

jdk:
- openjdk6
- openjdk7
Expand Down
16 changes: 15 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
:dependencies [[org.clojure/clojure "1.5.1"]
[compliment "0.0.3"]
[cljs-tooling "0.1.2"]
[org.ow2.asm/asm "5.0.2"]
[org.ow2.asm/asm-commons "5.0.2"]
[org.tcrawley/dynapath "0.2.3"]
[org.clojure/tools.nrepl "0.2.3"]
[org.clojure/java.classpath "0.2.0"]
[org.clojure/tools.namespace "0.2.3"]]
Expand All @@ -15,7 +18,18 @@
cider.nrepl.middleware.info/wrap-info
cider.nrepl.middleware.inspect/wrap-inspect
]}

;; The "sources" jar should be the same version as Clojure,
;; but bad sources jars were deployed to the Maven Central
;; "releases" repo, so for the moment, use sources from
;; "snapshot" builds to run tests.
;; See http://dev.clojure.org/jira/browse/CLJ-1161.
:repositories [["snapshots"
"http://oss.sonatype.org/content/repositories/snapshots"]]
:dependencies [[org.clojure/clojure "1.5.2-SNAPSHOT"
:classifier "sources"]]

;; Wait til 1.5 comes out for a fix to cljs dep
;:plugins [[com.cemerick/austin "0.1.5"]]
;; :plugins [[com.cemerick/austin "0.1.5"]]
}}
)
11 changes: 9 additions & 2 deletions src/cider/nrepl/middleware/info.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:require [clojure.string :as s]
[clojure.java.io :as io]
[cider.nrepl.middleware.util.cljs :as cljs]
[cider.nrepl.middleware.util.java :as java]
[cider.nrepl.middleware.util.misc :as u]
[clojure.repl]
[cljs-tooling.info :as cljs-info]
Expand Down Expand Up @@ -63,12 +64,18 @@
(let [x (cljs-info/info env symbol ns)]
(select-keys x [:file :line :ns :doc :column :name :arglists])))

(defn info-java
[class member]
(apply java/method-info (map str [class member])))

(defn info
[{:keys [ns symbol] :as msg}]
[{:keys [ns symbol class member] :as msg}]
(let [[ns symbol] (map u/as-sym [ns symbol])]
(if-let [cljs-env (cljs/grab-cljs-env msg)]
(info-cljs cljs-env symbol ns)
(info-clj ns symbol))))
(if ns
(info-clj ns symbol)
(info-java class member)))))

(defn resource-path
"If it's a resource, return a tuple of the relative path and the full resource path."
Expand Down
96 changes: 96 additions & 0 deletions src/cider/nrepl/middleware/util/java.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
(ns cider.nrepl.middleware.util.java
"Source info for Java classes and members"
{:author "Jeff Valk"}
(:require [clojure.java.io :as io]
[clojure.string :as str]
[dynapath.util :as dp])
(:import (org.objectweb.asm ClassReader ClassVisitor MethodVisitor Opcodes)
(org.objectweb.asm.commons Method)))

;;; ## Source Files
;; Java source files are resolved from the classpath. For library dependencies,
;; this simply entails having the corresponding source artifacts in the
;; project's dev dependencies. The core Java API classes are the exception to
;; this, since these are external to lein/maven dependency management. For
;; these, we look at the JDK itself and add the source classpath entry manually.

;; This is used only to add text resources to the classpath. Issues of class
;; dependency are not in play.
(defn add-classpath!
"Similar to the deprecated `clojure.core/add-classpath`, adds the URL to the
classpath and returns it if successful, or nil otherwise."
[url]
(let [classloader (->> (.. Thread currentThread getContextClassLoader)
(iterate #(.getParent %))
(take-while identity)
(filter dp/addable-classpath?)
(last))]
(when (dp/add-classpath-url classloader url)
url)))

(def jdk-sources
"The JDK sources path. If available, this is added to the classpath. By
convention, this is the file `src.zip` in the root of the JDK directory
(parent of the `java.home` JRE directory)."
(let [zip (-> (io/file (System/getProperty "java.home"))
(.getParentFile)
(io/file "src.zip"))]
(when (.canRead zip)
(add-classpath! (io/as-url zip)))))

(defn java-source
"Return the relative .java source path for the top-level class name."
[class]
(-> (str/replace class #"\$.*" "")
(str/replace "." "/")
(str ".java")))


;;; ## Class/Method Info

;; Getting class member info (i.e. method names, argument/return types, etc) is
;; straightforward using reflection...but this leaves us without source
;; location. For line numbers, we either have to parse bytecode, or the .java
;; source itself. For present purposes, we'll take the former approach.

;; N.b. Java class LineNumberTables map source lines to bytecode instructions.
;; This means that the line numbers returned for methods are generally the first
;; executable line of the method, rather than its declaration. (Constructors are
;; an exception to this.)

(defn class-info
"For the named class, return Java source and member info, including line
numbers. Methods are indexed first by name, and then by argument types to
list all overloads."
[class]
(let [methods (atom {})
typesym #(-> % .getClassName symbol)
visitor (proxy [ClassVisitor] [Opcodes/ASM4]
(visitMethod [access name desc signature exceptions]
(let [m (Method. name desc)
ret (typesym (.getReturnType m))
args (mapv typesym (.getArgumentTypes m))]
(proxy [MethodVisitor] [Opcodes/ASM4]
(visitLineNumber [line label]
(when-not (get-in @methods [name args])
(swap! methods assoc-in [name args]
{:ret ret :args args :line line})))))))]
(try (-> (ClassReader. class)
(.accept visitor 0))
{:class class
:methods @methods
:file (java-source class)}
(catch Exception _))))

(defn method-info
"Return Java member info, including argument (type) lists and source
file/line. If the member is an overloaded method, line number is that of the
first overload."
[class method]
(let [c (class-info class)]
(when-let [m (get-in c [:methods method])]
(-> (dissoc c :methods)
(assoc :method method
:line (->> (vals m) (map :line) sort first)
:arglists (keys m)
:doc nil)))))
4 changes: 3 additions & 1 deletion test/cider/nrepl/middleware/test_info.clj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
(is (info/info-clj 'cider.nrepl.middleware.info 'io))

(is (info/info-clj 'cider.nrepl.middleware.info 'info-clj))


(is (info/info-java "clojure.lang.Atom" "swap"))

(is (info/format-response (info/info-clj 'cider.nrepl.middleware.info 'clojure.core)))

(is (-> (info/info-clj 'cider.nrepl.middleware.info 'clojure.core)
Expand Down
54 changes: 54 additions & 0 deletions test/cider/nrepl/middleware/util/java_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
(ns cider.nrepl.middleware.util.java-test
(:require [cider.nrepl.middleware.util.java :refer :all]
[clojure.test :refer :all]
[clojure.java.io :as io]))

(deftest test-sources
(let [resolve-src (comp io/resource java-source)]
(testing "Source file resolution"
(testing "from Clojure"
(is (resolve-src "clojure.lang.Compiler"))
(is (resolve-src "clojure.lang.PersistentHashSet")))
(testing "from JDK"
(when jdk-sources ; system dependent; not managed by project.clj
(is (resolve-src "java.lang.String"))
(is (resolve-src "java.util.regex.Matcher"))))
(testing "for non-existent classes"
(is (not (resolve-src "not.actually.AClass")))))))

(deftest test-class-info
(let [c1 (class-info "clojure.lang.PersistentHashMap")
c2 (class-info "clojure.lang.PersistentHashMap$ArrayNode")
c3 (class-info "not.actually.AClass")]
(testing "Class"
(testing "source file"
(is (string? (:file c1)))
(is (io/resource (:file c1))))
(testing "source file for nested class"
(is (string? (:file c2)))
(is (io/resource (:file c2))))
(testing "method info"
(is (map? (:methods c1)))
(is (every? map? (vals (:methods c1))))
(is (apply (every-pred :ret :args :line)
(mapcat vals (vals (:methods c1))))))
(testing "that doesn't exist"
(is (nil? c3))))))

(deftest test-method-info
(let [m1 (method-info "clojure.lang.PersistentHashMap" "assoc")
m2 (method-info "clojure.lang.PersistentHashMap" "nothing")
m3 (method-info "not.actually.AClass" "nada")]
(testing "Method"
(testing "source file"
(is (string? (:file m1)))
(is (io/resource (:file m1))))
(testing "line number"
(is (number? (:line m1))))
(testing "arglists"
(is (seq? (:arglists m1)))
(is (every? vector? (:arglists m1))))
(testing "that doesn't exist"
(is (nil? m2)))
(testing "in a class that doesn't exist"
(is (nil? m3))))))