-
Notifications
You must be signed in to change notification settings - Fork 177
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
language: clojure | ||
script: lein2 with-profile dev test | ||
jdk: | ||
- openjdk6 | ||
- openjdk7 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)))))) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.