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

fn-deps for lambdas #141

Merged
merged 18 commits into from
Jan 8, 2022
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## master (unreleased)

## 0.9.0 (2022-01-8)

* [#51](https://github.com/clojure-emacs/orchard/issues/51): extend find-usages
* `orchard.xref/fn-deps` now also finds anonymous function dependencies
* added `orchard.xref/fn-deps-class` as a lower level API so you can still get the main functions deps only

## 0.8.0 (2021-12-15)

### Changes
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,25 @@ Just add `orchard` as a dependency and start hacking.
Consult the [API documentation](https://cljdoc.org/d/cider/orchard/CURRENT) to get a better idea about the
functionality that's provided.

#### Using `enrich-classpath` for best results
### Using `enrich-classpath` for best results

There are features that Orchard intends to provide (especially, those related to Java interaction) which need to assume a pre-existing initial classpath that already has various desirable items, such as the JDK sources, third-party sources, special jars such as `tools` (for JDK8), a given project's own Java sources... all that is a domain in itself, which is why our [enrich-classpath](https://github.com/clojure-emacs/enrich-classpath) project does it.

For getting the most out of Orchard, it is therefore recommended/necessary to use `enrich-classpath`. Please refer to its installation/usage instructions.

### xref/fn-deps and xref/fn-refs limitations

These functions use a Clojure compiler implementation detail to find references to other function var dependencies.

You can find a more in-depth explanation in this [post](https://lukas-domagala.de/blog/clojure-analysis-and-introspection.html).

The important implications from this are:

* very fast
* functions marked with meta :inline will not be found (inc, +, ...)
* redefining function vars that include lambdas will still return the dependencies of the old plus the new ones
([explanation](https://lukas-domagala.de/blog/clojure-compiler-class-cache.html))

## Configuration options

So far, Orchard follows these options, which can be specified as Java system properties
Expand All @@ -98,6 +111,12 @@ So far, Orchard follows these options, which can be specified as Java system pro
* `"-Dorchard.initialize-cache.silent=true"` (default: `true`)
* if `false`, the _class info cache_ initialization may print warnings (possibly spurious ones).

## Tests and formatting

To run the CI tasks locally use:

`make test cljfmt kondo eastwood`

## History

Originally [SLIME][] was the most
Expand Down
98 changes: 83 additions & 15 deletions src/orchard/xref.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,66 @@
references."
{:added "0.5"}
(:require
[clojure.repl :as repl]
[clojure.string :as str]
[orchard.query :as q]))

(defn- as-val
(defn- to-fn
"Convert `thing` to a function value."
[thing]
(cond
(var? thing) (var-get thing)
(symbol? thing) (var-get (find-var thing))
(fn? thing) thing))

(defn- fn-name [^java.lang.Class f]
(-> f .getName repl/demunge symbol))

(defn fn-deps-class
"Returns a set with all the functions invoked by `v`.
`v` can be a function class or a symbol."
{:added "0.8"}
[v]
(let [^java.lang.Class v (if (class? v)
v
(eval v))]
(into #{} (keep (fn [^java.lang.reflect.Field f]
(or (and (identical? clojure.lang.Var (.getType f))
(java.lang.reflect.Modifier/isPublic (.getModifiers f))
(java.lang.reflect.Modifier/isStatic (.getModifiers f))
(-> f .getName (.startsWith "const__"))
(.get f (fn-name v)))
nil))
(.getDeclaredFields v)))))

(def ^:private class-cache
"Reference to Clojures class cache.
This holds of classes compiled by the Clojure compiler,
one class per function and one per repl eval.
This field is package private, so it has to be set to
accessible otherwise an IllegalAccess exception would
be thrown."
(let [classCache* (.getDeclaredField clojure.lang.DynamicClassLoader "classCache")]
(.setAccessible classCache* true)
(.get classCache* clojure.lang.DynamicClassLoader)))
Cyrik marked this conversation as resolved.
Show resolved Hide resolved

(defn fn-deps
"Returns a set with all the functions invoked by `val`.
`val` can be a function value, a var or a symbol."
"Returns a set with all the functions invoked inside `v` or any contained anonymous functions.
`v` can be a function value, a var or a symbol.
If a function was defined multiple times, old lambda deps will
be returned.
This does not return functions marked with meta :inline like `+`
since they are already compiled away at this point."
{:added "0.5"}
[val]
(let [val (as-val val)]
(set (some->> val class .getDeclaredFields
(keep (fn [^java.lang.reflect.Field f]
(or (and (identical? clojure.lang.Var (.getType f))
(java.lang.reflect.Modifier/isPublic (.getModifiers f))
(java.lang.reflect.Modifier/isStatic (.getModifiers f))
(-> f .getName (.startsWith "const__"))
(.get f val))
nil)))))))
[v]
(when-let [^clojure.lang.AFn v (to-fn v)]
(let [f-class-name (-> v .getClass .getName)]
;; this uses the implementation detail that the clojure compiler always
;; prefixes names of lambdas with the name of its surrounding function class
(into #{} (comp (filter (fn [[k _v]] (clojure.string/includes? k f-class-name)))
(map (fn [[_k value]] (.get ^java.lang.ref.Reference value)))
(mapcat fn-deps-class))
class-cache))))

(defn- fn->sym
"Convert a function value `f` to symbol."
Expand All @@ -45,8 +81,40 @@
"Find all functions that refer `var`.
`var` can be a function value, a var or a symbol."
{:added "0.5"}
[var]
(let [var (as-var var)
[v]
(let [var (as-var v)
all-vars (q/vars {:ns-query {:project? true} :private? true})
deps-map (zipmap all-vars (map fn-deps all-vars))]
(map first (filter (fn [[_k v]] (contains? v var)) deps-map))))

(comment
Copy link
Member

Choose a reason for hiding this comment

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

Are those some functions you used for test purposes? Might be good idea to add a comment explaining this.

(defn oom []
(try (let [memKiller (java.util.ArrayList.)]
(loop [free 10000000]
(.add memKiller (object-array free))
(.get memKiller 0)
(recur 100000 #_(if (< (Math/abs (.. Runtime (getRuntime) (freeMemory))) Integer/MAX_VALUE)
(Math/abs (.. Runtime (getRuntime) (freeMemory)))
Integer/MAX_VALUE))))
(catch OutOfMemoryError _
(println "freed"))))

(fn-deps #'fn-refs)
(fn-deps #'orchard.xref/fn-deps)
(fn-refs #'orchard.xref/fn->sym)

(let [f-class-name "orchard.xref" #_(-> orchard.xref/fn-deps .getClass .getName)
classes (into #{} (comp (filter (fn [[k _v]] (clojure.string/includes? k f-class-name)))
(map (fn [[_k v]] (.get ^java.lang.ref.Reference v))))
class-cache)]
classes)

(let [memKiller (java.util.ArrayList.)]
(loop [free (.. Runtime (getRuntime) (freeMemory))]
(.add memKiller (object-array free))
(recur (.. Runtime (getRuntime) (freeMemory)))))
(oom)
(Math/min 1 2)
(def vars (q/vars {:ns-query {:project? true} :private? true}))

(map fn-deps vars))
Copy link
Member

Choose a reason for hiding this comment

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

The final newline seems to be missing.

15 changes: 10 additions & 5 deletions test/orchard/xref_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
[clojure.test :refer [deftest is testing]]
[orchard.xref :as xref]))

(defn- times [a b]
(* a b))

(defn- dummy-fn [_x]
(map #(* % 2) (filter even? (range 1 10))))
(map #(times % 2) (filter even? (range 1 10))))

(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})))
#'clojure.core/even? #'clojure.core/range #'orchard.xref-test/times})))
(testing "with a var"
(is (= (xref/fn-deps #'dummy-fn)
#{#'clojure.core/map #'clojure.core/filter
#'clojure.core/even? #'clojure.core/range})))
#'clojure.core/even? #'clojure.core/range #'orchard.xref-test/times})))
(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}))))
#'clojure.core/even? #'clojure.core/range #'orchard.xref-test/times}))))

;; The mere presence of this var can reproduce a certain issue. See:
;; https://github.com/clojure-emacs/orchard/issues/135#issuecomment-939731698
Expand All @@ -38,4 +41,6 @@
(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))))
(is (contains? (into #{} (xref/fn-refs #'map)) #'orchard.xref-test/dummy-fn)))
(testing "with a lambda"
vemv marked this conversation as resolved.
Show resolved Hide resolved
(is (contains? (into #{} (xref/fn-refs #'times)) #'orchard.xref-test/dummy-fn))))