Skip to content

Commit

Permalink
Add ns reference sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
weavejester authored and or committed Jul 2, 2022
1 parent 066be05 commit c686ae5
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 5 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ selectively enabled or disabled:
true if cljfmt should break hashmaps onto multiple lines. This will
convert `{:a 1 :b 2}` to `{:a 1\n:b 2}`. Defaults to false.

* `:sort-ns-references?` -
true if cljfmt should sort `ns` blocks `:require`, `:require-macros`, `:use`,
and `:import` by namespace. This will convert `(ns (:require [c] b [a.b.c]))`
to `(ns (:require [a.b.c] b [c]))`. Defaults to false.

You can also configure the behavior of cljfmt:

* `:paths` - determines which directories to include in the
Expand Down Expand Up @@ -220,7 +225,7 @@ Indentation types are:
* `:inner` -
two character indentation applied to form arguments at a depth
relative to a form symbol

* `:block` -
first argument aligned indentation applied to form arguments at form
depth 0 for a symbol
Expand All @@ -235,9 +240,9 @@ Form depth is the nested depth of any element within the form.
A contrived example will help to explain depth:

```clojure
(foo
(foo
bar
(baz
(baz
(qux plugh)
corge)
(grault
Expand Down
81 changes: 81 additions & 0 deletions cljfmt/src/cljfmt/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -400,13 +400,92 @@
(defn remove-multiple-non-indenting-spaces [form]
(transform form edit-all non-indenting-whitespace? replace-with-one-space))

(def ^:private ns-reference-symbols
#{:import :require :require-macros :use})

(defn- ns-reference? [zloc]
(and (z/list? zloc)
(some-> zloc z/up ns-form?)
(-> zloc z/sexpr first ns-reference-symbols)))

(defn- re-indexes [re s]
(let [matcher #?(:clj (re-matcher re s)
:cljs (js/RegExp. (.-source re) "g"))
next-match #?(:clj #(when (.find matcher)
[(.start matcher) (.end matcher)])
:cljs #(when-let [result (.exec matcher s)]
[(.-index result) (.-lastIndex matcher)]))]
(take-while some? (repeatedly next-match))))

(defn- re-seq-matcher [re charmap coll]
{:pre (every? charmap coll)}
(let [s (apply str (map charmap coll))
v (vec coll)]
(for [[start end] (re-indexes re s)]
{:value (subvec v start end)
:start start
:end end})))

(defn- find-elements-with-comments [nodes]
(re-seq-matcher #"(CNS*)*E(S*C)?"
#(case (n/tag %)
(:whitespace :comma) \S
:comment \C
:newline \N
\E)
nodes))

(defn- splice-into [coll splices]
(letfn [(splice [v i splices]
(when-let [[{:keys [value start end]} & splices] (seq splices)]
(lazy-cat (subvec v i start) value (splice v end splices))))]
(splice (vec coll) 0 splices)))

(defn- add-newlines-after-comments [nodes]
(mapcat #(if (n/comment? %) [% (n/newlines 1)] [%]) nodes))

(defn- remove-newlines-after-comments [nodes]
(mapcat #(when-not (and %1 (n/comment? %1) (n/linebreak? %2)) [%2])
(cons nil nodes)
nodes))

(defn- sort-node-arguments-by [f nodes]
(let [nodes (add-newlines-after-comments nodes)
args (rest (find-elements-with-comments nodes))
sorted (sort-by f (map :value args))]
(->> sorted
(map #(assoc %1 :value %2) args)
(splice-into nodes)
(remove-newlines-after-comments))))

(defn- update-children [zloc f]
(let [node (z/node zloc)]
(z/replace zloc (n/replace-children node (f (n/children node))))))

(defn- nodes-string [nodes]
(apply str (map n/string nodes)))


(defn- node-sort-string [nodes]
(-> (remove (some-fn n/comment? n/whitespace?) nodes)
(nodes-string)
(str/replace #"[\[\]\(\)\{\}]|\^[:\w-]+" "")
(str/trim)))

(defn sort-arguments [zloc]
(update-children zloc #(sort-node-arguments-by node-sort-string %)))

(defn sort-ns-references [form]
(transform form edit-all ns-reference? sort-arguments))

(def default-options
{:indentation? true
:insert-missing-whitespace? true
:remove-consecutive-blank-lines? true
:remove-multiple-non-indenting-spaces? false
:remove-surrounding-whitespace? true
:remove-trailing-whitespace? true
:sort-ns-references? false
:split-keypairs-over-multiple-lines? false
:indents default-indents
:alias-map {}})
Expand All @@ -417,6 +496,8 @@
([form opts]
(let [opts (merge default-options opts)]
(-> form
(cond-> (:sort-ns-references? opts)
sort-ns-references)
(cond-> (:split-keypairs-over-multiple-lines? opts)
(split-keypairs-over-multiple-lines))
(cond-> (:remove-consecutive-blank-lines? opts)
Expand Down
15 changes: 14 additions & 1 deletion cljfmt/src/cljfmt/main.clj
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,17 @@
{:project-root "."
:file-pattern #"\.clj[csx]?$"
:ansi? true
:parallel? false})
:parallel? false
:indentation? true
:insert-missing-whitespace? true
:remove-multiple-non-indenting-spaces? false
:remove-surrounding-whitespace? true
:remove-trailing-whitespace? true
:remove-consecutive-blank-lines? true
:sort-ns-references? false
:split-keypairs-over-multiple-lines? false
:indents cljfmt/default-indents
:alias-map {}})

(defn merge-default-options [options]
(-> (merge default-options options)
Expand Down Expand Up @@ -201,6 +211,9 @@
[nil "--[no-]remove-consecutive-blank-lines"
:default (:remove-consecutive-blank-lines? cljfmt/default-options)
:id :remove-consecutive-blank-lines?]
[nil "--[no-]sort-ns-references"
:default (:sort-ns-references? default-options)
:id :sort-ns-references?]
[nil "--[no-]split-keypairs-over-multiple-lines"
:default (:split-keypairs-over-multiple-lines? cljfmt/default-options)
:id :split-keypairs-over-multiple-lines?]])
Expand Down
186 changes: 185 additions & 1 deletion cljfmt/test/cljfmt/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,19 @@
":three four}"]
["{:one two #_comment"
" :three four}"]
{:split-keypairs-over-multiple-lines? true})))
{:split-keypairs-over-multiple-lines? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require c b a))"]
["(ns foo.bar"
" (:require c b a))"]
{}))
(is (reformats-to?
["(ns foo.bar"
" (:require c b a))"]
["(ns foo.bar"
" (:require a b c))"]
{:sort-ns-references? true})))

(deftest test-parsing
(is (reformats-to?
Expand Down Expand Up @@ -1236,3 +1248,175 @@
(is (= ((wrap-normalize-newlines identity) "foo\nbar\nbaz") "foo\nbar\nbaz"))
(is (= ((wrap-normalize-newlines identity) "foo\r\nbar\r\nbaz") "foo\r\nbar\r\nbaz"))
(is (= ((wrap-normalize-newlines identity) "foobarbaz") "foobarbaz")))

(deftest test-sort-ns-references
(is (reformats-to?
["(ns foo"
" (:require b c a))"]
["(ns foo"
" (:require a b c))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo"
" (:require b"
" c"
" a))"]
["(ns foo"
" (:require a"
" b"
" c))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require"
" c"
" \"d\""
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]))"]
["(ns foo.bar"
" (:require"
" \"d\""
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]"
" c))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require c"
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]"
" \"d\"))"]
["(ns foo.bar"
" (:require \"d\""
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]"
" c))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require [c]"
" [a.b :as b] ;; aabb"
" ;; bbb"
" b))"]
["(ns foo.bar"
" (:require [a.b :as b] ;; aabb"
" ;; bbb"
" b"
" [c]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require"
" [foo]c"
" [a.b :as b]))"]
["(ns foo.bar"
" (:require"
" [a.b :as b] c"
" [foo]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require"
" [foo]c"
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]))"]
["(ns foo.bar"
" (:require"
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]"
" c"
" [foo]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require"
" [foo]c"
" #?(:clj [clojure.java.io :as io])"
" #?(:clj [a.b.c])"
" ^:keep"
" [a.b :as b] #_[comment behind] #_another"
" ;; comment above"
" [b]))"]
["(ns foo.bar"
" (:require"
" #?(:clj [a.b.c])"
" #?(:clj [clojure.java.io :as io])"
" ^:keep"
" [a.b :as b] #_[comment behind] #_another"
" ;; comment above"
" [b]"
" c"
" [foo]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require"
" [foo]"
" c"
" #?(:clj [clojure.java.io :as io])"
" #?(:clj [a.b.c])"
" ^:keep"
" [a.b :as b] #_[comment behind] #_another"
" ;; comment above"
" [b]))"]
["(ns foo.bar"
" (:require"
" #?(:clj [a.b.c])"
" #?(:clj [clojure.java.io :as io])"
" ^:keep"
" [a.b :as b] #_[comment behind] #_another"
" ;; comment above"
" [b]"
" c"
" [foo]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require [foo] c [a.b :as b] #_[comment behind] [b]))"]
["(ns foo.bar"
" (:require [a.b :as b] #_[comment behind] [b] c [foo]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (;; foobar"
" :require [foo] c))"]
["(ns foo.bar"
" (;; foobar"
" :require c [foo]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require"
" [c]"
" [a.b :as b] ;; aabb"
" ;; bbb"
" b))"]
["(ns foo.bar"
" (:require"
" [a.b :as b] ;; aabb"
" ;; bbb"
" b"
" [c]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require"
" ^{:foo :bar"
" :bar :foo}"
" [c]"
" [a.b :as b]"
" b))"]
["(ns foo.bar"
" (:require"
" [a.b :as b]"
" b"
" ^{:foo :bar"
" :bar :foo}"
" [c]))"]
{:sort-ns-references? true})))

0 comments on commit c686ae5

Please sign in to comment.