From 23daaf0020526aaaaab1cd6363288e79091a97ba Mon Sep 17 00:00:00 2001 From: James Reeves Date: Fri, 3 Jun 2022 17:02:14 +0100 Subject: [PATCH] Add optional sorting for ns references 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. --- cljfmt/src/cljfmt/core.cljc | 87 +++++++++++++++++++++++++++++++ cljfmt/test/cljfmt/core_test.cljc | 68 ++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/cljfmt/src/cljfmt/core.cljc b/cljfmt/src/cljfmt/core.cljc index f4ce4e09..2d60e211 100644 --- a/cljfmt/src/cljfmt/core.cljc +++ b/cljfmt/src/cljfmt/core.cljc @@ -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 @@ -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 {}}) @@ -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) diff --git a/cljfmt/test/cljfmt/core_test.cljc b/cljfmt/test/cljfmt/core_test.cljc index 74356adb..2a18f3fb 100644 --- a/cljfmt/test/cljfmt/core_test.cljc +++ b/cljfmt/test/cljfmt/core_test.cljc @@ -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})))