diff --git a/fixtures/core_test/a.clj b/fixtures/core_test/a.clj index 0cd8acf..ffaa67b 100644 --- a/fixtures/core_test/a.clj +++ b/fixtures/core_test/a.clj @@ -5,4 +5,6 @@ d [z :as-alias z]) (:import - [java.io File])) + [java.io File])) + +(def a nil) diff --git a/fixtures/core_test/b.clj b/fixtures/core_test/b.clj index 411c59a..bacf4ea 100644 --- a/fixtures/core_test/b.clj +++ b/fixtures/core_test/b.clj @@ -1 +1,3 @@ -(ns b) \ No newline at end of file +(ns b) + +(def b nil) diff --git a/fixtures/core_test/c.clj b/fixtures/core_test/c.clj index 7b6340a..5a841ca 100644 --- a/fixtures/core_test/c.clj +++ b/fixtures/core_test/c.clj @@ -1,2 +1,4 @@ (ns c - (:require e)) \ No newline at end of file + (:require e)) + +(def c nil) diff --git a/fixtures/core_test/d.clj b/fixtures/core_test/d.clj index bb98b0b..165a74f 100644 --- a/fixtures/core_test/d.clj +++ b/fixtures/core_test/d.clj @@ -1,2 +1,4 @@ (ns d - (:require e)) \ No newline at end of file + (:require e)) + +(def d nil) diff --git a/fixtures/core_test/e.clj b/fixtures/core_test/e.clj index 62f86fb..978fd2c 100644 --- a/fixtures/core_test/e.clj +++ b/fixtures/core_test/e.clj @@ -1 +1,3 @@ -(ns e) \ No newline at end of file +(ns e) + +(def e nil) diff --git a/fixtures/core_test/f.clj b/fixtures/core_test/f.clj index 6aaa368..88dc9b3 100644 --- a/fixtures/core_test/f.clj +++ b/fixtures/core_test/f.clj @@ -1,2 +1,4 @@ (ns f - (:require d g)) \ No newline at end of file + (:require d g)) + +(def f nil) diff --git a/fixtures/core_test/g.clj b/fixtures/core_test/g.clj index 94720c1..6026927 100644 --- a/fixtures/core_test/g.clj +++ b/fixtures/core_test/g.clj @@ -1 +1,3 @@ -(ns g) \ No newline at end of file +(ns g) + +(def g nil) diff --git a/fixtures/core_test/h.clj b/fixtures/core_test/h.clj index 986c3bd..e67e122 100644 --- a/fixtures/core_test/h.clj +++ b/fixtures/core_test/h.clj @@ -1,2 +1,4 @@ (ns h - (:require e)) \ No newline at end of file + (:require e)) + +(def h nil) diff --git a/fixtures/core_test/i.clj b/fixtures/core_test/i.clj index 952a6bc..cbdb8ec 100644 --- a/fixtures/core_test/i.clj +++ b/fixtures/core_test/i.clj @@ -1,2 +1,4 @@ (ns i (:require j)) + +(def i nil) diff --git a/fixtures/core_test/j.clj b/fixtures/core_test/j.clj index a2248ca..7034950 100644 --- a/fixtures/core_test/j.clj +++ b/fixtures/core_test/j.clj @@ -1,2 +1,4 @@ (ns j (:require k)) + +(def j nil) diff --git a/fixtures/core_test/k.clj b/fixtures/core_test/k.clj index bb8ebc3..ae3f9df 100644 --- a/fixtures/core_test/k.clj +++ b/fixtures/core_test/k.clj @@ -1 +1,3 @@ (ns k) + +(def k nil) diff --git a/fixtures/core_test/l.clj b/fixtures/core_test/l.clj index 5ec33e3..79e2e56 100644 --- a/fixtures/core_test/l.clj +++ b/fixtures/core_test/l.clj @@ -1 +1,3 @@ (ns l) + +(def l nil) diff --git a/src/clj_reload/core.clj b/src/clj_reload/core.clj index 6656f04..a87a466 100644 --- a/src/clj_reload/core.clj +++ b/src/clj_reload/core.clj @@ -5,7 +5,9 @@ [clj-reload.util :as util] [clojure.java.io :as io]) (:import - [java.util.concurrent.locks ReentrantLock])) + [java.util.concurrent.locks ReentrantLock] + [java.io File] + [java.net URL])) ; Config :: {:dirs [ ...] - where to look for files ; :files #"" - which files to scan, defaults to #".*\.cljc?" @@ -268,17 +270,23 @@ (dosync (alter @#'clojure.core/*loaded-libs* disj ns))) -(defn- ns-load [ns file keeps] +(defn- ns-load [ns file-or-url keeps] (util/log "Loading" ns #_"from" #_(util/file-path file)) (try (if (empty? keeps) - (util/ns-load-file (slurp file) ns file) - (keep/ns-load-patched ns file keeps)) - + (util/ns-load-file (slurp file-or-url) ns (if (instance? java.io.File file-or-url) + (.getName ^File file-or-url) + (.getFile ^URL file-or-url))) + (if (instance? java.io.File file-or-url) + (keep/ns-load-patched ns file-or-url keeps) + (throw (ex-info "Can only use keeps with java.io.File" {:ns ns + :file-or-url file-or-url + :keeps keeps})))) + (when-some [reload-hook (:reload-hook *config*)] (when-some [reload-fn (ns-resolve (find-ns ns) reload-hook)] (reload-fn))) - + nil (catch Throwable t (util/log " failed to load" ns t) @@ -365,6 +373,57 @@ {:unloaded unloaded :loaded loaded}))))))))) +(defn reload-all + + "Reload all loaded namespaces that contains at least one var, which matches + regex, and any other namespaces depending on them." + + [regex] + + (let [{:keys [no-unload no-reload]} *config* + ;; collect all loaded namespaces resources files paths set + all-paths (->> (all-ns) + (reduce (fn [files ns] + (reduce (fn [files' ns-var] + (if-let [f-path (some-> ns-var meta :file)] + (conj files' f-path) + files')) + files + (vals (ns-interns ns)))) + #{})) + + ;; build the namespaces map + namespaces (reduce (fn [nss path] + (let [res (parse/read-resource path)] + ;; throwables here are caused for example by reading + ;; files which contains "#{`ns 'ns}". This is because + ;; of reading with clj-reload.util/dummy-resolver + (if-not (util/throwable? res) + (merge nss res) + nss))) + {} + all-paths) + reload? #(and + (not (:clj-reload/no-unload (:meta (namespaces %)))) + (not (:clj-reload/no-reload (:meta (namespaces %)))) + (not (no-unload %)) + (not (no-reload %))) + dependees (parse/dependees namespaces) + topo-sort (topo-sort-fn dependees) + matched-ns (->> (keys namespaces) + (filterv (fn [ns] (re-matches regex (name ns)))) + (into #{})) + to-reload (->> (parse/deep-dependees-set matched-ns dependees) + topo-sort) + to-unload (reverse to-reload)] + + (doseq [ns to-unload] + (ns-unload ns)) + + (doseq [ns to-reload] + (doseq [ns-files (get-in namespaces [ns :ns-files])] + (ns-load ns ns-files {}))))) + (defmulti keep-methods (fn [tag] tag)) diff --git a/src/clj_reload/keep.clj b/src/clj_reload/keep.clj index ef70674..304f3b0 100644 --- a/src/clj_reload/keep.clj +++ b/src/clj_reload/keep.clj @@ -175,15 +175,15 @@ (keep-patch ns sym keep))))) (defn ns-load-patched [ns ^File file keeps] - (try + (try (let [content (patch-file (slurp file) (patch-fn ns keeps))] - (util/ns-load-file content ns file)) - - ;; check + (util/ns-load-file content ns (.getName file))) + + ;; check (@#'clojure.core/throw-if (not (find-ns ns)) "namespace '%s' not found after loading '%s'" ns (.getPath file)) - + (finally ;; drop everything in stash (remove-ns 'clj-reload.stash) diff --git a/src/clj_reload/parse.clj b/src/clj_reload/parse.clj index 53eda07..7f69024 100644 --- a/src/clj_reload/parse.clj +++ b/src/clj_reload/parse.clj @@ -2,7 +2,8 @@ (:require [clj-reload.util :as util] [clojure.string :as str] - [clojure.walk :as walk]) + [clojure.walk :as walk] + [clojure.java.io :as io]) (:import [java.io File])) @@ -70,56 +71,67 @@ "Returns { NS} or Exception" ([file] (with-open [rdr (util/file-reader file)] - (try - (read-file rdr file) - (catch Exception e - (util/log "Failed to read" (.getPath ^File file) (.getMessage e)) - (ex-info (str "Failed to read" (.getPath ^File file)) {:file file} e))))) + (read-file rdr file))) ([rdr file] - (loop [ns nil - nses {}] - (let [form (util/read-form rdr) - tag (when (list? form) - (first form))] - (cond - (= :clj-reload.util/eof form) - nses - - (= 'ns tag) - (let [[ns requires] (parse-ns-form form) - requires (disj requires ns)] - (recur ns (update nses ns util/assoc-some - :meta (meta ns) - :requires requires - :ns-files (util/some-set file)))) - - (= 'in-ns tag) - (let [[_ ns] (expand-quotes form)] - (recur ns (update nses ns util/assoc-some - :in-ns-files (util/some-set file)))) - - (and (nil? ns) (#{'require 'use} tag)) - (throw (ex-info (str "Unexpected " tag " before ns definition in " file) {:form form})) - - (#{'require 'use} tag) - (let [requires' (parse-require-form (expand-quotes form)) - requires' (disj requires' ns)] - (recur ns (update-in nses [ns :requires] util/intos requires'))) - - (or + (try + (loop [ns nil + nses {}] + (let [form (util/read-form rdr) + tag (when (list? form) + (first form))] + (cond + (= :clj-reload.util/eof form) + nses + + (= 'ns tag) + (let [[ns requires] (parse-ns-form form) + requires (disj requires ns)] + (recur ns (update nses ns util/assoc-some + :meta (meta ns) + :requires requires + :ns-files (util/some-set file)))) + + (= 'in-ns tag) + (let [[_ ns] (expand-quotes form)] + (recur ns (update nses ns util/assoc-some + :in-ns-files (util/some-set file)))) + + (and (nil? ns) (#{'require 'use} tag)) + (throw (ex-info (str "Unexpected " tag " before ns definition in " file) {:form form})) + + (#{'require 'use} tag) + (let [requires' (parse-require-form (expand-quotes form)) + requires' (disj requires' ns)] + (recur ns (update-in nses [ns :requires] util/intos requires'))) + + (or (= 'defonce tag) (:clj-reload/keep (meta form)) (and - (list? form) - (:clj-reload/keep (meta (second form))))) - (let [[_ name] form] - (recur ns (assoc-in nses [ns :keep name] {:tag tag - :form form}))) - - :else - (recur ns nses)))))) + (list? form) + (:clj-reload/keep (meta (second form))))) + (let [[_ name] form] + (recur ns (assoc-in nses [ns :keep name] {:tag tag + :form form}))) + + :else + (recur ns nses)))) + (catch Exception e + (util/log "Failed to read" (.getPath file) (.getMessage e)) + (ex-info (str "Failed to read" (.getPath file)) {:file file} e))))) + +(defn read-resource -(defn dependees + "Like read-file but will read any resource from res-path. + Returns the same as `read-file`." + + [res-path] + + (if-let [f-url (io/resource res-path)] + (let [rdr (util/string-reader (slurp (io/reader f-url)))] + (read-file rdr f-url)))) + +(defn dependees "Inverts the requies graph. Returns {ns -> #{downstream-ns ...}}" [namespaces] (let [*m (volatile! (transient {}))] @@ -130,6 +142,16 @@ (vswap! *m util/update! to util/conjs from))) (persistent! @*m))) +(defn deep-dependees-set + "Given a set of some initial namespaces and a dependees map like the one + calculated by `dependees`, return a set off all trasitively reached namespaces + including those on the initial set (initial-nss)." + [initial-nss ns-dependees] + (->> initial-nss + (mapcat (fn [ns] + (deep-dependees-set (get ns-dependees ns) ns-dependees) )) + (reduce conj initial-nss))) + (defn transitive-closure "Starts from starts, expands using dependees {ns -> #{downsteram-ns ...}}, returns #{ns ...}" diff --git a/src/clj_reload/util.clj b/src/clj_reload/util.clj index 702be4f..0948592 100644 --- a/src/clj_reload/util.clj +++ b/src/clj_reload/util.clj @@ -118,10 +118,10 @@ (LineNumberingPushbackReader. (StringReader. s))) -(defn ns-load-file [content ns ^File file] - (let [[_ ext] (re-matches #".*\.([^.]+)" (.getName file)) +(defn ns-load-file [content ns file-name] + (let [[_ ext] (re-matches #".*\.([^.]+)" file-name) path (-> ns str (str/replace #"\-" "_") (str/replace #"\." "/") (str "." ext))] - (Compiler/load (StringReader. content) path (.getName file)))) + (Compiler/load (StringReader. content) path file-name))) (defn loader-classpath [] (->> (clojure.lang.RT/baseLoader) diff --git a/test/clj_reload/core_test.clj b/test/clj_reload/core_test.clj index cfa6abf..52d6515 100644 --- a/test/clj_reload/core_test.clj +++ b/test/clj_reload/core_test.clj @@ -59,6 +59,28 @@ (is (= '["Unloading" i f a j h d c l k e "Loading" e k l c d h j a f i] (modify opts 'e 'k 'l))) (is (= '["Unloading" i f a j h d c l k g e b "Loading" b e g k l c d h j a f i] (modify opts 'a 'b 'c 'd 'e 'f 'g 'h 'i 'j 'k 'l))))) +(deftest reload-all-regex-test + (let [*reloads (atom [])] + (apply tu/init '[b e c d h g a f k j i l]) + (binding [util/*log-fn* (fn [& [x :as log-line]] + (when (#{"Loading" "Unloading" } x) + (swap! *reloads conj log-line)))] + (reload/reload-all #"^e$") + (is (= '[("Unloading" f) + ("Unloading" a) + ("Unloading" h) + ("Unloading" d) + ("Unloading" c) + ("Unloading" e) + ("Loading" e) + ("Loading" c) + ("Loading" d) + ("Loading" h) + ("Loading" a) + ("Loading" f)] + + @*reloads))))) + (deftest return-value-ok-test (tu/init 'a 'f 'h) (is (= {:unloaded '[]