Skip to content

Commit

Permalink
[clj-kondo#708] Unsorted namespaces linter
Browse files Browse the repository at this point in the history
  • Loading branch information
Heliosmaster authored and borkdude committed Jan 28, 2020
1 parent f894f22 commit 75f22a5
Show file tree
Hide file tree
Showing 21 changed files with 134 additions and 81 deletions.
3 changes: 2 additions & 1 deletion .clj-kondo/config.edn
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
:ret :map}}}}
clj-kondo.impl.config #include "../src/clj_kondo/impl/config.types.edn"
clj-kondo.impl.findings #include "../src/clj_kondo/impl/findings.types.edn"}}
:missing-docstring {:level :off}}
:missing-docstring {:level :off}
:unsorted-namespaces {:level :warning}}
:lint-as {me.raynes.conch/programs clojure.core/declare
me.raynes.conch/let-programs clojure.core/let}
:output {:exclude-files ["src/clj_kondo/impl/rewrite_clj_patch.clj"]}}
1 change: 1 addition & 0 deletions doc/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ linting took 10ms, errors: 0, warnings: 0
Some linters are not enabled by default. Right now these linters are:

- `:missing-docstring`: warn when public var doesn't have a docstring.
- `:unsorted-namespaces`: warn when namespaces in `:require` are not sorted.

You can enable these linters by setting the `:level`:

Expand Down
5 changes: 5 additions & 0 deletions doc/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
3. Configuration spread across multiple files is harder to debug.

6) Clj-kondo should be unobtrusive. Users of clj-kondo should not have to change their code only to make the linter happy. Team members who do not wish to use clj-kondo should not be confronted with clj-kondo-related annotations in their code.

## Adding a new linter
If you wish to add a new linter, do not forget to add the appropriate keyword in `clj-kondo.impl.config/default-config`, the map that defines the default configuration.

This is necessary because only the linters with a keyword in the default config appear in the report.

## PR

Expand Down
27 changes: 14 additions & 13 deletions src/clj_kondo/impl/analyzer.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
{:no-doc true}
(:refer-clojure :exclude [ns-name])
(:require
[clj-kondo.impl.analyzer.common :refer [common]]
[clj-kondo.impl.analyzer.compojure :as compojure]
[clj-kondo.impl.analyzer.core-async :as core-async]
[clj-kondo.impl.analyzer.datalog :as datalog]
[clj-kondo.impl.analyzer.namespace :as namespace-analyzer
:refer [analyze-ns-decl]]
[clj-kondo.impl.analyzer.potemkin :as potemkin]
Expand All @@ -23,10 +26,7 @@
[clj-kondo.impl.utils :as utils :refer
[symbol-call node->line parse-string tag select-lang deep-merge one-of
linter-disabled? tag sexpr string-from-token assoc-some ctx-with-bindings]]
[clojure.string :as str]
[clj-kondo.impl.analyzer.datalog :as datalog]
[clj-kondo.impl.analyzer.common :refer [common]]
[clj-kondo.impl.analyzer.compojure :as compojure]))
[clojure.string :as str]))

(set! *warn-on-reflection* true)

Expand Down Expand Up @@ -930,15 +930,16 @@
(let [ns-name (-> ctx :ns :name)
children (:children expr)
require-node (first children)
children (next children)]
(doseq [child children]
(if (= :quote (tag child))
(when-let [libspec-expr (first (:children child))]
(let [analyzed
(namespace-analyzer/analyze-require-clauses ctx ns-name [[require-node [libspec-expr]]])]
(namespace/reg-required-namespaces! ctx ns-name analyzed)))
(analyze-children ctx children)))
(analyze-children ctx children)))
children (next children)
[quoted-children non-quoted-children]
(utils/filter-remove #(= :quote (tag %))
children)
libspecs (map #(first (:children %)) quoted-children)]
(let [analyzed
(namespace-analyzer/analyze-require-clauses ctx ns-name [[require-node libspecs]])]
(namespace/reg-required-namespaces! ctx ns-name analyzed))
;; also analyze children that weren't quoted
(analyze-children ctx non-quoted-children)))

(defn analyze-import-libspec [ctx ns-name expr]
(let [libspec-expr (if (= :quote (tag expr))
Expand Down
13 changes: 7 additions & 6 deletions src/clj_kondo/impl/analyzer/namespace.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
[clj-kondo.impl.analysis :as analysis]
[clj-kondo.impl.analyzer.common :as common]
[clj-kondo.impl.findings :as findings]
[clj-kondo.impl.linters.misc :refer [lint-duplicate-requires!]]
[clj-kondo.impl.metadata :as meta]
[clj-kondo.impl.namespace :as namespace]
[clj-kondo.impl.utils :refer [node->line one-of tag sexpr vector-node
Expand Down Expand Up @@ -214,11 +213,13 @@
:referred #{}})
acc))
{}
analyzed)]
(lint-duplicate-requires! ctx (map (juxt :require-kw :ns) analyzed))
{:required (map (fn [req]
(vary-meta (:ns req)
#(assoc % :alias (:as req)))) analyzed)
analyzed)
required-namespaces (map (fn [req]
(vary-meta (:ns req)
#(assoc % :alias (:as req)))) analyzed)]
(namespace/lint-unsorted-namespaces! ctx required-namespaces)
(namespace/lint-duplicate-requires! ctx (map (juxt :require-kw :ns) analyzed))
{:required required-namespaces
:qualify-ns (reduce (fn [acc sc]
(cond-> (assoc acc (:ns sc) (:ns sc))
(:as sc)
Expand Down
8 changes: 4 additions & 4 deletions src/clj_kondo/impl/analyzer/spec.clj
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
(ns clj-kondo.impl.analyzer.spec
{:no-doc true}
(:require
[clj-kondo.impl.utils :as utils]
[clj-kondo.impl.findings :as findings]
[clj-kondo.impl.namespace :as namespace]
[clj-kondo.impl.linters.keys :as keys]))
[clj-kondo.impl.findings :as findings]
[clj-kondo.impl.linters.keys :as keys]
[clj-kondo.impl.namespace :as namespace]
[clj-kondo.impl.utils :as utils]))

(defn analyze-fdef [{:keys [:analyze-children :ns] :as ctx} expr]
(let [[sym-expr & body] (next (:children expr))
Expand Down
4 changes: 2 additions & 2 deletions src/clj_kondo/impl/analyzer/test.clj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(ns clj-kondo.impl.analyzer.test
{:no-doc true}
(:require
[clj-kondo.impl.utils :as utils]
[clj-kondo.impl.analyzer.common :as common]))
[clj-kondo.impl.analyzer.common :as common]
[clj-kondo.impl.utils :as utils]))

(defn analyze-deftest [ctx deftest-ns expr]
(common/analyze-defn
Expand Down
11 changes: 5 additions & 6 deletions src/clj_kondo/impl/analyzer/usages.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
{:no-doc true}
(:refer-clojure :exclude [ns-name])
(:require
[clj-kondo.impl.namespace :as namespace]
[clj-kondo.impl.analyzer.common :as common]
[clj-kondo.impl.utils :as utils :refer
[tag one-of symbol-from-token tag kw->sym]]
[clj-kondo.impl.metadata :as meta]
[clojure.string :as str])
[clj-kondo.impl.analyzer.common :as common]
[clj-kondo.impl.metadata :as meta]
[clj-kondo.impl.namespace :as namespace]
[clj-kondo.impl.utils :as utils :refer [tag one-of symbol-from-token tag kw->sym]]
[clojure.string :as str])
(:import [clj_kondo.impl.rewrite_clj.node.seq NamespacedMapNode]))

(set! *warn-on-reflection* true)
Expand Down
1 change: 1 addition & 0 deletions src/clj_kondo/impl/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
:unbound-destructuring-default {:level :warning}
:unused-binding {:level :warning
:exclude-destructured-keys-in-fn-args false}
:unsorted-namespaces {:level :off}
:unused-namespace {:level :warning
;; don't warn about these namespaces:
:exclude [#_clj-kondo.impl.var-info-gen]
Expand Down
12 changes: 6 additions & 6 deletions src/clj_kondo/impl/linters.clj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
(ns clj-kondo.impl.linters
{:no-doc true}
(:require
[clj-kondo.impl.utils :as utils :refer [node->line constant? sexpr]]
[clj-kondo.impl.var-info :as var-info]
[clj-kondo.impl.config :as config]
[clj-kondo.impl.analysis :as analysis]
[clj-kondo.impl.config :as config]
[clj-kondo.impl.findings :as findings]
[clojure.set :as set]
[clj-kondo.impl.namespace :as namespace]
[clojure.string :as str]
[clj-kondo.impl.types :as types]))
[clj-kondo.impl.types :as types]
[clj-kondo.impl.utils :as utils :refer [node->line constant? sexpr]]
[clj-kondo.impl.var-info :as var-info]
[clojure.set :as set]
[clojure.string :as str]))

(set! *warn-on-reflection* true)

Expand Down
24 changes: 0 additions & 24 deletions src/clj_kondo/impl/linters/misc.clj

This file was deleted.

5 changes: 2 additions & 3 deletions src/clj_kondo/impl/macroexpand.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
(ns clj-kondo.impl.macroexpand
{:no-doc true}
(:require
[clj-kondo.impl.utils :refer [parse-string tag vector-node list-node
token-node]]
[clj-kondo.impl.profiler :as profiler]))
[clj-kondo.impl.profiler :as profiler]
[clj-kondo.impl.utils :refer [parse-string tag vector-node list-node token-node]]))

(defn expand-> [_ctx expr]
(profiler/profile
Expand Down
6 changes: 3 additions & 3 deletions src/clj_kondo/impl/metadata.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
(ns clj-kondo.impl.metadata
{:no-doc true}
(:require
[clj-kondo.impl.linters.keys :as key-linter]
[clj-kondo.impl.utils :as utils]
[clj-kondo.impl.analyzer.common :as common]))
[clj-kondo.impl.analyzer.common :as common]
[clj-kondo.impl.linters.keys :as key-linter]
[clj-kondo.impl.utils :as utils]))

(defn meta-node->map [ctx node]
(let [s (utils/sexpr node)]
Expand Down
38 changes: 37 additions & 1 deletion src/clj_kondo/impl/namespace.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,49 @@
[clj-kondo.impl.analysis :as analysis]
[clj-kondo.impl.config :as config]
[clj-kondo.impl.findings :as findings]
[clj-kondo.impl.linters.misc :refer [lint-duplicate-requires!]]
[clj-kondo.impl.utils :refer [node->line deep-merge linter-disabled? one-of]]
[clj-kondo.impl.var-info :as var-info]
[clojure.string :as str])
(:import [java.util StringTokenizer]))

(set! *warn-on-reflection* true)

(defn lint-duplicate-requires!
([ctx namespaces] (lint-duplicate-requires! ctx #{} namespaces))
([ctx init namespaces]
(reduce (fn [required ns]
(if (contains? required ns)
(let [ns (if (symbol? ns) ns (second ns))]
(findings/reg-finding!
(:findings ctx)
(node->line (:filename ctx)
ns
:warning
:duplicate-require
(str "duplicate require of " ns)))
required)
(conj required ns)))
(set init)
namespaces)
nil))

(defn lint-unsorted-namespaces! [{:keys [config filename findings] :as _ctx} namespaces]
(when-not (= :off (-> config :linters :unsorted-namespaces :level))
(loop [last-processed-ns (first namespaces)
ns-list (next namespaces)]
(when ns-list
(let [ns (first ns-list)]
(if-not (neg? (compare last-processed-ns ns))
(findings/reg-finding!
findings
(node->line filename
ns
:warning
:unsorted-namespaces
(str "Unsorted namespace: " ns)))
(recur ns
(next ns-list))))))))

(defn reg-namespace!
"Registers namespace. Deep-merges with already registered namespaces
with the same name. Returns updated namespace."
Expand Down Expand Up @@ -148,6 +183,7 @@
[{:keys [:base-lang :lang :namespaces] :as ctx} ns-sym analyzed-require-clauses]
(swap! namespaces update-in [base-lang lang ns-sym]
(fn [ns]
(lint-unsorted-namespaces! ctx (:required analyzed-require-clauses))
(lint-duplicate-requires! ctx (:required ns) (:required analyzed-require-clauses))
(merge-with into ns analyzed-require-clauses)))
nil)
Expand Down
4 changes: 2 additions & 2 deletions src/clj_kondo/impl/parser.clj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(ns clj-kondo.impl.parser
{:no-doc true}
(:require
[clj-kondo.impl.utils :as utils :refer [parse-string-all]]
[clj-kondo.impl.profiler :refer [profile]]))
[clj-kondo.impl.profiler :refer [profile]]
[clj-kondo.impl.utils :as utils :refer [parse-string-all]]))

(defn parse-string [s]
(let [parsed (profile :parse-string-all (parse-string-all s))]
Expand Down
11 changes: 11 additions & 0 deletions src/clj_kondo/impl/utils.clj
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,17 @@
(transient {})
ks))))

(defn filter-remove [p xs]
(loop [xs xs
filtered (transient [])
removed (transient [])]
(if xs
(let [x (first xs)] (if (p x)
(recur (next xs)
(conj! filtered x) removed)
(recur (next xs) filtered (conj! removed x))))
[(persistent! filtered) (persistent! removed)])))

;;;; Scratch

(comment
Expand Down
4 changes: 2 additions & 2 deletions test/clj_kondo/impl/extract_var_info_test.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(ns clj-kondo.impl.extract-var-info-test
(:require
[clojure.test :as t :refer [deftest is]]
[clj-kondo.impl.extract-var-info :as extract-var-info]))
[clj-kondo.impl.extract-var-info :as extract-var-info]
[clojure.test :refer [deftest is]]))

(deftest eastwood-var-info-test
(let [var-info (extract-var-info/eastwood-var-info)]
Expand Down
21 changes: 21 additions & 0 deletions test/clj_kondo/main_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2428,6 +2428,27 @@
(is (empty? (lint! "(ns foo (:require [clojure.string])) clojure.string/join"
{:linters {:consistent-alias {:aliases '{clojure.string str}}}}))))

(deftest unsorted-namespaces-test
(assert-submaps
[{:file "<stdin>"
:row 1
:col 31
:level :warning
:message "Unsorted namespace: abar.core"}]
(lint! "(ns foo (:require [bar.core] [abar.core]))" {:linters {:unsorted-namespaces {:level :warning}}}))
(assert-submaps
[{:file "<stdin>"
:row 1
:col 21
:level :warning
:message "Unsorted namespace: abar.core"}]
(lint! "(require 'bar.core 'abar.core)" {:linters {:unsorted-namespaces {:level :warning}}}))
(is (empty? (lint! "(ns foo (:require [bar.core] [abar.core]))" {:linters {:unsorted-namespaces {:level :off}}})))
(is (empty? (lint! "(ns foo (:require [abar.core] [bar.core]))" {:linters {:unsorted-namespaces {:level :warning}}})))
(is (empty? (lint! "(ns foo (:require [abar.core] [bar.core]) (:import [java.lib JavaClass] [ajava.lib AnotherClass]))"
{:linters {:unsorted-namespaces {:level :warning}
:unused-import {:level :off}}}))))

(deftest set!-test
(assert-submaps '[{:col 13 :message #"arg"}]
(lint! "(declare x) (set! (.-foo x) 1 2 3)"))
Expand Down
6 changes: 3 additions & 3 deletions test/clj_kondo/test_test.clj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(ns clj-kondo.test-test
(:require
[clj-kondo.test-utils :refer [lint! assert-submaps]]
[clojure.test :refer [deftest is]]
[clojure.java.io :as io]))
[clj-kondo.test-utils :refer [lint! assert-submaps]]
[clojure.java.io :as io]
[clojure.test :refer [deftest is]]))

(deftest missing-test-assertion-test
(is (empty? (lint! "(ns foo (:require [clojure.test :as t])) (t/deftest (t/is (odd? 1)))")))
Expand Down
3 changes: 2 additions & 1 deletion test/clj_kondo/test_utils.clj
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
'{:linters {:unused-binding {:level :off}
:unresolved-symbol {:level :off}
:refer-all {:level :off}
:type-mismatch {:level :off}}})
:type-mismatch {:level :off}
:unsorted-namespaces {:level :off}}})

(defn lint-jvm!
([input]
Expand Down
8 changes: 4 additions & 4 deletions test/clj_kondo/unresolved_symbol_test.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
(ns clj-kondo.unresolved-symbol-test
(:require
[clj-kondo.test-utils :refer [lint! assert-submaps]]
[clojure.java.io :as io]
[clojure.test :as t :refer [deftest is testing]]
[clojure.edn :as edn]))
[clj-kondo.test-utils :refer [lint! assert-submaps]]
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.test :refer [deftest is testing]]))

(deftest unresolved-symbol-test
(assert-submaps
Expand Down

0 comments on commit 75f22a5

Please sign in to comment.