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

Implement reload-all by regex #14

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Implement reload-all by regex
jpmonettas committed Jun 26, 2024
commit 0327f5abc98dcd89cbbfccda6165b5e2579eac6c
4 changes: 3 additions & 1 deletion fixtures/core_test/a.clj
Original file line number Diff line number Diff line change
@@ -5,4 +5,6 @@
d
[z :as-alias z])
(:import
[java.io File]))
[java.io File]))

(def a nil)
4 changes: 3 additions & 1 deletion fixtures/core_test/b.clj
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
(ns b)
(ns b)

(def b nil)
4 changes: 3 additions & 1 deletion fixtures/core_test/c.clj
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
(ns c
(:require e))
(:require e))

(def c nil)
4 changes: 3 additions & 1 deletion fixtures/core_test/d.clj
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
(ns d
(:require e))
(:require e))

(def d nil)
4 changes: 3 additions & 1 deletion fixtures/core_test/e.clj
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
(ns e)
(ns e)

(def e nil)
4 changes: 3 additions & 1 deletion fixtures/core_test/f.clj
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
(ns f
(:require d g))
(:require d g))

(def f nil)
4 changes: 3 additions & 1 deletion fixtures/core_test/g.clj
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
(ns g)
(ns g)

(def g nil)
4 changes: 3 additions & 1 deletion fixtures/core_test/h.clj
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
(ns h
(:require e))
(:require e))

(def h nil)
2 changes: 2 additions & 0 deletions fixtures/core_test/i.clj
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
(ns i
(:require j))

(def i nil)
2 changes: 2 additions & 0 deletions fixtures/core_test/j.clj
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
(ns j
(:require k))

(def j nil)
2 changes: 2 additions & 0 deletions fixtures/core_test/k.clj
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
(ns k)

(def k nil)
2 changes: 2 additions & 0 deletions fixtures/core_test/l.clj
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
(ns l)

(def l nil)
71 changes: 65 additions & 6 deletions src/clj_reload/core.clj
Original file line number Diff line number Diff line change
@@ -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 [<string> ...] - where to look for files
; :files #"<regex>" - 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))
10 changes: 5 additions & 5 deletions src/clj_reload/keep.clj
Original file line number Diff line number Diff line change
@@ -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)
114 changes: 68 additions & 46 deletions src/clj_reload/parse.clj
Original file line number Diff line number Diff line change
@@ -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 {<symbol> 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 ...}"
6 changes: 3 additions & 3 deletions src/clj_reload/util.clj
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 22 additions & 0 deletions test/clj_reload/core_test.clj
Original file line number Diff line number Diff line change
@@ -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 '[]