diff --git a/CHANGELOG.md b/CHANGELOG.md index f81b7610..4e279a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## master (unreleased) +### Changes + +- [#176](https://github.com/clojure-emacs/orchard/issues/176): `orchard.xref`: include info for test vars. +- `orchard.xref`: avoid duplicate vars that might appear following REPL re-evaluation. + ## 0.14.1 (2023-08-05) ### Changes diff --git a/src/orchard/xref.clj b/src/orchard/xref.clj index 4663e2b3..a35e2a54 100644 --- a/src/orchard/xref.clj +++ b/src/orchard/xref.clj @@ -8,12 +8,27 @@ [clojure.string :as string] [orchard.query :as q])) +(defn- var->symbol + ;; TODO: use `symbol` once we start targeting Clojure >= 1.10 after CIDER 1.8 is released. + "Normally one could just use `(symbol var-ref)`, + but that doesn't work in older Clojures." + [var-ref] + (let [{:keys [ns name]} (meta var-ref)] + (symbol (str (ns-name ns)) + (str name)))) + +(defn- var->fn [var-ref] + (let [{:keys [test]} (meta var-ref)] + (if (fn? test) + test ;; for deftests, :test metadata contains the actual test implementation, with all the interesting contents. + (var-get var-ref)))) + (defn- to-fn "Convert `thing` to a function value." [thing] (cond - (var? thing) (var-get thing) - (symbol? thing) (var-get (find-var thing)) + (var? thing) (var->fn thing) + (symbol? thing) (var->fn (find-var thing)) (fn? thing) thing)) (defn- fn-name [^java.lang.Class f] @@ -71,12 +86,19 @@ (map (fn [[_k value]] (.get ^java.lang.ref.Reference value))) (mapcat fn-deps-class)) - class-cache)] - ;; if there's no deps the class is most likely AoT compiled, - ;; try to access it directly - (if (empty? deps) - (-> v .getClass fn-deps-class) - deps)))) + class-cache) + result + ;; if there's no deps the class is most likely AoT compiled, + ;; try to access it directly + (if (empty? deps) + (-> v .getClass fn-deps-class) + deps)] + (into #{} + (map resolve) ;; choose the freshest one + ;; group duplicates. This is important + ;; because there can be two seemingly equal #'foo.bar/baz var objects in the result. + ;; That can happen as one re-evaluates code and the old var hasn't been GC'd yet. + (keys (group-by var->symbol result)))))) (defn fn-transitive-deps "Returns a set with all the functions invoked inside `v` or inside those functions. diff --git a/test/orchard/xref_test.clj b/test/orchard/xref_test.clj index 4bfd8b72..57cb7ca4 100644 --- a/test/orchard/xref_test.clj +++ b/test/orchard/xref_test.clj @@ -12,19 +12,38 @@ (defn- dummy-fn [_x] (map #(fn-dep % 2) (filter even? (range 1 10)))) +;; Supports #'fn-deps-test +(deftest sample-test + (is (some? xref/eval-lock)) + (is (some? (xref/fn-refs #'dummy-fn)))) + (deftest fn-deps-test (testing "with a fn value" - (is (= (xref/fn-deps dummy-fn) - #{#'clojure.core/map #'clojure.core/filter - #'clojure.core/even? #'clojure.core/range #'orchard.xref-test/fn-dep}))) + (is (= #{#'clojure.core/map #'clojure.core/filter + #'clojure.core/even? #'clojure.core/range #'orchard.xref-test/fn-dep} + (xref/fn-deps dummy-fn)))) + (testing "with a var" - (is (= (xref/fn-deps #'dummy-fn) - #{#'clojure.core/map #'clojure.core/filter - #'clojure.core/even? #'clojure.core/range #'orchard.xref-test/fn-dep}))) + (is (= #{#'clojure.core/map #'clojure.core/filter + #'clojure.core/even? #'clojure.core/range #'orchard.xref-test/fn-dep} + (xref/fn-deps #'dummy-fn)))) + + (testing "with a var that backs a deftest" + (is (= #{#'orchard.xref-test/dummy-fn + #'orchard.xref/eval-lock + #'clojure.test/do-report + #'clojure.core/cons + #'clojure.core/some? + #'clojure.core/apply + #'orchard.xref/fn-refs + #'clojure.core/list} + (xref/fn-deps #'sample-test)))) + (testing "with a symbol" - (is (= (xref/fn-deps 'orchard.xref-test/dummy-fn) - #{#'clojure.core/map #'clojure.core/filter - #'clojure.core/even? #'clojure.core/range #'orchard.xref-test/fn-dep}))) + (is (= #{#'clojure.core/map #'clojure.core/filter + #'clojure.core/even? #'clojure.core/range #'orchard.xref-test/fn-dep} + (xref/fn-deps 'orchard.xref-test/dummy-fn)))) + (testing "AoT compiled functions return deps" (is (= #{#'clojure.core/conj} (xref/fn-deps reverse))))) @@ -38,19 +57,6 @@ (def yyy (symbol (str (gensym)) (str (gensym)))) -(deftest fn-refs-test - (testing "with a fn value" - (is (= (xref/fn-refs dummy-fn) '())) - (is (contains? (into #{} (xref/fn-refs #'map)) #'orchard.xref-test/dummy-fn))) - (testing "with a var" - (is (= (xref/fn-refs #'dummy-fn) '())) - (is (contains? (into #{} (xref/fn-refs #'map)) #'orchard.xref-test/dummy-fn))) - (testing "with a symbol" - (is (= (xref/fn-refs 'orchard.xref-test/dummy-fn) '())) - (is (contains? (into #{} (xref/fn-refs #'map)) #'orchard.xref-test/dummy-fn))) - (testing "that usage from inside an anonymous function is found" - (is (contains? (into #{} (xref/fn-refs #'fn-dep)) #'orchard.xref-test/dummy-fn)))) - (deftest fn-transitive-deps-test (testing "basics" (let [expected #{#'orchard.xref-test/fn-deps-test #'orchard.xref-test/fn-dep #'clojure.core/even? @@ -59,8 +65,36 @@ (is (contains? expected #'orchard.xref-test/fn-transitive-dep) "Specifically includes `#'fn-transitive-dep`, which is a transitive dep of `#'dummy-fn` (via `#'fn-dep`)") (is (contains? expected #'clojure.core/inc') - "Specifically includes `#'clojure.core/inc'`, which is a transitive dep of `#'dummy-fn` - (via `#'clojure.core/range'`). Unlike other AoT compiled core transitive dependancies + "Specifically includes `#'clojure.core/inc'`, which is a transitive dep of `#'dummy-fn` + (via `#'clojure.core/range'`). Unlike other AoT compiled core transitive dependancies it gets found because its a non `:static` dependancy.") (is (= expected (xref/fn-transitive-deps dummy-fn)))))) + +(deftest fn-refs-test + (testing "with a fn value" + (is (= #{#'orchard.xref-test/fn-deps-test + #'orchard.xref-test/fn-transitive-deps-test + #'orchard.xref-test/sample-test + #'orchard.xref-test/fn-refs-test} + (set (xref/fn-refs dummy-fn)))) + (is (contains? (into #{} (xref/fn-refs #'map)) #'orchard.xref-test/dummy-fn))) + + (testing "with a var" + (is (= #{#'orchard.xref-test/fn-deps-test + #'orchard.xref-test/fn-transitive-deps-test + #'orchard.xref-test/sample-test + #'orchard.xref-test/fn-refs-test} + (set (xref/fn-refs #'dummy-fn)))) + (is (contains? (into #{} (xref/fn-refs #'map)) #'orchard.xref-test/dummy-fn))) + + (testing "with a symbol" + (is (= #{#'orchard.xref-test/fn-deps-test + #'orchard.xref-test/fn-transitive-deps-test + #'orchard.xref-test/sample-test + #'orchard.xref-test/fn-refs-test} + (set (xref/fn-refs 'orchard.xref-test/dummy-fn)))) + (is (contains? (into #{} (xref/fn-refs #'map)) #'orchard.xref-test/dummy-fn))) + + (testing "that usage from inside an anonymous function is found" + (is (contains? (into #{} (xref/fn-refs #'fn-dep)) #'orchard.xref-test/dummy-fn))))