Skip to content

Commit ffcbf70

Browse files
POC for CLJS support for find-locals
as outlined in #195, see specially #195 (comment) Take aways: - one integration test is duplicated for cljs (together with the source file) - `cljs.analyzer` used directly instead of `jvm.tools.analyzer` -- latter errored with latest cljs, did not investigate further - workarounds needed for - no `end-line`, `end-column` info in CLJS AST (also see https://dev.clojure.org/jira/browse/CLJS-2051) - no `:raw-forms` in cljs AST containing the stages of macro expansion including the original form - :op = `:binding` nodes in CLJS ASTs seems to be missing `:children` entry so the AST can not be walked properly
1 parent c252bd4 commit ffcbf70

File tree

5 files changed

+222
-26
lines changed

5 files changed

+222
-26
lines changed

project.clj

+4-2
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@
3838
[org.clojure/clojurescript "1.10.520"]]}
3939
:dev {:plugins [[jonase/eastwood "0.2.0"]]
4040
:global-vars {*warn-on-reflection* true}
41-
:dependencies [[org.clojure/clojurescript "1.9.946"]
41+
:dependencies [[org.clojure/clojurescript "1.10.520"]
42+
[org.clojure/clojure "1.10.0"]
4243
[cider/piggieback "0.4.0"]
4344
[leiningen-core "2.9.0"]
44-
[commons-io/commons-io "2.6"]]
45+
[commons-io/commons-io "2.6"]
46+
[javax.xml.bind/jaxb-api "2.3.1"]]
4547
:repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]}
4648
:java-source-paths ["test/java"]
4749
:resource-paths ["test/resources"

src/refactor_nrepl/analyzer.clj

+115-11
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@
88
[clojure.tools.analyzer.jvm.utils :as ajutils]
99
[clojure.tools.namespace.parse :refer [read-ns-decl]]
1010
[clojure.walk :as walk]
11-
[refactor-nrepl
12-
[config :as config]]
11+
[refactor-nrepl.config :as config]
1312
[refactor-nrepl.ns.tracker :as tracker]
14-
[clojure.string :as str])
13+
[clojure.string :as str]
14+
[cljs.analyzer :as cljs-ana]
15+
[cljs.util :as cljs-util]
16+
[cljs.compiler :as cljs-comp]
17+
[cljs.env :as cljs-env]
18+
[cider.nrepl.middleware.util.cljs :as cljs])
1519
(:import java.io.PushbackReader
1620
java.util.regex.Pattern))
1721

1822
;;; The structure here is {ns {content-hash ast}}
1923
(def ^:private ast-cache (atom {}))
2024

2125
(defn get-alias [as v]
22-
(cond as (first v)
26+
(cond as (first v)
2327
(= (first v) :as) (get-alias true (rest v))
24-
:else (get-alias nil (rest v))))
28+
:else (get-alias nil (rest v))))
2529

2630
(defn parse-ns
2731
"Returns tuples with the ns as the first element and
@@ -80,6 +84,64 @@
8084
reader/*data-readers* *data-readers*]
8185
(assoc-in (aj/analyze-ns ns (aj/empty-env) opts) [0 :alias-info] aliases)))))
8286

87+
(comment
88+
;; integration test: find-used-locals
89+
;; myast is the parsed ast of com.example.five and com.example.five-cljs respectively
90+
91+
;; cljs
92+
(->> (nth myast 3) :init :methods first :body :ret :bindings first :children)
93+
;; returns nil, but ... :init :form) returns (trim p), strangely :init has the same children as in the clj case
94+
95+
;; clj
96+
(->> (nth myast 3) :init :expr :methods first :body :bindings first :children)
97+
;; returns [:init] .. :init :form) returns (trim p)
98+
99+
)
100+
101+
(defn- repair-binding-children
102+
"Repairs cljs AST by adding `:children` entries to `:binding` AST nodes, see above comment tag."
103+
[]
104+
(fn [env ast opts]
105+
(if (= :let (:op ast))
106+
(update
107+
ast
108+
:bindings
109+
(fn [bindings]
110+
(mapv #(assoc % :children [:init]) bindings)))
111+
ast)))
112+
113+
(defn cljs-analyze-ns
114+
"Returns a sequence of abstract syntax trees for each form in
115+
the namespace."
116+
[ns]
117+
(cljs-env/ensure
118+
(let [f (cljs-util/ns->relpath ns)
119+
res (if (re-find #"^file://" f) (java.net.URL. f) (io/resource f))]
120+
(assert res (str "Can't find " f " in classpath"))
121+
(binding [cljs-ana/*cljs-ns* 'cljs.user
122+
cljs-ana/*cljs-file* (.getPath ^java.net.URL res)
123+
cljs-ana/*passes* [cljs-ana/infer-type cljs-ana/check-invoke-arg-types cljs-ana/ns-side-effects (repair-binding-children)]]
124+
(with-open [r (io/reader res)]
125+
(let [env (cljs-ana/empty-env)
126+
pbr (clojure.lang.LineNumberingPushbackReader. r)
127+
eof (Object.)]
128+
(loop [asts []
129+
r (read pbr false eof false)]
130+
(let [env (assoc env :ns (cljs-ana/get-namespace cljs-ana/*cljs-ns*))]
131+
(if-not (identical? eof r)
132+
(recur (conj asts (cljs-ana/analyze env r)) (read pbr false eof false))
133+
asts)))))))))
134+
135+
(defn cljs-analyze-form [form]
136+
(cljs-env/ensure
137+
(binding [cljs-ana/*cljs-ns* 'cljs.user]
138+
(cljs-ana/analyze (cljs-ana/empty-env) form))))
139+
140+
(defn build-cljs-ast
141+
[file-content]
142+
(let [[ns aliases] (parse-ns file-content)]
143+
(assoc-in (cljs-analyze-ns ns) [0 :alias-info] aliases)))
144+
83145
(defn- cachable-ast [file-content]
84146
(let [[ns aliases] (parse-ns file-content)]
85147
(when ns
@@ -133,16 +195,16 @@
133195
;; The node for ::an-ns-alias/foo, when it appeared as a toplevel form,
134196
;; had nil as position info
135197
(and line end-line column end-column
136-
(and (>= loc-line line)
137-
(<= loc-line end-line)
138-
(>= loc-column column)
139-
(<= loc-column end-column)))))
198+
(<= line loc-line end-line)
199+
(<= column loc-column end-column))))
140200

141-
(defn- normalize-anon-fn-params
201+
(defn normalize-anon-fn-params
142202
"replaces anon fn params in a read form"
143203
[form]
144204
(walk/postwalk
145-
(fn [token] (if (re-matches #"p\d+__\d+#" (str token)) 'p token)) form))
205+
(fn [token] (cond (re-matches #"p\d+__\d+#" (str token)) 'p
206+
(instance? java.util.regex.Pattern token) (str token)
207+
:default token)) form))
146208

147209
(defn- read-when-sexp [form]
148210
(let [f-string (str form)]
@@ -155,13 +217,31 @@
155217
(binding [*read-eval* false]
156218
(let [sexp-sans-comments-and-meta (normalize-anon-fn-params (read-string sexp))
157219
pattern (re-pattern (Pattern/quote (str sexp-sans-comments-and-meta)))]
220+
;; (println "raw-forms" (:raw-forms node))
221+
;; (println "form " (-> (:form node)
222+
;; read-when-sexp
223+
;; normalize-anon-fn-params))
158224
(if-let [forms (:raw-forms node)]
159225
(some #(re-find pattern %)
160226
(map (comp str normalize-anon-fn-params read-when-sexp) forms))
161227
(= sexp-sans-comments-and-meta (-> (:form node)
162228
read-when-sexp
163229
normalize-anon-fn-params))))))
164230

231+
(defn node-for-sexp-cljs?
232+
"Is NODE the ast node for SEXP for cljs?
233+
234+
As `:raw-forms` (stages of macro expansion, including the original form) is not available in cljs AST it does the comparison the other way around. Eg parses `sexp` with the cljs parser and compares that with the `:form` of the AST node."
235+
[sexp node]
236+
(binding [*read-eval* false]
237+
(let [sexp-sans-comments-and-meta-form (:form (cljs-analyze-form (normalize-anon-fn-params (read-string sexp))))
238+
node-form (-> (:form node)
239+
read-when-sexp
240+
normalize-anon-fn-params)]
241+
;; (println "sexp-sans-comments-and-meta" sexp-sans-comments-and-meta-form "types" (map type sexp-sans-comments-and-meta-form))
242+
;; (println "form " node-form "types" (map type node-form))
243+
(= sexp-sans-comments-and-meta-form node-form))))
244+
165245
(defn top-level-form-index
166246
[line column ns-ast]
167247
(->> ns-ast
@@ -170,3 +250,27 @@
170250
(some (partial node-at-loc? line column)))))
171251
(filter #(second %))
172252
ffirst))
253+
254+
(defn node-at-loc-cljs?
255+
"Works around the fact that cljs AST nodes don't have end-line and end-column info in them. This cheat only works for top level forms because after a `clojure.tools.analyzer.ast/nodes` call we can't expect the nodes in the right order."
256+
[^long loc-line ^long loc-column node next-node]
257+
(let [{:keys [^long line ^long column]} (:env node)
258+
env-next-node (:env next-node)
259+
^long end-column (:column env-next-node)
260+
^long end-line (:line env-next-node)]
261+
;; The node for ::an-ns-alias/foo, when it appeared as a toplevel form,
262+
;; had nil as position info
263+
(and line end-line column end-column
264+
(or (< line loc-line end-line)
265+
(and (or (= line loc-line)
266+
(= end-line loc-line))
267+
(<= column loc-column end-column))))))
268+
269+
(defn top-level-form-index-cljs
270+
[line column ns-ast]
271+
(loop [[top-level-ast & top-level-asts-rest] ns-ast
272+
index 0]
273+
(if (or (node-at-loc-cljs? line column top-level-ast (first top-level-asts-rest))
274+
(not (first top-level-asts-rest)))
275+
index
276+
(recur top-level-asts-rest (inc index)))))

src/refactor_nrepl/find/find_locals.clj

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
(ns refactor-nrepl.find.find-locals
22
(:require [clojure.set :as set]
33
[clojure.tools.analyzer.ast :refer [nodes]]
4-
[refactor-nrepl
5-
[analyzer :as ana]
6-
[s-expressions :as sexp]
7-
[core :as core]]))
4+
[refactor-nrepl.analyzer :as ana]
5+
[refactor-nrepl.core :as core]
6+
[refactor-nrepl.s-expressions :as sexp]))
87

98
(defn find-used-locals [{:keys [file ^long line ^long column]}]
109
{:pre [(number? line)
1110
(number? column)
1211
(not-empty file)]}
13-
(core/throw-unless-clj-file file)
12+
;(core/throw-unless-clj-file file)
1413
(let [content (slurp file)
15-
ast (ana/ns-ast content)
14+
cljs? (core/cljs-file? file)
15+
;; fork for cljs using `cljs.analyzer` directly
16+
ast (if cljs? (ana/build-cljs-ast content) (ana/ns-ast content))
1617
sexp (sexp/get-enclosing-sexp content (dec line) (dec column))
18+
;; work around for cljs ASTs not having end-line and end-column info
19+
top-level-form-index-fn (if cljs? ana/top-level-form-index-cljs ana/top-level-form-index)
20+
;; work around the fact that cljs ASTs don't have raw-forms in them. the original form before macro expansion can not be reproduced
21+
node-for-sexp-fn (if cljs? ana/node-for-sexp-cljs? ana/node-for-sexp?)
1722
selected-sexp-node (->> ast
18-
(ana/top-level-form-index line column)
23+
(top-level-form-index-fn line column)
1924
(nth ast)
2025
nodes
21-
(filter (partial ana/node-at-loc? line column))
22-
(filter (partial ana/node-for-sexp? sexp))
26+
((fn [nds]
27+
(if cljs?
28+
nds; can't use `node-at-loc-cljs?` after `nodes`
29+
(filter (partial ana/node-at-loc? line column) nds))))
30+
(filter (partial node-for-sexp-fn sexp))
2331
last)
2432
sexp-locals (->> selected-sexp-node
2533
nodes

test/refactor_nrepl/integration_tests.clj

+27-4
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,35 @@
228228
(is (= (find-unbound :transport transport :file five-file :line 41 :column 4)
229229
'(x y z a b c))))))
230230

231-
(deftest find-unbound-fails-on-cljs
231+
(deftest test-find-used-locals-cljs
232232
(with-testproject-on-classpath
233-
(let [cljs-file (str test-project-dir "/tmp/src/com/example/file.cljs")
233+
(let [five-file (str test-project-dir "/src/com/example/five_cljs.cljs")
234234
transport (connect :port 7777)]
235-
(is (:error (find-unbound :transport transport :file cljs-file
236-
:line 12 :column 6))))))
235+
(is (= (find-unbound :transport transport :file five-file :line 12 :column 6)
236+
'(s)))
237+
;; maybe fails because of a bug in `refactor-nrepl.s-expressions/get-enclosing-sexp`?! which is covered up by the fact that we can prefilter AST nodes with `nodes-at-loc` for clj but for cljs?!
238+
;; (is (= (find-unbound :transport transport :file five-file :line 13 :column 13)
239+
;; '(s sep)))
240+
241+
(is (= (find-unbound :transport transport :file five-file :line 20 :column 16)
242+
'(p)))
243+
(is (= (find-unbound :transport transport :file five-file :line 27 :column 8)
244+
'(sep strings)))
245+
246+
(is (= (find-unbound :transport transport :file five-file :line 34 :column 8)
247+
'(name)))
248+
249+
(is (= (find-unbound :transport transport :file five-file :line 37 :column 5)
250+
'(n)))
251+
(is (= (find-unbound :transport transport :file five-file :line 41 :column 4)
252+
'(x y z a b c))))))
253+
254+
;; (deftest find-unbound-fails-on-cljs
255+
;; (with-testproject-on-classpath
256+
;; (let [cljs-file (str test-project-dir "/tmp/src/com/example/file.cljs")
257+
;; transport (connect :port 7777)]
258+
;; (is (:error (find-unbound :transport transport :file cljs-file
259+
;; :line 12 :column 6))))))
237260

238261
(deftest test-version
239262
(is (= (str (core/version))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
(ns com.example.five-cljs
2+
(:require [clojure.string :refer [join split blank? trim] :as str]))
3+
4+
;; remove parameters to run the tests
5+
(defn fn-with-unbounds [s sep]
6+
(when-not (blank? s)
7+
(-> s (split #" ")
8+
(join sep)
9+
trim)))
10+
11+
(defn orig-fn [s]
12+
(let [sep ";"]
13+
(when-not (blank? s)
14+
(-> s
15+
(split #" ")
16+
((partial join sep))
17+
trim))))
18+
19+
(defn find-in-let [s p]
20+
(let [z (trim p)]
21+
(assoc {:s s
22+
:p p
23+
:z z} :k "foobar")))
24+
25+
(defn threading-macro [strings]
26+
(let [sep ","]
27+
(->> strings
28+
flatten
29+
(join sep))))
30+
31+
(defn repeated-sexp []
32+
(map name [:a :b :c])
33+
(let [name #(str "myname" %)]
34+
(map name [:a :b :c])))
35+
36+
(defn sexp-with-anon-fn [n]
37+
(let [g 5]
38+
(#(+ g %) n)))
39+
40+
(defn many-params [x y z a b c]
41+
(* x y z a b c))
42+
43+
(defn fn-with-default-optmap
44+
[{:keys [foo bar] :or {foo "foo"}}]
45+
[:bar :foo]
46+
(count foo))
47+
48+
(defn fn-with-default-optmap-linebreak
49+
[{:keys [foo
50+
bar]
51+
:or {foo
52+
"foo"}}]
53+
[:bar :foo]
54+
(count foo))
55+
56+
(defn fn-with-let-default-optmap []
57+
(let [{:keys [foo bar] :or {foo "foo"}} (hash-map)]
58+
[:bar :foo]
59+
(count foo)))

0 commit comments

Comments
 (0)