Skip to content

Commit

Permalink
Add optional sorting for ns references
Browse files Browse the repository at this point in the history
Add :sort-ns-references? option (default false) that will sort the
references (requires, includes etc.) contained in a ns form. Sorting is
alphanumeric and ignores brackets and metadata when determing order.

Closes #251.
  • Loading branch information
weavejester committed Jul 25, 2022
1 parent 7a20ec9 commit 23daaf0
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 0 deletions.
87 changes: 87 additions & 0 deletions cljfmt/src/cljfmt/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,90 @@
(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- remove-node-metadata [nodes]
(mapcat #(if (= (n/tag %) :meta)
(rest (n/children %))
[%])
nodes))

(defn- node-sort-string [nodes]
(-> (remove (some-fn n/comment? n/whitespace?) nodes)
(remove-node-metadata)
(nodes-string)
(str/replace #"[\[\]\(\)\{\}]" "")
(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
Expand All @@ -408,6 +492,7 @@
:remove-surrounding-whitespace? true
:remove-trailing-whitespace? true
:split-keypairs-over-multiple-lines? false
:sort-ns-references? false
:indents default-indents
:alias-map {}})

Expand All @@ -417,6 +502,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
68 changes: 68 additions & 0 deletions cljfmt/test/cljfmt/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1236,3 +1236,71 @@
(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"
" (:require b"
" [c :as d]"
" a))"]
["(ns foo"
" (:require a"
" b"
" [c :as d]))"]
{: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"
" [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"
" [c]"
" ^:keep a"
" #?(:clj d)"
" ^{:x 1} b))"]
["(ns foo.bar"
" (:require"
" #?(:clj d)"
" ^:keep a"
" ^{:x 1} b"
" [c]))"]
{:sort-ns-references? true})))

1 comment on commit 23daaf0

@or
Copy link
Contributor

@or or commented on 23daaf0 Jul 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! It still needs to be documented.

And it needs to be added to cljfmt.main:

   [nil "--[no-]sort-ns-references"
    :default (:sort-ns-references? cljfmt/default-options)
    :id :sort-ns-references?]

Please sign in to comment.