diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index d3214d7..26df064 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -44,4 +44,9 @@ :unresolved-var {:exclude [org.httpkit.client/get]} ;; for integration tests: :unused-namespace {:exclude [sample.unused.namespace - "more.unused.namespaces*"]}}} + "more.unused.namespaces*"]}} + + :config-in-ns {;; for integration tests: + refactor-nrepl.ns.libspec-allowlist-test + {:linters {:unused-namespace {:exclude [from-config-in-ns + "^from-config-in-ns.re*"]}}}}} diff --git a/CHANGELOG.md b/CHANGELOG.md index db04e98..632937d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +* [#387](https://github.com/clojure-emacs/refactor-nrepl/issues/387): extend clj-kondo `:unused-namespace` integration. Now namespace local configuration is also taken into account. + `:libspec-whitelist` can be augmented for particular namespace by: + * Adding `:unused-namespace` linter configuration under `:config-in-ns` key in clj-kondo's config file. Like so: `:config-in-ns {example.target.ns {:linters {:unused-namespace {:exclude [ns.to.exclude]}}}}` + * Adding `:unused-namespace` linter configuration under `:clj-kondo/config` key in metadata or attribute map of namespace. Like so: `(ns example.target.ns {:clj-kondo/config '{:linters {:unused-namespace {:exclude [ns.to.exclude]}}}})` + ## 3.5.5 * [#385](https://github.com/clojure-emacs/refactor-nrepl/pull/385): only `suggest-aliases` that are valid symbols. diff --git a/src/refactor_nrepl/core.clj b/src/refactor_nrepl/core.clj index f7a8d07..60e75c9 100644 --- a/src/refactor_nrepl/core.clj +++ b/src/refactor_nrepl/core.clj @@ -365,9 +365,14 @@ (StringReader.) (PushbackReader.) parse/read-ns-decl) - ns-meta (meta (second ns-form))] + ns-meta (meta (second ns-form)) + attr-map (->> ns-form + (drop 2) + (take 2) + (some (fn [e] (when (map? e) e))))] {:top-level-meta ns-meta - :gc-methods-meta (extract-gen-class-methods-meta ns-form)})) + :gc-methods-meta (extract-gen-class-methods-meta ns-form) + :attr-map attr-map})) (defn read-ns-form-with-meta "Read the ns form found at PATH. diff --git a/src/refactor_nrepl/ns/libspec_allowlist.clj b/src/refactor_nrepl/ns/libspec_allowlist.clj index 59b2452..1719236 100644 --- a/src/refactor_nrepl/ns/libspec_allowlist.clj +++ b/src/refactor_nrepl/ns/libspec_allowlist.clj @@ -6,20 +6,33 @@ (clojure.lang IFn) (java.util.regex Pattern))) -(defn- libspec-allowlist* [] - (let [kondo-file (io/file ".clj-kondo" "config.edn") - exclude (when (.exists kondo-file) - (try - (-> kondo-file slurp read-string :linters :unused-namespace :exclude) - (catch Exception e - (when (System/getenv "CI") - (throw e)))))] - (->> exclude - (mapv (fn [entry] - (if (symbol? entry) - (str "^" (Pattern/quote (str entry)) "$") - entry))) - (into (:libspec-whitelist config/*config*))))) +(defn maybe-unwrap-quote [obj] + (if (and (seq? obj) (= 'quote (first obj))) + (second obj) + obj)) + +(defn- kondo-excludes [{namespace-name :ns + ns-meta :meta}] + (let [linter-path [:linters :unused-namespace :exclude] + local-config (-> ns-meta :clj-kondo/config maybe-unwrap-quote) + kondo-file (io/file ".clj-kondo" "config.edn") + config (when (.exists kondo-file) + (try + (-> kondo-file slurp read-string) + (catch Exception e + (when (System/getenv "CI") + (throw e)))))] + (reduce into [(get-in config linter-path) + (get-in config (into [:config-in-ns namespace-name] linter-path)) + (get-in local-config linter-path)]))) + +(defn- libspec-allowlist* [current-ns] + (->> (kondo-excludes current-ns) + (mapv (fn [entry] + (if (symbol? entry) + (str "^" (Pattern/quote (str entry)) "$") + entry))) + (into (:libspec-whitelist config/*config*)))) (def ^:private ^:dynamic ^IFn *libspec-allowlist* nil) @@ -32,9 +45,9 @@ with clj-kondo's `:unused-namespace` config. Uses a memoized version if available." - [] - (or (some-> *libspec-allowlist* .invoke) - (libspec-allowlist*))) + [current-ns] + (or (some-> *libspec-allowlist* (.invoke current-ns)) + (libspec-allowlist* current-ns))) (defmacro with-memoized-libspec-allowlist "Memoizes the libspec-allowlist internals while `body` is executing. diff --git a/src/refactor_nrepl/ns/ns_parser.clj b/src/refactor_nrepl/ns/ns_parser.clj index 247710e..32a845a 100644 --- a/src/refactor_nrepl/ns/ns_parser.clj +++ b/src/refactor_nrepl/ns/ns_parser.clj @@ -112,12 +112,16 @@ (parse-clj-or-cljs-ns path :cljs))) (defn parse-ns [path-or-file] - (assoc - (if (core/cljc-file? (io/file path-or-file)) - (parse-cljc-ns path-or-file) - (parse-clj-or-cljs-ns path-or-file)) - :ns (second (core/read-ns-form-with-meta path-or-file)) - :source-dialect (core/file->dialect path-or-file))) + (let [[_ namespace-name :as ns-form] (core/read-ns-form-with-meta path-or-file) + select-metadata (juxt :top-level-meta :attr-map) + metadata (-> ns-form meta select-metadata)] + (assoc + (if (core/cljc-file? (io/file path-or-file)) + (parse-cljc-ns path-or-file) + (parse-clj-or-cljs-ns path-or-file)) + :ns namespace-name + :meta (apply merge metadata) + :source-dialect (core/file->dialect path-or-file)))) (def ^:dynamic *read-ns-form-with-meta* core/read-ns-form-with-meta) diff --git a/src/refactor_nrepl/ns/prune_dependencies.clj b/src/refactor_nrepl/ns/prune_dependencies.clj index f533099..caaad83 100644 --- a/src/refactor_nrepl/ns/prune_dependencies.clj +++ b/src/refactor_nrepl/ns/prune_dependencies.clj @@ -124,11 +124,11 @@ ;; pruned. (defn libspec-should-never-be-pruned? "Should `libspec` never be pruned away by the `clean-ns` op?" - [libspec] + [current-ns libspec] (let [ns-name (str (:ns libspec))] (boolean (some (fn [^String pattern] (-> pattern re-pattern (re-find ns-name))) - (libspec-allowlist/libspec-allowlist))))) + (libspec-allowlist/libspec-allowlist current-ns))))) (defn imports->namespaces "Given a collection of `:import` clauses, returns the set of namespaces denoted by them, as symbols. @@ -185,7 +185,7 @@ (defn- prune-libspec [symbols-in-file current-ns imports-namespaces libspec] (cond - (libspec-should-never-be-pruned? libspec) + (libspec-should-never-be-pruned? current-ns libspec) libspec (imports-contain-libspec? imports-namespaces (:ns libspec)) diff --git a/test-resources/ns_with_meta_and_attr_map.clj b/test-resources/ns_with_meta_and_attr_map.clj new file mode 100644 index 0000000..6da686e --- /dev/null +++ b/test-resources/ns_with_meta_and_attr_map.clj @@ -0,0 +1,2 @@ +(ns ^:foo ^:bar ^{:a 1, :b 2} ns-with-meta-and-attr-map {:c 3, :d 4} + (:require [clojure.test :as t])) diff --git a/test/refactor_nrepl/core_test.clj b/test/refactor_nrepl/core_test.clj index 12b9578..74187b9 100644 --- a/test/refactor_nrepl/core_test.clj +++ b/test/refactor_nrepl/core_test.clj @@ -77,3 +77,13 @@ (testing "`:as-alias` directives are kept" (is (= '(ns as-alias (:require [foo :as-alias f])) (sut/read-ns-form-with-meta "test-resources/as_alias.clj"))))) + +(deftest extract-ns-meta + (testing "namespace metadata and attr-map are extracted and merged together" + (let [ns-meta (sut/extract-ns-meta (slurp "test-resources/ns_with_meta_and_attr_map.clj"))] + (is (= {:a 1 + :b 2 + :bar true + :foo true} + (:top-level-meta ns-meta))) + (is (= {:c 3, :d 4} (:attr-map ns-meta)))))) diff --git a/test/refactor_nrepl/ns/libspec_allowlist_test.clj b/test/refactor_nrepl/ns/libspec_allowlist_test.clj index f1ad696..e85404a 100644 --- a/test/refactor_nrepl/ns/libspec_allowlist_test.clj +++ b/test/refactor_nrepl/ns/libspec_allowlist_test.clj @@ -1,8 +1,13 @@ (ns refactor-nrepl.ns.libspec-allowlist-test + {:clj-kondo/config '{:linters {:unused-namespace {:exclude [from-ns-attr-map + "from-ns-attr-map.re*"]}}}} (:require [clojure.test :refer [are deftest is testing]] [refactor-nrepl.ns.libspec-allowlist :as sut] - [refactor-nrepl.ns.prune-dependencies :as prune-dependencies])) + [refactor-nrepl.ns.prune-dependencies :as prune-dependencies] + [refactor-nrepl.ns.ns-parser :refer [parse-ns]])) + +(def this-ns (parse-ns "test/refactor_nrepl/ns/libspec_allowlist_test.clj")) (deftest libspec-allowlist-test (testing "Takes into account refactor-nrepls own config, and .clj-kondo/config files alike, @@ -12,26 +17,50 @@ merging their results" ;; from our .clj-kondo file - symbols become quoted patterns: "^\\Qsample.unused.namespace\\E$" ;; from our .clj-kondo file - strings have 'regex' semantics so are kept as-is: - "more.unused.namespaces*"] + "more.unused.namespaces*" + ;; from our .clj-konfo file, namespace local configuration + "^\\Qfrom-config-in-ns\\E$" + "^from-config-in-ns.re*" + ;; from attr-map of this ns + "^\\Qfrom-ns-attr-map\\E$" + "from-ns-attr-map.re*"] - (sut/libspec-allowlist))) + (sut/libspec-allowlist this-ns))) - (is (every? string? (sut/libspec-allowlist)) + (is (every? string? (sut/libspec-allowlist this-ns)) "Items coming from different sources all have the same class, ensuring they will be treated homogeneously by refactor-nrepl") (testing "`libspec-should-never-be-pruned?` is integrated with clj-kondo logic, -effecively parsing its config into well-formed regexes" +effectively parsing its config into well-formed regexes" (are [input expected] (= expected - (prune-dependencies/libspec-should-never-be-pruned? {:ns input})) + (prune-dependencies/libspec-should-never-be-pruned? this-ns {:ns input})) 'sample.unused.namespace true 'Asample.unused.namespace false 'sample.unused.namespaceB false 'more.unused.namespaces true 'more.unused.namespacessss true - 'more.unused.namespac false)) + 'more.unused.namespac false + 'from-config-in-ns true + 'from-config-in-ns.core false + 'from-config-in-ns.re true + 'from-config-in-ns.re.f true + 'Bfrom-config-in-ns.re false + 'from-ns-attr-map true + 'Efrom-ns-attr-map false + 'from-ns-attr-map.re true + 'from-ns-attr-map.re.f true)) (testing "Always returns a sequence, memoized or not" (is (seq (sut/with-memoized-libspec-allowlist - (sut/libspec-allowlist)))) - (is (seq (sut/libspec-allowlist)))))) + (sut/libspec-allowlist this-ns)))) + (is (seq (sut/libspec-allowlist this-ns)))))) + +(deftest maybe-unwrap-quote + (testing "unwraps object if it is quoted, returns it unchanged otherwise" + (are [input expected] (= expected (sut/maybe-unwrap-quote input)) + ''{:a 1} {:a 1} + {:b 2} {:b 2} + nil nil + ''{} {} + {} {})))