Here is an example of how to read Clojure source code and manipulate it using the tupelo.forest
library. You can see the live code in the GitHub repo.
First set up a test and parse the source:
(dotest
(hid-count-reset)
(with-forest (new-forest)
(let [debug-flg true
edn-str ; Notice that there are 3 forms in the source
(ts/quotes->double
"(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test))
(defn add2 [x y] (+ x y))
(dotest
(is= 5 (spyx (add2 2 3)))
(is= 'abc' (str 'ab' 'c'))) ")
; since `edn/read-string` only returns the next form,
; we wrap all forms in an artifical [:root ...] node
parse-txt (str "[:root " edn-str " ]")
edn-data (edn/read-string parse-txt) ; reads only the first form
At this point, we load the EDN data into a tree using the tupelo.forest
lib:
root-hid (add-tree-edn edn-data) ; add edn data to a single forest tree
ns-path (only (find-paths root-hid [:** {::tf/value (symbol "ns")}])) ; search for the `ns` symbol`
; ns-path looks like `[1038 1009 1002]`, where 1002 points to the `ns` node
ns-hid (xlast ns-path) ; ns-hid is a pointer to the node with `ns`
ns-parent-hid (xsecond (reverse ns-path)) ; get the parent hid (eg 1009)
ns-parent-khids (hid->kids ns-parent-hid) ; vector with `ns` contains 4 kids, of which `ns` is the first
ns-sym-hid (xsecond ns-parent-khids)] ; symbol `tst.demo.core` is the 2nd kid
(when debug-flg
(newline)
(spyx-pretty (hid->bush root-hid))
(newline)
(spyx (hid->node ns-hid))
(spyx (hid->node ns-parent-hid))
(spyx ns-parent-khids)
(newline)
(spyx (hid->node ns-sym-hid)))
The above debug printouts show what is happening. Here is a "bush" view of the tree structure:
(hid->bush root-hid) =>
[{:tag :tupelo.forest/vec, :tupelo.forest/index nil}
[#:tupelo.forest{:value :root, :index 0}]
[{:tag :tupelo.forest/list, :tupelo.forest/index 1}
[#:tupelo.forest{:value ns, :index 0}]
[#:tupelo.forest{:value tst.demo.core, :index 1}]
[{:tag :tupelo.forest/list, :tupelo.forest/index 2}
[#:tupelo.forest{:value :use, :index 0}]
[#:tupelo.forest{:value demo.core, :index 1}]
[#:tupelo.forest{:value tupelo.core, :index 2}]
[#:tupelo.forest{:value tupelo.test, :index 3}]]]
[{:tag :tupelo.forest/list, :tupelo.forest/index 2}
[#:tupelo.forest{:value defn, :index 0}]
[#:tupelo.forest{:value add2, :index 1}]
[{:tag :tupelo.forest/vec, :tupelo.forest/index 2}
[#:tupelo.forest{:value x, :index 0}]
[#:tupelo.forest{:value y, :index 1}]]
[{:tag :tupelo.forest/list, :tupelo.forest/index 3}
[#:tupelo.forest{:value +, :index 0}]
[#:tupelo.forest{:value x, :index 1}]
[#:tupelo.forest{:value y, :index 2}]]]
[{:tag :tupelo.forest/list, :tupelo.forest/index 3}
[#:tupelo.forest{:value dotest, :index 0}]
[{:tag :tupelo.forest/list, :tupelo.forest/index 1}
[#:tupelo.forest{:value is=, :index 0}]
[#:tupelo.forest{:value 5, :index 1}]
[{:tag :tupelo.forest/list, :tupelo.forest/index 2}
[#:tupelo.forest{:value spyx, :index 0}]
[{:tag :tupelo.forest/list, :tupelo.forest/index 1}
[#:tupelo.forest{:value add2, :index 0}]
[#:tupelo.forest{:value 2, :index 1}]
[#:tupelo.forest{:value 3, :index 2}]]]]
[{:tag :tupelo.forest/list, :tupelo.forest/index 2}
[#:tupelo.forest{:value is=, :index 0}]
[#:tupelo.forest{:value "abc", :index 1}]
[{:tag :tupelo.forest/list, :tupelo.forest/index 2}
[#:tupelo.forest{:value str, :index 0}]
[#:tupelo.forest{:value "ab", :index 1}]
[#:tupelo.forest{:value "c", :index 2}]]]]]
and the other debug printouts:
(hid->node ns-hid) => #:tupelo.forest{:khids [], :value ns, :index 0}
(hid->node ns-parent-hid) => {:tupelo.forest/khids [1002 1003 1008], :tag :tupelo.forest/list, :tupelo.forest/index 1}
ns-parent-khids => [1002 1003 1008]
(hid->node ns-sym-hid) => #:tupelo.forest{:khids [], :value tst.demo.core, :index 1}
We then replace the old namespace symbol & convert the forms back to string format:
; replace the old namespace symbol with a new one
(attrs-merge ns-sym-hid {::tf/value (symbol "something.new.core")})
; find the 3 kids of the `:root` node
(let [root-khids (it-> root-hid
(hid->node it)
(grab ::tf/khids it)
(drop 1 it) ; remove :root tag we added
)
kids-edn (forv [hid root-khids] ; still 3 forms to output
(hid->edn hid))
modified-src (with-out-str ; convert EDN forms to a single string
(doseq [form kids-edn]
(prn form)))
; expected-result is the original edn-str but with the new namespace symbol
expected-result (str/replace edn-str "tst.demo.core" "something.new.core")]
(when debug-flg
(spyx (hid->node ns-sym-hid))
(newline)
(spyx-pretty kids-edn)
(newline)
(println :modified-src \newline modified-src))
And the debug printouts show it in action:
(hid->node ns-sym-hid) => #:tupelo.forest{:khids [], :value tst.demo.core, :index 1}
(hid->node ns-sym-hid) => #:tupelo.forest{:khids [], :value something.new.core, :index 1}
kids-edn =>
[(ns something.new.core (:use demo.core tupelo.core tupelo.test))
(defn add2 [x y] (+ x y))
(dotest (is= 5 (spyx (add2 2 3))) (is= "abc" (str "ab" "c")))]
:modified-src
(ns something.new.core (:use demo.core tupelo.core tupelo.test))
(defn add2 [x y] (+ x y))
(dotest (is= 5 (spyx (add2 2 3))) (is= "abc" (str "ab" "c")))
The single unit test verifies the modified source is as expected (ignoring whitespace):
(is-nonblank= modified-src expected-result)))))