Skip to content

Commit a973bd1

Browse files
authored
Support Clojure 1.12 array class tokens (#290)
* Support Clojure 1.12 array class tokens The new `foo/3` was reader-illegal prior to 1.12. Bring in and customize some tools.reader bits and bobs to support reading this new syntax. Clojure's `symbol` allows us to construct technically illegal symbols, we take advantage of this to test and support this new syntax in any version of Clojure via, for example, `(symbol "foo" "3")`. Closes #279 * fix typo [skip ci]
1 parent b7dd989 commit a973bd1

File tree

7 files changed

+134
-7
lines changed

7 files changed

+134
-7
lines changed

CHANGELOG.adoc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ A release with known breaking changes is marked with:
2424
* `sexpr` now 1) expands tag metadata to its long form 2) throws on invalid metadata
2525
https://github.com/clj-commons/rewrite-clj/issues/280[#280]
2626
(@lread)
27-
* Add support for Clojure 1.12 vector metadata
27+
* Add support for Clojure 1.12 vector metadata (ex. `(^[double] String/valueOf 3)` )
28+
https://github.com/clj-commons/rewrite-clj/issues/279[#279]
29+
(@lread)
30+
* Add support for Clojure 1.12 array class syntax (ex. `byte/3`)
2831
https://github.com/clj-commons/rewrite-clj/issues/279[#279]
2932
(@lread)
3033
* `rewrite-clj.paredit/barf-forward` on zipper created with `:track-position? true` now correctly barfs when current node has children

src/rewrite_clj/interop.cljc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,12 @@
3939
[data]
4040
#?(:clj (instance? clojure.lang.IMeta data)
4141
:cljs (implements? IWithMeta data)))
42+
43+
(defn numeric?
44+
"Checks whether a given character is numeric
45+
46+
Cribbed from clojure/cljs.tools.reader.impl.util."
47+
[^Character ch]
48+
(when ch
49+
#?(:clj (Character/isDigit ch)
50+
:cljs (gstring/isNumeric ch))))

src/rewrite_clj/parser/token.cljc

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
(ns ^:no-doc rewrite-clj.parser.token
2-
(:require [rewrite-clj.node.token :as ntoken]
2+
(:require [rewrite-clj.interop :as interop]
3+
[rewrite-clj.node.token :as ntoken]
34
[rewrite-clj.reader :as r]))
45

56
#?(:clj (set! *warn-on-reflection* true))
@@ -31,18 +32,51 @@
3132
(ntoken/token-node value value-string)
3233
(let [s (str value-string suffix)]
3334
(ntoken/token-node
34-
(r/string->edn s)
35+
(r/read-symbol s)
3536
s)))))
3637

38+
(defn- number-literal?
39+
"Checks whether the reader is at the start of a number literal
40+
41+
Cribbed and adapted from clojure.tools.reader.impl.commons"
42+
[[c1 c2]]
43+
(or (interop/numeric? c1)
44+
(and (or (identical? \+ c1) (identical? \- c1))
45+
(interop/numeric? c2))))
46+
3747
(defn parse-token
38-
"Parse a single token."
48+
"Parse a single token. For example: symbol, number or character."
3949
[#?(:cljs ^not-native reader :default reader)]
4050
(let [first-char (r/next reader)
4151
s (->> (if (= first-char \\)
4252
(read-to-char-boundary reader)
4353
(read-to-boundary reader))
4454
(str first-char))
45-
v (r/string->edn s)]
55+
v (if (or (= first-char \\) ;; character like \n or \newline
56+
(= first-char \#) ;; something like ##Inf, ##Nan
57+
(number-literal? s))
58+
(r/string->edn s)
59+
(r/read-symbol s))]
4660
(if (symbol? v)
4761
(symbol-node reader v s)
4862
(ntoken/token-node v s))))
63+
64+
(comment
65+
(require '[clojure.tools.reader.reader-types :as rt])
66+
67+
(parse-token (rt/string-push-back-reader "foo"))
68+
;; => {:value foo, :string-value "foo", :map-qualifier nil}
69+
70+
(parse-token (rt/string-push-back-reader "42"))
71+
;; => {:value 42, :string-value "42"}
72+
73+
(parse-token (rt/string-push-back-reader "+42"))
74+
;; => {:value 42, :string-value "+42"}
75+
76+
(parse-token (rt/string-push-back-reader "\\newline"))
77+
;; => {:value \newline, :string-value "\\newline"}
78+
79+
(parse-token (rt/string-push-back-reader "##Inf"))
80+
;; => {:value ##Inf, :string-value "##Inf"}
81+
82+
:eoc)

src/rewrite_clj/reader.cljc

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns ^:no-doc rewrite-clj.reader
22
(:refer-clojure :exclude [peek next])
3-
(:require #?@(:clj [[clojure.java.io :as io]])
3+
(:require [clojure.string :as string]
4+
#?@(:clj [[clojure.java.io :as io]])
45
[clojure.tools.reader.edn :as edn]
56
[clojure.tools.reader.impl.commons :as reader-impl-commons]
67
[clojure.tools.reader.impl.errors :as reader-impl-errors]
@@ -203,6 +204,54 @@
203204
(reader-impl-errors/throw-invalid reader :keyword token)))
204205
(reader-impl-errors/throw-single-colon reader))))
205206

207+
(defn- parse-symbol
208+
"Parses a string into a vector of the namespace and symbol
209+
210+
Cribbed from clojure/cljs.tools.reader.impl.commons/parse-symbol merging clj and cljs fns into single implementation
211+
Added in equivalent of TRDR-73 patch to allow array class symbols (e.g. foobar/3)."
212+
[^String token]
213+
(when-not (or (identical? "" token)
214+
(string/ends-with? token ":")
215+
(string/starts-with? token "::"))
216+
(if-let [ns-idx (string/index-of token "/")]
217+
(let [ns (subs token 0 ns-idx)
218+
ns-idx (inc ns-idx)]
219+
(when-not (== ns-idx (count token))
220+
(let [sym (subs token ns-idx)]
221+
(cond
222+
(contains? #{"1" "2" "3" "4" "5" "6" "7" "8" "9"} sym)
223+
[ns sym]
224+
225+
(and (not (interop/numeric? (nth sym 0)))
226+
(not (identical? "" sym))
227+
(not (string/ends-with? ns ":"))
228+
(or (identical? sym "/")
229+
(nil? (string/index-of sym "/"))))
230+
[ns sym]))))
231+
(when (or (identical? token "/")
232+
(nil? (string/index-of token "/")))
233+
[nil token]))))
234+
235+
(defn read-symbol
236+
"Return symbol parsed from `token`.
237+
238+
Cribbed from clojure/cljs.tools.reader.edn/read-symbol and - adapted to work on string"
239+
[^String token]
240+
(case token
241+
;; special symbols
242+
"nil" nil
243+
"true" true
244+
"false" false
245+
"/" '/
246+
247+
(or (when-let [p (parse-symbol token)]
248+
(symbol (p 0) (p 1)))
249+
;; Throw in same way that tools.reader would when reading a string
250+
;; for exeption compatibility. Some users, like clojure-lsp, currently rely
251+
;; on parsing exception strings. A user having to resort
252+
;; to parsing exception messages is not great, but a separate issue.
253+
(reader-impl-errors/throw-invalid nil :symbol token))))
254+
206255
;; ## Reader Types
207256

208257
;;

test/rewrite_clj/node/coercer_test.cljc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
;; symbol/keyword/string/...
2525
['symbol :token :symbol]
26+
[(symbol "ns" "3") :token :symbol]
2627
['namespace/symbol :token :symbol]
2728
[:keyword :token :keyword]
2829
[:1.5.1 :token :keyword]

test/rewrite_clj/node_test.cljc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
["[1 2 3]" :vector :seq true]
2525
["\"string\"" :token :string true]
2626
["symbol" :token :symbol true]
27+
["foo/3" :token :symbol true]
2728
["43" :token :token true]
2829
["#_ nope" :uneval :uneval false]
2930
[" " :whitespace :whitespace false]]]
@@ -201,3 +202,21 @@
201202
(is (= 'my.current.ns (custom-mqn-sexpr "#:: {:a 1 :b 2}")))
202203
(is (= 'my.aliased.ns (custom-mqn-sexpr "#::my-alias {:a 1 :b 2}")))
203204
(is (= 'my-alias-nope-unresolved (custom-mqn-sexpr "#::my-alias-nope {:a 1 :b 2}"))))))
205+
206+
(deftest t-create-nodes
207+
;; The *-node creation fns were initially created to serve the parser, and are therefore not
208+
;; always entirely user-friendly, but are often useful.
209+
;; Here's a good place to add some tests as we see fit.
210+
(doseq
211+
[[n expected-node-type expected-str expected-sexpr]
212+
[[(n/token-node (symbol "foobar" "3")) :symbol "foobar/3" (symbol "foobar" "3")]
213+
[(n/token-node (symbol "sym")) :symbol "sym" 'sym]
214+
[(n/token-node '\newline "\\newline") :token "\\newline" '\newline]
215+
[(n/token-node 42) :token "42" 42]
216+
[(n/token-node +42 "+42") :token "+42" 42]
217+
[(n/token-node "foo") :token "\"foo\"" "foo"]]]
218+
219+
(testing expected-str
220+
(is (= expected-node-type (proto/node-type n)))
221+
(is (= expected-str (n/string n)))
222+
(is (= expected-sexpr (n/sexpr n))))))

test/rewrite_clj/parser_test.cljc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
["\\\\" \\]
5353
["\\a" \a]
5454
["\\space" \space]
55+
["\\u2202" \u2202]
5556
["\\'" \']
5657
[":1.5" :1.5]
5758
[":1.5.0" :1.5.0]
@@ -69,6 +70,15 @@
6970
(is (= s (node/string n)))
7071
(is (= r (node/sexpr n))))))
7172

73+
(deftest t-parsing-clojure-1-12-array-class-tokens
74+
(doseq [dimension (range 1 10)]
75+
(let [s (str "foobar/" dimension)
76+
n (p/parse-string s)]
77+
(is (= :token (node/tag n)))
78+
(is (= s (node/string n)))
79+
(is (= (symbol "foobar" (str dimension))
80+
(node/sexpr n))))))
81+
7282
(deftest t-parsing-garden-selectors
7383
;; https://github.com/noprompt/garden
7484
(doseq [[s expected-r]
@@ -564,6 +574,8 @@
564574
["\"abc" #".*EOF.*"]
565575
["#\"abc" #".*Unexpected EOF.*"]
566576
["(def x 0]" #".*Unmatched delimiter.*"]
577+
["foobar/0" #".*Invalid symbol: foobar/0"] ;; array class dimension can be 1 to 9
578+
["foobar/11" #".*Invalid symbol: foobar/11"] ;; array class dimension can be 1 to 9
567579
["##wtf" #".*Invalid token: ##wtf"]
568580
["#=" #".*:eval node expects 1 value.*"]
569581
["#^" #".*:meta node expects 2 values.*"]
@@ -581,7 +593,7 @@
581593
["#:: token" #".*namespaced map expects a map*"]
582594
["#::alias [a]" #".*namespaced map expects a map*"]
583595
["#:prefix [a]" #".*namespaced map expects a map.*"]]]
584-
(is (thrown-with-msg? ExceptionInfo p (p/parse-string s)))))
596+
(is (thrown-with-msg? ExceptionInfo p (p/parse-string s)) s)))
585597

586598
(deftest t-sexpr-exceptions
587599
(doseq [s ["#_42" ;; reader ignore/discard

0 commit comments

Comments
 (0)