Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add namespace sorting #251

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
87 changes: 87 additions & 0 deletions cljfmt/src/cljfmt/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -400,13 +400,98 @@
(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- unpack-metadata-and-uneval [nodes]
(mapcat (fn [node]
(case (n/tag node)
:meta (rest (n/children node))
:uneval (n/children node)
[node])) nodes))

(defn- node-sort-string [nodes]
(-> (remove (some-fn n/comment? n/whitespace?) (unpack-metadata-and-uneval nodes))
(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
: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 +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
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
164 changes: 163 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,153 @@
(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]"
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]))"]
["(ns foo.bar"
" (:require"
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]"
" [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"
" ;; comment above"
" [b]))"]
["(ns foo.bar"
" (:require"
" #?(:clj [a.b.c])"
" #?(:clj [clojure.java.io :as io])"
" ^:keep"
" [a.b :as b] ;; comment behind"
" ;; comment above"
" [b]"
" c"
" [foo]))"]
{:sort-ns-references? true}))
(is (reformats-to?
["(ns foo.bar"
" (:require [foo] c [a.b :as b] #_[a.d] [b]))"]
["(ns foo.bar"
" (:require [a.b :as b] #_[a.d] [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})))