diff --git a/.circleci/config.yml b/.circleci/config.yml index e170a8ee..786110ad 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,28 +15,43 @@ commands: - ~/.m2 key: v1-dependencies-{{ checksum "project.clj" }} +executor_defaults: &executor_defaults + working_directory: ~/repo + executors: - lein-executor: + openjdk8: docker: - image: circleci/clojure:openjdk-8-lein-2.9.1 - working_directory: ~/repo environment: LEIN_ROOT: "true" JVM_OPTS: -Xmx3200m + <<: *executor_defaults + openjdk11: + docker: + - image: circleci/clojure:openjdk-11-lein-2.9.1 + environment: + LEIN_ROOT: "true" + JVM_OPTS: -Xmx3200m --illegal-access=deny + <<: *executor_defaults jobs: - build: - executor: lein-executor + test_code: + description: | + Runs tests against given version of the JDK + parameters: + jdk_version: + description: Version of the JDK to test against + type: string + lein_test_command: + description: A Leiningen command that will run a test suite + type: string + executor: << parameters.jdk_version >> steps: - setup-env - run: - name: 'Run JVM tests, including refactor-nrepl functionality' - command: lein with-profile -dev,+ci,+refactor-nrepl do clean, test - - run: - name: 'Run JVM tests, excluding refactor-nrepl functionality' - command: lein with-profile -dev,+ci do clean, test + command: << parameters.lein_test_command >> deploy: - executor: lein-executor + executor: openjdk8 steps: - setup-env - run: @@ -52,21 +67,44 @@ jobs: name: release to Clojars command: lein deploy clojars +test_code_filters: &test_code_filters + filters: + branches: + only: /.*/ + tags: + only: /^v\d+\.\d+\.\d+(-alpha\d+)?$/ + workflows: - version: 2 - CircleCI: + version: 2.1 + ci-test-matrix: jobs: - - build: - context: JFrog - filters: - branches: - only: /.*/ - tags: - only: /^v\d+\.\d+\.\d+(-alpha\d+)?$/ + - test_code: + name: "JDK 8 including refactor-nrepl" + jdk_version: openjdk8 + lein_test_command: lein with-profile -dev,+ci,+refactor-nrepl do clean, test + <<: *test_code_filters + - test_code: + name: "JDK 8 excluding refactor-nrepl" + jdk_version: openjdk8 + lein_test_command: lein with-profile -dev,+ci do clean, test + <<: *test_code_filters + - test_code: + name: "JDK 11 including refactor-nrepl" + jdk_version: openjdk11 + lein_test_command: lein with-profile -dev,+ci,+refactor-nrepl do clean, test + <<: *test_code_filters + - test_code: + name: "JDK 11 excluding refactor-nrepl" + jdk_version: openjdk11 + lein_test_command: lein with-profile -dev,+ci do clean, test + <<: *test_code_filters - deploy: context: JFrog requires: - - build + - "JDK 8 including refactor-nrepl" + - "JDK 8 excluding refactor-nrepl" + - "JDK 11 including refactor-nrepl" + - "JDK 11 excluding refactor-nrepl" filters: branches: ignore: /.*/ diff --git a/.gitignore b/.gitignore index d86667f4..1aa8ebbb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *~ .*.swo .*.swp +.clj-kondo/ .DS_Store .eastwood .idea/ diff --git a/README.md b/README.md index 04ae36a6..63a8628e 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,6 @@ As of today, it is integrated with: * Defaults to processing .cljs files only, given the overlap with Eastwood. * [refactor-nrepl](https://github.com/clojure-emacs/refactor-nrepl) * Used for "clean unused imports" functionality - * [bikeshed](https://github.com/dakrone/lein-bikeshed) - * Used for checking max column count * [all-my-files-should-end-with-exactly-one-newline-character](https://github.com/gfredericks/lein-all-my-files-should-end-with-exactly-one-newline-character) * Configurable, you can ensure either 0 or 1 ending newlines per file. @@ -32,6 +30,7 @@ And it also bundles a few tiny linters of its own: * [loc-per-ns](https://github.com/nedap/formatting-stack/blob/debdab8129dae7779d390216490625a3264c9d2c/src/formatting_stack/linters/loc_per_ns.clj) warns if a given NS surpasses a targeted LOC count. * [ns-aliases](https://github.com/nedap/formatting-stack/blob/debdab8129dae7779d390216490625a3264c9d2c/src/formatting_stack/linters/ns_aliases.clj) warns if [Sierra's](https://stuartsierra.com/2015/05/10/clojure-namespace-aliases) aliasing guide is disregarded. * [one-resource-per-ns](https://github.com/nedap/formatting-stack/blob/master/src/formatting_stack/linters/one_resource_per_ns.clj) warns if a Clojure namespace is defined in more than one file. + * [line-length](https://github.com/nedap/formatting-stack/blob/f1cf4206399a77a83fde4140095d4c59c10b1605/src/formatting_stack/linters/line_length.clj) warns if max line length is reached. It is fully extensible: you can configure the bundled formatters, remove them, and/or add your own. @@ -74,7 +73,7 @@ The general intent is to make formatting: #### Coordinates ```clojure -[formatting-stack "2.0.1-alpha2"] +[formatting-stack "3.0.0-alpha6"] ``` **Also** you might have to add the [refactor-nrepl](https://github.com/clojure-emacs/refactor-nrepl) dependency. diff --git a/dev/dev.clj b/dev/dev.clj index 03b1338a..1d1c7fec 100644 --- a/dev/dev.clj +++ b/dev/dev.clj @@ -12,16 +12,21 @@ ;; the "worker" source-path must be excluded. (set-refresh-dirs "src" "test" "dev") +(defn prepare-tests [] + (clear) + (alter-var-root #'clojure.test/*load-tests* (constantly true)) + (refresh)) + (defn suite [] - (refresh) + (prepare-tests) (run-all-tests #".*\.formatting-stack.*")) (defn unit [] - (refresh) + (prepare-tests) (run-all-tests #"unit\.formatting-stack.*")) (defn slow [] - (refresh) + (prepare-tests) (run-all-tests #"integration\.formatting-stack.*")) (defn diff [x y] diff --git a/project.clj b/project.clj index 58682a30..b67e3df4 100644 --- a/project.clj +++ b/project.clj @@ -1,18 +1,17 @@ ;; Please don't bump the library version by hand - use ci.release-workflow instead. -(defproject formatting-stack "2.0.1-alpha2" +(defproject formatting-stack "3.0.0-alpha6" ;; Please keep the dependencies sorted a-z. - :dependencies [[clj-kondo "2019.05.19-alpha"] + :dependencies [[clj-kondo "2020.01.13"] [cljfmt "0.6.5" :exclusions [rewrite-clj]] [com.gfredericks/how-to-ns "0.2.6"] [com.gfredericks/lein-all-my-files-should-end-with-exactly-one-newline-character "0.1.1"] [com.nedap.staffing-solutions/speced.def "2.0.0"] [com.nedap.staffing-solutions/utils.collections "2.0.0"] - [com.nedap.staffing-solutions/utils.modular "2.0.0"] + [com.nedap.staffing-solutions/utils.modular "2.1.0"] [com.nedap.staffing-solutions/utils.spec.predicates "1.1.0"] [com.stuartsierra/component "0.4.0"] [integrant "0.7.0"] [jonase/eastwood "0.3.5"] - [lein-bikeshed "0.5.1"] [medley "1.2.0"] [org.clojure/clojure "1.10.1"] [org.clojure/tools.namespace "0.3.1"] @@ -73,11 +72,41 @@ [org.clojure/test.check "0.10.0-alpha3"]] :jvm-opts ["-Dclojure.compiler.disable-locals-clearing=true"] :source-paths ["dev"] - :repl-options {:init-ns dev}} + :repl-options {:init-ns dev} + :middleware [~(do ;; the following ensures that :exclusions are honored in all cases + (create-ns 'user) + (intern 'user + 'nedap-ensure-exclusions + (fn [project] + (let [exclusions (->> project + :exclusions + (map (fn [x] + (str (if (namespace (symbol x)) + x + (symbol (str x) (str x)))))) + (set))] + (update project :dependencies (fn [deps] + (->> deps + (remove (fn [[dep version]] + (exclusions (str dep)))) + vec)))))) + 'user/nedap-ensure-exclusions)]} + + :provided {:dependencies [[org.clojure/clojurescript "1.10.597" + :exclusions [com.cognitect/transit-clj + com.google.code.findbugs/jsr305 + com.google.errorprone/error_prone_annotations]] + [com.google.guava/guava "25.1-jre" #_"transitive"] + [com.google.protobuf/protobuf-java "3.4.0" #_"transitive"] + [com.cognitect/transit-clj "0.8.313" #_"transitive"] + [com.google.errorprone/error_prone_annotations "2.1.3" #_"transitive"] + [com.google.code.findbugs/jsr305 "3.0.2" #_"transitive"]]} ;; `dev` in :test is important - a test depends on it: :test {:source-paths ["dev"] - :dependencies [[com.nedap.staffing-solutions/utils.test "1.6.2"]] + :dependencies [[com.nedap.staffing-solutions/utils.test "1.6.2"] + [nubank/matcher-combinators "1.0.1" + :exclusions [commons-codec]]] :jvm-opts ["-Dclojure.core.async.go-checking=true" "-Duser.language=en-US"] :resource-paths ["test-resources-extra" diff --git a/src/formatting_stack/branch_formatter.clj b/src/formatting_stack/branch_formatter.clj index 1a73a134..95ba50ca 100644 --- a/src/formatting_stack/branch_formatter.clj +++ b/src/formatting_stack/branch_formatter.clj @@ -9,13 +9,14 @@ [formatting-stack.formatters.no-extra-blank-lines :as formatters.no-extra-blank-lines] [formatting-stack.formatters.trivial-ns-duplicates :as formatters.trivial-ns-duplicates] [formatting-stack.indent-specs] - [formatting-stack.linters.bikeshed :as linters.bikeshed] [formatting-stack.linters.eastwood :as linters.eastwood] [formatting-stack.linters.kondo :as linters.kondo] + [formatting-stack.linters.line-length :as linters.line-length] [formatting-stack.linters.loc-per-ns :as linters.loc-per-ns] [formatting-stack.linters.ns-aliases :as linters.ns-aliases] [formatting-stack.linters.one-resource-per-ns :as linters.one-resource-per-ns] [formatting-stack.processors.cider :as processors.cider] + [formatting-stack.reporters.pretty-printer :as pretty-printer] [formatting-stack.strategies :as strategies] [medley.core :refer [mapply]])) @@ -46,39 +47,41 @@ (filterv some?))) (defn default-linters [default-strategies] - [(-> (linters.ns-aliases/new {}) + [(-> (linters.kondo/new {}) + (assoc :strategies (conj default-strategies + strategies/exclude-edn))) + (-> (linters.one-resource-per-ns/new {}) + (assoc :strategies (conj default-strategies + strategies/files-with-a-namespace))) + (-> (linters.ns-aliases/new {}) (assoc :strategies (conj default-strategies strategies/files-with-a-namespace ;; reader conditionals may confuse `linters.ns-aliases` strategies/exclude-cljc ;; string requires may confuse clojure.tools.* strategies/exclude-cljs))) - (-> (linters.loc-per-ns/new {}) + (-> (linters.line-length/new {}) (assoc :strategies (conj default-strategies strategies/exclude-edn))) - (-> (linters.bikeshed/new {}) + (-> (linters.loc-per-ns/new {}) (assoc :strategies (conj default-strategies strategies/exclude-edn))) (-> (linters.eastwood/new {}) (assoc :strategies (conj default-strategies strategies/exclude-cljs strategies/jvm-requirable-files - strategies/namespaces-within-refresh-dirs-only))) - (-> (linters.kondo/new) - (assoc :strategies (conj default-strategies - strategies/exclude-edn - strategies/exclude-clj - strategies/exclude-cljc))) - (-> (linters.one-resource-per-ns/new {}) - (assoc :strategies (conj default-strategies - strategies/files-with-a-namespace)))]) + strategies/namespaces-within-refresh-dirs-only)))]) (def default-processors [(processors.cider/new {:third-party-indent-specs third-party-indent-specs})]) -(defn format-and-lint-branch! [& {:keys [target-branch in-background?] +(def default-reporter + (pretty-printer/new {})) + +(defn format-and-lint-branch! [& {:keys [target-branch in-background? reporter] :or {target-branch "master" - in-background? (not (System/getenv "CI"))}}] + in-background? (not (System/getenv "CI")) + reporter default-reporter}}] (let [default-strategies [(fn [& {:as options}] (mapply strategies/git-diff-against-default-branch (assoc options :target-branch target-branch)))] formatters (default-formatters default-strategies) @@ -86,16 +89,20 @@ (formatting-stack.core/format! :strategies default-strategies :processors default-processors :formatters formatters + :reporter reporter :linters linters :in-background? in-background?))) -(defn lint-branch! [& {:keys [target-branch in-background?] - :or {target-branch "master"}}] +(defn lint-branch! [& {:keys [target-branch in-background? reporter] + :or {target-branch "master" + in-background? false + reporter default-reporter}}] (let [default-strategies [(fn [& {:as options}] (mapply strategies/git-diff-against-default-branch (assoc options :target-branch target-branch)))] linters (default-linters default-strategies)] (formatting-stack.core/format! :strategies default-strategies :formatters [] :processors default-processors + :reporter reporter :linters linters :in-background? in-background?))) diff --git a/src/formatting_stack/core.clj b/src/formatting_stack/core.clj index 21e5fa51..70d264d6 100644 --- a/src/formatting_stack/core.clj +++ b/src/formatting_stack/core.clj @@ -2,13 +2,14 @@ (:require [clojure.main] [formatting-stack.background] - [formatting-stack.defaults :refer :all] + [formatting-stack.defaults :refer [default-formatters default-linters default-processors default-strategies]] [formatting-stack.indent-specs :refer [default-third-party-indent-specs]] [formatting-stack.protocols.formatter :as protocols.formatter] [formatting-stack.protocols.linter :as protocols.linter] [formatting-stack.protocols.processor :as protocols.processor] - [formatting-stack.util :refer [with-serialized-output]] - [nedap.utils.modular.api :refer [implement]])) + [formatting-stack.protocols.reporter :refer [report]] + [formatting-stack.reporters.pretty-printer :as reporters.pretty-printer] + [formatting-stack.util :refer [with-serialized-output]])) (defn files-from-strategies [strategies] (->> strategies @@ -17,74 +18,60 @@ []) distinct)) -(def print-newline - (constantly println)) - -(def newliner - (implement {} - protocols.linter/--lint! print-newline - protocols.processor/--process! print-newline - protocols.formatter/--format! print-newline)) - -(defn process! [method members category-strategies default-strategies intersperse-newlines?] +(defn process! [method members category-strategies default-strategies] ;; `memoize` rationale: results are cached not for performance, ;; but for avoiding the scenario where one `member` alters the git status, ;; so the subsequent `member`s' strategies won't perceive the same set of files than the first one. ;; e.g. cljfmt may operate upon `strategies/git-completely-staged`, formatting some files accordingly. ;; Then `how-to-ns`, which follows the same strategy, would perceive a dirty git status. ;; Accordingly it would do nothing, which is undesirable. - (let [members (if-not intersperse-newlines? - members - (->> members (interpose newliner))) - files (memoize (fn [strategies] + (let [files (memoize (fn [strategies] (files-from-strategies strategies)))] (with-serialized-output - (doseq [member members] - (let [{specific-strategies :strategies} member - strategies (or specific-strategies category-strategies default-strategies)] - (try - (->> strategies files (method member)) - (catch Exception e - (println "Encountered an exception, which will be printed in the next line." - "formatting-stack execution has *not* been aborted.") - (-> e .printStackTrace)) - (catch AssertionError e - (println "Encountered an exception, which will be printed in the next line." - "formatting-stack execution has *not* been aborted.") - (-> e .printStackTrace)))))))) + (->> members + (mapcat (fn [member] + (let [{specific-strategies :strategies} member + strategies (or specific-strategies category-strategies default-strategies)] + (try + (->> strategies files (method member)) + (catch Exception e + [{:exception e + :source :formatting-stack/process! + :msg (str "Exception during " member) + :level :exception}]) + (catch AssertionError e + [{:exception e + :source :formatting-stack/process! + :msg (str "Exception during " member) + :level :exception}]))))) + (doall))))) (defn format! [& {:keys [strategies third-party-indent-specs formatters linters processors - in-background? - intersperse-newlines?]}] + reporter + in-background?]}] ;; the following `or` clauses ensure that Components don't pass nil values (let [strategies (or strategies default-strategies) third-party-indent-specs (or third-party-indent-specs default-third-party-indent-specs) formatters (or formatters (default-formatters third-party-indent-specs)) linters (or linters default-linters) processors (or processors (default-processors third-party-indent-specs)) + reporter (or reporter (reporters.pretty-printer/new {})) in-background? (if (some? in-background?) in-background? true) - {formatters-strategies - :formatters - linters-strategies - :linters - processors-strategies - :processors - default-strategies - :default} strategies + {formatters-strategies :formatters + linters-strategies :linters + processors-strategies :processors} strategies impl (bound-fn [] ;; important that it's a bound-fn (for an undetermined reason) - (process! protocols.formatter/format! formatters formatters-strategies strategies intersperse-newlines?) - (when intersperse-newlines? - (println)) - (process! protocols.linter/lint! linters linters-strategies strategies intersperse-newlines?) - (when intersperse-newlines? - (println)) - (process! protocols.processor/process! processors processors-strategies strategies intersperse-newlines?))] + (->> [(process! protocols.formatter/format! formatters formatters-strategies strategies) + (process! protocols.linter/lint! linters linters-strategies strategies) + (process! protocols.processor/process! processors processors-strategies strategies)] + (apply concat) + (report reporter)))] (if in-background? (do (reset! formatting-stack.background/workload impl) diff --git a/src/formatting_stack/defaults.clj b/src/formatting_stack/defaults.clj index 28c5d7a4..3df34555 100644 --- a/src/formatting_stack/defaults.clj +++ b/src/formatting_stack/defaults.clj @@ -6,9 +6,9 @@ [formatting-stack.formatters.newlines :as formatters.newlines] [formatting-stack.formatters.no-extra-blank-lines :as formatters.no-extra-blank-lines] [formatting-stack.formatters.trivial-ns-duplicates :as formatters.trivial-ns-duplicates] - [formatting-stack.linters.bikeshed :as linters.bikeshed] [formatting-stack.linters.eastwood :as linters.eastwood] [formatting-stack.linters.kondo :as linters.kondo] + [formatting-stack.linters.line-length :as linters.line-length] [formatting-stack.linters.loc-per-ns :as linters.loc-per-ns] [formatting-stack.linters.ns-aliases :as linters.ns-aliases] [formatting-stack.linters.one-resource-per-ns :as linters.one-resource-per-ns] @@ -21,58 +21,57 @@ strategies/git-not-completely-staged]) (defn default-formatters [third-party-indent-specs] - (let [opts {:third-party-indent-specs third-party-indent-specs} - ;; the following exists (for now) to guarantee that how-to-ns uses cached git results from cljfmt. + (let [;; the following exists (for now) to guarantee that how-to-ns uses cached git results from cljfmt. ;; ideally the how-to-ns formatter would have an extra `files-with-a-namespace` strategy but that would break git caching, ;; making usage more awkward. ;; the strategies mechanism needs some rework to avoid this limitation. - cljfmt-and-how-to-ns-opts (-> opts (assoc :strategies default-strategies))] - (->> [(formatters.cljfmt/new cljfmt-and-how-to-ns-opts) - (formatters.how-to-ns/new cljfmt-and-how-to-ns-opts) + cached-strategies default-strategies] + (->> [(-> (formatters.cljfmt/new {:third-party-indent-specs third-party-indent-specs}) + (assoc :strategies cached-strategies)) + (-> (formatters.how-to-ns/new {}) + (assoc :strategies cached-strategies)) (formatters.no-extra-blank-lines/new) - (formatters.newlines/new opts) - (formatters.trivial-ns-duplicates/new (assoc opts :strategies (conj default-strategies - strategies/files-with-a-namespace - strategies/exclude-edn))) + (formatters.newlines/new {}) + (-> (formatters.trivial-ns-duplicates/new {}) + (assoc :strategies (conj default-strategies + strategies/files-with-a-namespace + strategies/exclude-edn))) (when (strategies/refactor-nrepl-available?) - (formatters.clean-ns/new (assoc opts :strategies (conj default-strategies - strategies/when-refactor-nrepl - strategies/files-with-a-namespace - strategies/exclude-cljc - strategies/exclude-cljs - strategies/exclude-edn - strategies/namespaces-within-refresh-dirs-only - strategies/do-not-use-cached-results!))))] - + (-> (formatters.clean-ns/new {}) + (assoc :strategies (conj default-strategies + strategies/files-with-a-namespace + strategies/exclude-cljc + strategies/exclude-cljs + strategies/exclude-edn + strategies/namespaces-within-refresh-dirs-only + strategies/do-not-use-cached-results!))))] (filterv some?)))) -(def default-linters [(-> (linters.ns-aliases/new {}) +(def default-linters [(-> (linters.kondo/new {}) + (assoc :strategies (conj extended-strategies + strategies/exclude-edn))) + (-> (linters.one-resource-per-ns/new {}) + (assoc :strategies (conj extended-strategies + strategies/files-with-a-namespace))) + (-> (linters.ns-aliases/new {}) (assoc :strategies (conj extended-strategies strategies/files-with-a-namespace ;; reader conditionals may confuse `linters.ns-aliases` strategies/exclude-cljc ;; string requires may confuse clojure.tools.* strategies/exclude-cljs))) - (-> (linters.loc-per-ns/new {}) + (-> (linters.line-length/new {}) (assoc :strategies (conj extended-strategies strategies/exclude-edn))) - (-> (linters.bikeshed/new {}) + (-> (linters.loc-per-ns/new {}) (assoc :strategies (conj extended-strategies strategies/exclude-edn))) (-> (linters.eastwood/new {}) (assoc :strategies (conj extended-strategies strategies/exclude-cljs strategies/jvm-requirable-files - strategies/namespaces-within-refresh-dirs-only))) - (-> (linters.kondo/new) - (assoc :strategies (conj extended-strategies - strategies/exclude-edn - strategies/exclude-clj - strategies/exclude-cljc))) - (-> (linters.one-resource-per-ns/new {}) - (assoc :strategies (conj extended-strategies - strategies/files-with-a-namespace)))]) + strategies/namespaces-within-refresh-dirs-only)))]) (defn default-processors [third-party-indent-specs] - [(processors.cider/new {:third-party-indent-specs third-party-indent-specs - :strategies extended-strategies})]) + [(-> (processors.cider/new {:third-party-indent-specs third-party-indent-specs}) + (assoc :strategies extended-strategies))]) diff --git a/src/formatting_stack/formatters/clean_ns.clj b/src/formatting_stack/formatters/clean_ns.clj index af47b94b..abd203af 100644 --- a/src/formatting_stack/formatters/clean_ns.clj +++ b/src/formatting_stack/formatters/clean_ns.clj @@ -68,7 +68,8 @@ refactor-nrepl-opts namespaces-that-should-never-cleaned libspec-whitelist - filename)))))) + filename))))) + nil) (defn new [{:keys [refactor-nrepl-opts libspec-whitelist how-to-ns-opts namespaces-that-should-never-cleaned] :or {namespaces-that-should-never-cleaned default-namespaces-that-should-never-cleaned diff --git a/src/formatting_stack/formatters/clean_ns/impl.clj b/src/formatting_stack/formatters/clean_ns/impl.clj index 4f425ad0..1c9d84bd 100644 --- a/src/formatting_stack/formatters/clean_ns/impl.clj +++ b/src/formatting_stack/formatters/clean_ns/impl.clj @@ -4,9 +4,10 @@ [clojure.tools.reader :as tools.reader] [clojure.tools.reader.reader-types :refer [push-back-reader]] [clojure.walk :as walk] - [formatting-stack.util :refer [ensure-coll rcomp try-require]] + [formatting-stack.util :refer [ensure-coll rcomp]] [nedap.speced.def :as speced]) (:import + (clojure.lang Namespace) (java.io File))) (speced/defn ns-form-of [^string? filename] @@ -18,7 +19,7 @@ nil (throw e)))))) -(speced/defn safely-read-ns-contents [^string? buffer, ^clojure.lang.Namespace ns-obj] +(speced/defn safely-read-ns-contents [^string? buffer, ^Namespace ns-obj] (binding [tools.reader/*alias-map* (ns-aliases ns-obj)] (tools.reader/read-string {:read-cond :allow :features #{:clj}} diff --git a/src/formatting_stack/formatters/cljfmt.clj b/src/formatting_stack/formatters/cljfmt.clj index 2c922d21..feea8c73 100644 --- a/src/formatting_stack/formatters/cljfmt.clj +++ b/src/formatting_stack/formatters/cljfmt.clj @@ -11,7 +11,8 @@ (->> files (process-in-parallel! (fn [filename] (let [indents (impl/cljfmt-indents-for filename third-party-indent-specs)] - (cljfmt.main/fix [filename] {:indents indents})))))) + (cljfmt.main/fix [filename] {:indents indents}))))) + nil) (speced/defn new [{:keys [third-party-indent-specs] :as options}] (implement options diff --git a/src/formatting_stack/formatters/cljfmt/impl.clj b/src/formatting_stack/formatters/cljfmt/impl.clj index d67ee7c6..5d12c0ba 100644 --- a/src/formatting_stack/formatters/cljfmt/impl.clj +++ b/src/formatting_stack/formatters/cljfmt/impl.clj @@ -23,7 +23,7 @@ (locking require-lock (require namespace)) (ns-map namespace) - (catch Exception e + (catch Exception _ {}))) (spec/def ::indent-key #{:style/indent :style.cljfmt/indent :style.cljfmt/type}) diff --git a/src/formatting_stack/formatters/how_to_ns.clj b/src/formatting_stack/formatters/how_to_ns.clj index dc34ef17..04bd57e2 100644 --- a/src/formatting_stack/formatters/how_to_ns.clj +++ b/src/formatting_stack/formatters/how_to_ns.clj @@ -18,9 +18,10 @@ (defn format! [{:keys [how-to-ns-options]} files] (->> (remove #(str/ends-with? % ".edn") files) (process-in-parallel! (fn [filename] - (how-to-ns.main/fix [filename] how-to-ns-options))))) + (how-to-ns.main/fix [filename] how-to-ns-options)))) + nil) (defn new [{:keys [how-to-ns-options] :or {how-to-ns-options {}}}] - (implement {:how-to-ns-options (deep-merge formatting-stack.formatters.how-to-ns/default-how-to-ns-opts how-to-ns-options)} + (implement {:how-to-ns-options (deep-merge default-how-to-ns-opts how-to-ns-options)} formatter/--format! format!)) diff --git a/src/formatting_stack/formatters/newlines.clj b/src/formatting_stack/formatters/newlines.clj index d4d1d601..40b70f7e 100644 --- a/src/formatting_stack/formatters/newlines.clj +++ b/src/formatting_stack/formatters/newlines.clj @@ -10,7 +10,8 @@ (with-out-str ;; Supress "All newlines are good, nothing to fix." (->> files (process-in-parallel! (fn [filename] - (impl/so-fix-them [filename] :expected-newline-count expected-newline-count)))))) + (impl/so-fix-them [filename] :expected-newline-count expected-newline-count))))) + nil) (speced/defn new [{:keys [^pos-int? expected-newline-count] :or {expected-newline-count 1}}] diff --git a/src/formatting_stack/formatters/no_extra_blank_lines.clj b/src/formatting_stack/formatters/no_extra_blank_lines.clj index c03746c4..a5c02073 100644 --- a/src/formatting_stack/formatters/no_extra_blank_lines.clj +++ b/src/formatting_stack/formatters/no_extra_blank_lines.clj @@ -18,7 +18,8 @@ formatted (without-extra-newlines contents)] (when-not (= contents formatted) (println "Removing extra blank lines:" filename) - (spit filename formatted))))))) + (spit filename formatted)))))) + nil) (defn new [] (implement {} diff --git a/src/formatting_stack/formatters/trivial_ns_duplicates.clj b/src/formatting_stack/formatters/trivial_ns_duplicates.clj index 2234117e..28417a8c 100644 --- a/src/formatting_stack/formatters/trivial_ns_duplicates.clj +++ b/src/formatting_stack/formatters/trivial_ns_duplicates.clj @@ -142,7 +142,8 @@ (speced/fn ^{::speced/spec (complement #{"nil"})} [ns-form] (some-> ns-form remove-exact-duplicates pr-str)) "Removing trivial duplicates in `ns` form:" - how-to-ns-opts)))))) + how-to-ns-opts))))) + nil) (defn new [{:keys [how-to-ns-opts] :or {how-to-ns-opts {}}}] diff --git a/src/formatting_stack/linters/bikeshed.clj b/src/formatting_stack/linters/bikeshed.clj deleted file mode 100644 index ddfb25b3..00000000 --- a/src/formatting_stack/linters/bikeshed.clj +++ /dev/null @@ -1,32 +0,0 @@ -(ns formatting-stack.linters.bikeshed - (:require - [bikeshed.core :as bikeshed] - [clojure.java.io :as io] - [clojure.string :as str] - [formatting-stack.protocols.linter :as linter] - [nedap.utils.modular.api :refer [implement]])) - -(defn lint! [{:keys [max-line-length]} filenames] - (let [files (map io/file filenames) - output (->> (with-out-str - (bikeshed/long-lines files :max-line-length max-line-length)) - (str/split-lines) - (remove (fn [line] - (or (str/blank? line) - (some (fn [re] - (re-find re line)) - [#"^Checking for lines" - #":refer \[" - #"^No lines found"])))))] - (when (-> output count (> 1)) - (let [[head & tail] output - replacement (str "Lines exceeding " max-line-length " columns") - output (-> head - (str/replace #"^Badly formatted files" replacement) - (cons tail))] - (->> output (str/join "\n") println))))) - -(defn new [{:keys [max-line-length] - :or {max-line-length 130}}] - (implement {:max-line-length max-line-length} - linter/--lint! lint!)) diff --git a/src/formatting_stack/linters/eastwood.clj b/src/formatting_stack/linters/eastwood.clj index b0adc798..a59be9e4 100644 --- a/src/formatting_stack/linters/eastwood.clj +++ b/src/formatting_stack/linters/eastwood.clj @@ -2,11 +2,14 @@ (:require [clojure.string :as str] [eastwood.lint] + [eastwood.reporting-callbacks :as reporting-callbacks] [eastwood.util] [formatting-stack.protocols.linter :as linter] [formatting-stack.util :refer [ns-name-from-filename]] [medley.core :refer [deep-merge]] - [nedap.utils.modular.api :refer [implement]])) + [nedap.utils.modular.api :refer [implement]]) + (:import + (java.io File))) (def default-eastwood-options ;; Avoid false positives or more-annoying-than-useful checks: @@ -15,39 +18,43 @@ (-> eastwood.lint/default-opts (assoc :linters linters)))) -(def default-warnings-to-silence - [#"== Eastwood" - #"^dbg " - #"Warning: protocol .* is overwriting function" ;; False positive with nedap.speced.def - #"Directories scanned" - #"Entering directory" - #".*wrong-pre-post.*\*.*\*" ;; False positives for dynamic vars https://git.io/fhQTx - #"== Warnings" - #"== Linting done"]) +(defrecord TrackingReporter [reports]) -(defn lint! [{:keys [options warnings-to-silence]} filenames] +(defmethod reporting-callbacks/lint-warning TrackingReporter [{:keys [reports]} warning] + (swap! reports update :warnings (fnil conj []) warning) + nil) + +(defmethod reporting-callbacks/analyzer-exception TrackingReporter [{:keys [reports]} exception] + (swap! reports update :errors (fnil conj []) exception) + nil) + +(defmethod reporting-callbacks/note TrackingReporter [{:keys [reports]} msg] + (swap! reports update :note (fnil conj []) msg) + nil) + +(defn lint! [{:keys [options]} filenames] (reset! eastwood.util/warning-enable-config-atom []) ;; https://github.com/jonase/eastwood/issues/317 (let [namespaces (->> filenames (remove #(str/ends-with? % ".edn")) (keep ns-name-from-filename)) - result (->> (with-out-str - (binding [*warn-on-reflection* true] - (eastwood.lint/eastwood (-> options - (assoc :namespaces namespaces))))) - (str/split-lines) - (remove (fn [line] - (or (str/blank? line) - (some (fn [re] - (re-find re line)) - warnings-to-silence)))))] - (when-not (every? (fn [line] - (str/starts-with? line "== Linting")) - result) - (->> result (str/join "\n") println)))) + root-dir (-> (File. "") .getAbsolutePath) + reports (atom nil)] + (with-out-str + (eastwood.lint/eastwood (assoc options :namespaces namespaces) + (->TrackingReporter reports))) + (->> (:warnings @reports) + (map :warn-data) + (map (fn [{:keys [uri-or-file-name linter] :as m}] + (assoc m + :level :warning + :source (keyword "eastwood" (name linter)) + :filename (if (string? uri-or-file-name) + uri-or-file-name + (str/replace (-> uri-or-file-name .getPath) + root-dir + "")))))))) -(defn new [{:keys [eastwood-options warnings-to-silence] - :or {warnings-to-silence default-warnings-to-silence - eastwood-options {}}}] - (implement {:options (deep-merge default-eastwood-options eastwood-options) - :warnings-to-silence warnings-to-silence} +(defn new [{:keys [eastwood-options] + :or {eastwood-options {}}}] + (implement {:options (deep-merge default-eastwood-options eastwood-options)} linter/--lint! lint!)) diff --git a/src/formatting_stack/linters/kondo.clj b/src/formatting_stack/linters/kondo.clj index 527a0355..df6ecdac 100644 --- a/src/formatting_stack/linters/kondo.clj +++ b/src/formatting_stack/linters/kondo.clj @@ -1,22 +1,72 @@ (ns formatting-stack.linters.kondo (:require - [formatting-stack.linters.kondo.impl :as impl] - [formatting-stack.protocols.linter :as linter] + [clj-kondo.core :as kondo] + [clojure.set :as set] + [formatting-stack.kondo-classpath-cache] + [formatting-stack.protocols.linter :as protocols.linter] + [medley.core :refer [deep-merge]] [nedap.utils.modular.api :refer [implement]])) (def off {:level :off}) (def default-options - (list "--config" (pr-str {:linters {:invalid-arity off - :cond-without-else off}}))) + {:cache-dir formatting-stack.kondo-classpath-cache/cache-dir + :linters {:cond-else off ;; undesired + :missing-docstring off ;; undesired + :unused-binding off ;; undesired + :unresolved-symbol off ;; can give false positives + :unused-symbol off ;; can give false positives + :unused-private-var off ;; can give false positives + :consistent-alias off ;; already offered by how-to-ns + :duplicate-require off ;; already offered by clean-ns + :unused-import off ;; already offered by clean-ns + :unused-namespace off ;; already offered by clean-ns + :unused-referred-var off ;; already offered by clean-ns + :unresolved-namespace off ;; already offered by clean-ns + } + :lint-as '{nedap.speced.def/def-with-doc clojure.core/defonce + nedap.speced.def/defn clojure.core/defn + nedap.speced.def/defprotocol clojure.core/defprotocol + nedap.speced.def/doc clojure.repl/doc + nedap.speced.def/fn clojure.core/fn + nedap.speced.def/let clojure.core/let + nedap.speced.def/letfn clojure.core/letfn} + :output {:exclude-files ["test-resources/*" + "test/unit/formatting_stack/formatters/cljfmt/impl/sample_data.clj"]}}) -(defn lint! [this filenames] - (->> filenames - (cons "--lint") - (concat default-options) - (impl/parse-opts) - impl/lint!)) +(def clj-options + ;; .clj files are also linted by Eastwood, so we disable duplicate linters: + {:linters {:misplaced-docstring off + :invalid-arity off + :deprecated-var off + :inline-def off + :redefined-var off}}) -(defn new [] - (implement {} - linter/--lint! lint!)) +(defn lint! [{:keys [kondo-clj-options kondo-cljs-options]} + filenames] + + @formatting-stack.kondo-classpath-cache/classpath-cache + + (let [kondo-clj-options (or kondo-clj-options {}) + kondo-cljs-options (or kondo-cljs-options {}) + {cljs-files true + clj-files false} (->> filenames + (group-by (fn [f] + (-> (re-find #"\.cljs$" f) + boolean))))] + (->> [(kondo/run! {:lint clj-files + :config (deep-merge default-options clj-options kondo-clj-options)}) + (kondo/run! {:lint cljs-files + :config (deep-merge default-options kondo-cljs-options)})] + (mapcat :findings) + (map (fn [{source-type :type :as m}] + (-> (set/rename-keys m {:row :line + :message :msg + :col :column}) + (assoc :source (keyword "kondo" (name source-type))))))))) + +(defn new [{:keys [kondo-clj-options + kondo-cljs-options]}] + (implement {:kondo-clj-options kondo-clj-options + :kondo-cljs-options kondo-cljs-options} + protocols.linter/--lint! lint!)) diff --git a/src/formatting_stack/linters/kondo/impl.clj b/src/formatting_stack/linters/kondo/impl.clj deleted file mode 100644 index a206c0d2..00000000 --- a/src/formatting_stack/linters/kondo/impl.clj +++ /dev/null @@ -1,292 +0,0 @@ -(ns formatting-stack.linters.kondo.impl - "Vendorized/forked copy of clj-kondo.main. https://github.com/borkdude/clj-kondo#license" - (:require - [clj-kondo.impl.analyzer :as ana] - [clj-kondo.impl.cache :as cache] - [clj-kondo.impl.config :as config] - [clj-kondo.impl.linters :as l] - [clj-kondo.impl.namespace :as namespace] - [clj-kondo.impl.overrides :refer [overrides]] - [clj-kondo.impl.profiler :as profiler] - [clj-kondo.impl.state :as state] - [clojure.edn :as edn] - [clojure.java.io :as io] - [clojure.string :as str :refer [ends-with? starts-with?]]) - (:import - (java.util.jar JarFile JarFile$JarFileEntry))) - -(def dev? (= "true" (System/getenv "CLJ_KONDO_DEV"))) - -(def ^:private version (str/trim - (slurp (io/resource "CLJ_KONDO_VERSION")))) -(set! *warn-on-reflection* true) - -(defn- format-output [] - (if-let [^String pattern (-> @config/config :output :pattern)] - (fn [filename row col level message] - (-> pattern - (str/replace "{{filename}}" filename) - (str/replace "{{row}}" (str row)) - (str/replace "{{col}}" (str col)) - (str/replace "{{level}}" (name level)) - (str/replace "{{LEVEL}}" (str/upper-case (name level))) - (str/replace "{{message}}" message))) - (fn [filename row col level message] - (str filename ":" row ":" col ": " (name level) ": " message)))) - -(defn- print-findings [findings] - (let [format-fn (format-output)] - (doseq [{:keys [filename message level row col] :as finding} - (dedupe (sort-by (juxt :filename :row :col) findings))] - (println (format-fn filename row col level message))))) - -(defn- print-version [] - (println (str "clj-kondo v" version))) - -(defn- print-help [] - (print-version) - ;; TODO: document config format when stable enough - (println (format " -Usage: [ --help ] [ --version ] [ --lint ] [ --lang (clj|cljs) ] [ --cache [ ] ] [ --config ] - -Options: - - --lint: a file can either be a normal file, directory or classpath. In the - case of a directory or classpath, only .clj, .cljs and .cljc will be - processed. Use - as filename for reading from stdin. - - --lang: if lang cannot be derived from the file extension this option will be - used. - - --cache: if dir exists it is used to write and read data from, to enrich - analysis over multiple runs. If no value is provided, the nearest .clj-kondo - parent directory is detected and a cache directory will be created in it. - - --config: config may be a file or an EDN expression. See - https://cljdoc.org/d/clj-kondo/clj-kondo/%s/doc/configuration. -" version)) - nil) - -(defn- source-file? [filename] - (or (ends-with? filename ".clj") - (ends-with? filename ".cljc") - (ends-with? filename ".cljs"))) - -(defn- sources-from-jar - [^String jar-path] - (let [jar (JarFile. jar-path) - entries (enumeration-seq (.entries jar)) - entries (filter (fn [^JarFile$JarFileEntry x] - (let [nm (.getName x)] - (source-file? nm))) entries)] - (map (fn [^JarFile$JarFileEntry entry] - {:filename (.getName entry) - :source (slurp (.getInputStream jar entry))}) entries))) - -(defn- sources-from-dir - [dir] - (let [files (file-seq dir)] - (keep (fn [^java.io.File file] - (let [nm (.getPath file) - can-read? (.canRead file) - source? (source-file? nm)] - (cond - (and can-read? source?) - {:filename nm - :source (slurp file)} - (and (not can-read?) source?) - (do (println (str nm ":0:0:") "warning: can't read, check file permissions") - nil) - :else nil))) - files))) - -(defn- lang-from-file [file default-language] - (cond (ends-with? file ".clj") - :clj - (ends-with? file ".cljc") - :cljc - (ends-with? file ".cljs") - :cljs - :else default-language)) - -(def ^:private cp-sep (System/getProperty "path.separator")) - -(defn- classpath? [f] - (str/includes? f cp-sep)) - -(defn- process-file [filename default-language] - (try - (let [file (io/file filename)] - (cond - (.exists file) - (if (.isFile file) - (if (ends-with? file ".jar") - ;; process jar file - (mapcat #(ana/analyze-input (:filename %) (:source %) - (lang-from-file (:filename %) default-language) - dev?) - (sources-from-jar filename)) - ;; assume normal source file - (ana/analyze-input filename (slurp filename) - (lang-from-file filename default-language) - dev?)) - ;; assume directory - (mapcat #(ana/analyze-input (:filename %) (:source %) - (lang-from-file (:filename %) default-language) - dev?) - (sources-from-dir file))) - (= "-" filename) - (ana/analyze-input "" (slurp *in*) default-language dev?) - (classpath? filename) - (mapcat #(process-file % default-language) - (str/split filename - (re-pattern cp-sep))) - :else - [{:findings [{:level :warning - :filename filename - :col 0 - :row 0 - :message "file does not exist"}]}])) - (catch Throwable e - (if dev? (throw e) - [{:findings [{:level :warning - :filename filename - :col 0 - :row 0 - :message "could not process file"}]}])))) - -(defn- process-files [files default-lang] - (mapcat #(process-file % default-lang) files)) - -;;;; find cache/config dir - -(defn- config-dir - ([] (config-dir - (io/file - (System/getProperty "user.dir")))) - ([cwd] - (loop [dir (io/file cwd)] - (let [cfg-dir (io/file dir ".clj-kondo")] - (if (.exists cfg-dir) - (if (.isDirectory cfg-dir) - cfg-dir - (throw (Exception. (str cfg-dir " must be a directory")))) - (when-let [parent (.getParentFile dir)] - (recur parent))))))) - -(def ^:private - empty-cache-opt-warning - "WARNING: --cache option didn't specify directory, but no .clj-kondo directory found. Continuing without cache. See https://github.com/borkdude/clj-kondo/blob/master/README.md#project-setup.") - -(defn parse-opts [options] - (let [opts (loop [options options - opts-map {} - current-opt nil] - (if-let [opt (first options)] - (if (starts-with? opt "--") - (recur (rest options) - (assoc opts-map opt []) - opt) - (recur (rest options) - (update opts-map current-opt conj opt) - current-opt)) - opts-map)) - default-lang (case (first (get opts "--lang")) - "clj" :clj - "cljs" :cljs - "cljc" :cljc - :clj) - cache-opt (get opts "--cache") - cfg-dir (config-dir) - cache-dir (when cache-opt - (if-let [cd (first cache-opt)] - (io/file cd version) - (if cfg-dir (io/file cfg-dir ".cache" version) - (do (println empty-cache-opt-warning) - nil)))) - files (get opts "--lint") - raw-config (first (get opts "--config")) - config-edn? (when raw-config - (str/starts-with? raw-config "{")) - config-opt (and raw-config - (if config-edn? - (edn/read-string raw-config) - (edn/read-string (slurp raw-config)))) - config-edn (when cfg-dir - (let [f (io/file cfg-dir "config.edn")] - (when (.exists f) - (edn/read-string (slurp f)))))] - {:opts opts - :files files - :cache-dir cache-dir - :default-lang default-lang - :configs [config-edn config-opt]})) - -(defn- mmerge - "Merges maps no deeper than two levels" - [a b] - (merge-with merge a b)) - -(defn- index-defs-and-calls [defs-and-calls] - (reduce - (fn [acc {:keys [:calls :defs :used :lang] :as m}] - (-> acc - (update-in [lang :calls] (fn [prev-calls] - (merge-with into prev-calls calls))) - (update-in [lang :defs] mmerge defs) - (update-in [lang :used] into used))) - {:clj {:calls {} :defs {} :used #{}} - :cljs {:calls {} :defs {} :used #{}} - :cljc {:calls {} :defs {} :used #{}}} - defs-and-calls)) - -(def ^:private zinc (fnil inc 0)) - -(defn- summarize [findings] - (reduce (fn [acc {:keys [:level]}] - (update acc level zinc)) - {:error 0 :warning 0 :info 0} - findings)) - -(defn- filter-findings [findings] - (let [print-debug? (:debug @config/config) - filter-output (not-empty (-> @config/config :output :include-files)) - remove-output (not-empty (-> @config/config :output :exclude-files))] - (for [{:keys [:filename :level :type] :as f} findings - :let [level (or (when type (-> @config/config :linters type :level)) - level)] - :when (and level (not= :off level)) - :when (if (= :debug type) - print-debug? - true) - :when (if filter-output - (some (fn [pattern] - (re-find (re-pattern pattern) filename)) - filter-output) - true) - :when (not-any? (fn [pattern] - (re-find (re-pattern pattern) filename)) - remove-output)] - (assoc f :level level)))) - -(defn lint! [{:keys [opts files default-lang cache-dir configs]}] - (state/clear-findings!) - (reset! namespace/namespaces {}) - (run! config/merge-config! configs) - (let [processed (process-files files default-lang) - idacs (-> processed - index-defs-and-calls - (cache/sync-cache cache-dir) - overrides) - linted-calls (doall (l/lint-calls idacs)) - _ (l/lint-unused-namespaces!) - all-findings (concat linted-calls (mapcat :findings processed) - @state/findings) - all-findings (filter-findings all-findings) - {:keys [error warning]} (summarize all-findings)] - (when (-> @config/config :output :show-progress) - (println)) - (print-findings all-findings) - (cond (pos? error) 3 - (pos? warning) 2 - :else 0))) diff --git a/src/formatting_stack/linters/line_length.clj b/src/formatting_stack/linters/line_length.clj new file mode 100644 index 00000000..ec087590 --- /dev/null +++ b/src/formatting_stack/linters/line_length.clj @@ -0,0 +1,30 @@ +(ns formatting-stack.linters.line-length + (:require + [clojure.string :as string] + [formatting-stack.protocols.linter :as linter] + [formatting-stack.util :refer [ensure-coll ensure-sequential process-in-parallel!]] + [nedap.utils.modular.api :refer [implement]])) + +(defn exceeding-lines [threshold filename] + (->> (-> filename slurp (string/split #"\n")) + (map-indexed (fn [i row] + (let [column-count (count row)] + (when (< threshold column-count) + {:filename filename + :source :formatting-stack/line-length + :level :warning + :column column-count + :line (inc i) + :msg (str "Line exceeding " threshold " columns.")})))) + (remove nil?) + (vec))) + +(defn lint! [{:keys [max-line-length]} filenames] + (->> filenames + (process-in-parallel! (partial exceeding-lines max-line-length)) + (mapcat ensure-sequential))) + +(defn new [{:keys [max-line-length] + :or {max-line-length 130}}] + (implement {:max-line-length max-line-length} + linter/--lint! lint!)) diff --git a/src/formatting_stack/linters/loc_per_ns.clj b/src/formatting_stack/linters/loc_per_ns.clj index 9ad3c9c4..02068141 100644 --- a/src/formatting_stack/linters/loc_per_ns.clj +++ b/src/formatting_stack/linters/loc_per_ns.clj @@ -5,24 +5,26 @@ [formatting-stack.util :refer [process-in-parallel!]] [nedap.utils.modular.api :refer [implement]])) -(defn overly-long-ns? [filename threshold] +(defn count-lines [filename] (-> filename slurp (string/split #"\n") - (count) - (> threshold))) + (count))) (defn lint! [{:keys [max-lines-per-ns]} filenames] (->> filenames (process-in-parallel! (fn [filename] - (when (overly-long-ns? filename max-lines-per-ns) - (println "Warning:" - filename - "is longer than" - max-lines-per-ns - "LOC. Consider refactoring.")))))) + (let [lines (count-lines filename)] + (when (> lines max-lines-per-ns) + {:filename filename + :source :formatting-stack/loc-per-ns + :level :warning + :msg (str "Longer than " max-lines-per-ns " LOC.") + :line lines + :column 0})))) + (remove nil?))) (defn new [{:keys [max-lines-per-ns] - :or {max-lines-per-ns 350}}] + :or {max-lines-per-ns 350}}] (implement {:max-lines-per-ns max-lines-per-ns} linter/--lint! lint!)) diff --git a/src/formatting_stack/linters/ns_aliases.clj b/src/formatting_stack/linters/ns_aliases.clj index fa23e57c..1184af9c 100644 --- a/src/formatting_stack/linters/ns_aliases.clj +++ b/src/formatting_stack/linters/ns_aliases.clj @@ -1,11 +1,15 @@ (ns formatting-stack.linters.ns-aliases "Observes these guidelines: https://stuartsierra.com/2015/05/10/clojure-namespace-aliases" (:require + [clojure.java.io :as io] [clojure.string :as string] - [clojure.tools.namespace.file :as file] + [clojure.tools.namespace.parse :as parse] + [clojure.tools.reader.reader-types :refer [indexing-push-back-reader]] [formatting-stack.protocols.linter :as linter] - [formatting-stack.util :refer [process-in-parallel!]] - [nedap.utils.modular.api :refer [implement]])) + [formatting-stack.util :refer [ensure-sequential ensure-coll process-in-parallel!]] + [nedap.utils.modular.api :refer [implement]]) + (:import + (java.io PushbackReader))) (defn clause= [a b] (->> [a b] @@ -69,29 +73,33 @@ (derived? alias :from ns-name)) (boolean)))))) +(defn read-ns-decl + "Reads file with line/column metadata" + [filename] + (with-open [reader (-> (io/reader filename) PushbackReader. indexing-push-back-reader)] + (parse/read-ns-decl reader))) + (defn lint! [{:keys [acceptable-aliases-whitelist]} filenames] (->> filenames (process-in-parallel! (fn [filename] - (let [bad-require-clauses (->> filename - file/read-file-ns-decl - formatting-stack.util/require-from-ns-decl - (rest) - (remove (partial acceptable-require-clause? - acceptable-aliases-whitelist)))] - (when (seq bad-require-clauses) - (let [formatted-bad-requires (->> bad-require-clauses - (map (fn [x] - (str " " x))) - (string/join "\n"))] - (-> (str "Warning for " - filename - ": the following :require aliases are not derived from their refered namespace:" - "\n" - formatted-bad-requires - ". See https://stuartsierra.com/2015/05/10/clojure-namespace-aliases\n") - (println))))))))) + (->> filename + read-ns-decl + formatting-stack.util/require-from-ns-decl + (rest) + (remove (partial acceptable-require-clause? + acceptable-aliases-whitelist)) + (filter some?) + (mapv (fn [bad-alias] + {:filename filename + :line (-> bad-alias meta :line) + :column (-> bad-alias meta :column) + :level :warning + :warning-details-url "https://stuartsierra.com/2015/05/10/clojure-namespace-aliases" + :msg (str bad-alias " is not a derived alias.") + :source :formatting-stack/ns-aliases}))))) + (mapcat ensure-sequential))) (defn new [{:keys [acceptable-aliases-whitelist] - :or {acceptable-aliases-whitelist default-acceptable-aliases-whitelist}}] + :or {acceptable-aliases-whitelist default-acceptable-aliases-whitelist}}] (implement {:acceptable-aliases-whitelist acceptable-aliases-whitelist} linter/--lint! lint!)) diff --git a/src/formatting_stack/linters/one_resource_per_ns.clj b/src/formatting_stack/linters/one_resource_per_ns.clj index fe528e72..d76b3427 100644 --- a/src/formatting_stack/linters/one_resource_per_ns.clj +++ b/src/formatting_stack/linters/one_resource_per_ns.clj @@ -36,7 +36,9 @@ (mapv str))) (speced/defn analyze [^present-string? filename] - (for [extension [".clj" ".cljs" ".cljc"] + (for [extension (->> [".clj" ".cljs" ".cljc"] + (filter (fn [x] + (string/ends-with? filename x)))) :let [decl (-> filename file/read-file-ns-decl) resource-path (ns-decl->resource-path decl extension) filenames (resource-path->filenames resource-path)] @@ -50,11 +52,19 @@ (process-in-parallel! (fn [filename] (->> filename analyze - (run! (speced/fn [{:keys [^symbol? ns-name, ^coll? filenames]}] - (println "Warning: the namespace" - (str "`" ns-name "`") - "is defined over more than one file.\nFound:" - (->> filenames (interpose ", ") (apply str)))))))))) + (mapv (speced/fn [{:keys [^symbol? ns-name, ^coll? filenames]}] + {:filename filename + :level :warning + :line 0 + :column 0 + :msg (str "The namespace " + "`" ns-name "`" + " is defined over more than one file. Found:") + :msg-extra-data (->> filenames + (mapv (fn [s] + (string/replace s #"^file:" "")))) + :source :formatting-stack/one-resource-per-ns}))))) + (apply concat))) (speced/defn new [^map? opts] (implement opts diff --git a/src/formatting_stack/processors/test_runner.clj b/src/formatting_stack/processors/test_runner.clj index 728efbb0..43968b4b 100644 --- a/src/formatting_stack/processors/test_runner.clj +++ b/src/formatting_stack/processors/test_runner.clj @@ -6,7 +6,7 @@ and invokes `#'clojure.test/run-tests` out of that result." (:require [clojure.test] - [formatting-stack.processors.test-runner.impl :refer :all] + [formatting-stack.processors.test-runner.impl :refer [ns->sym testable-namespaces]] [formatting-stack.protocols.processor :as processor] [formatting-stack.strategies :refer [git-completely-staged git-diff-against-default-branch git-not-completely-staged]] [nedap.utils.modular.api :refer [implement]])) @@ -18,7 +18,8 @@ (testable-namespaces) (map ns->sym) (seq))] - (apply clojure.test/run-tests test-namespaces))) + (apply clojure.test/run-tests test-namespaces)) + nil) (defn test! "Convenience function provided in case it is desired to leverage this ns's functionality, diff --git a/src/formatting_stack/processors/test_runner/impl.clj b/src/formatting_stack/processors/test_runner/impl.clj index c5057560..d4640c1a 100644 --- a/src/formatting_stack/processors/test_runner/impl.clj +++ b/src/formatting_stack/processors/test_runner/impl.clj @@ -23,8 +23,7 @@ "Some projects have the naming convention of prefixing the last segment with `t-` to denote a testing namespace." [^Namespace n] - (let [s (str n) - segments (-> n str (string/split #"\.")) + (let [segments (-> n str (string/split #"\.")) first-segments (butlast segments) last-segment (->> segments last (str "t-"))] (->> [last-segment] diff --git a/src/formatting_stack/project_formatter.clj b/src/formatting_stack/project_formatter.clj index 93cc997a..43e1e614 100644 --- a/src/formatting_stack/project_formatter.clj +++ b/src/formatting_stack/project_formatter.clj @@ -9,19 +9,23 @@ [formatting-stack.formatters.no-extra-blank-lines :as formatters.no-extra-blank-lines] [formatting-stack.formatters.trivial-ns-duplicates :as formatters.trivial-ns-duplicates] [formatting-stack.indent-specs] - [formatting-stack.linters.bikeshed :as linters.bikeshed] [formatting-stack.linters.eastwood :as linters.eastwood] [formatting-stack.linters.kondo :as linters.kondo] + [formatting-stack.linters.line-length :as linters.line-length] [formatting-stack.linters.loc-per-ns :as linters.loc-per-ns] [formatting-stack.linters.ns-aliases :as linters.ns-aliases] [formatting-stack.linters.one-resource-per-ns :as linters.one-resource-per-ns] [formatting-stack.processors.cider :as processors.cider] + [formatting-stack.reporters.pretty-printer :as pretty-printer] [formatting-stack.strategies :as strategies])) (def third-party-indent-specs formatting-stack.indent-specs/default-third-party-indent-specs) (def default-strategies [strategies/all-files]) +(def default-reporter + (pretty-printer/new {})) + (def default-formatters (->> [(formatters.cljfmt/new {:third-party-indent-specs third-party-indent-specs}) (-> (formatters.how-to-ns/new {}) @@ -47,50 +51,50 @@ (filterv some?))) (def default-linters - [(-> (linters.ns-aliases/new {}) + [(-> (linters.kondo/new {}) + (assoc :strategies (conj default-strategies + strategies/exclude-edn))) + (-> (linters.one-resource-per-ns/new {}) + (assoc :strategies (conj default-strategies + strategies/files-with-a-namespace))) + (-> (linters.ns-aliases/new {}) (assoc :strategies (conj default-strategies strategies/files-with-a-namespace ;; reader conditionals may confuse `linters.ns-aliases` strategies/exclude-cljc ;; string requires may confuse clojure.tools.* strategies/exclude-cljs))) - (-> (linters.loc-per-ns/new {}) + (-> (linters.line-length/new {}) (assoc :strategies (conj default-strategies strategies/exclude-edn))) - (-> (linters.bikeshed/new {}) + (-> (linters.loc-per-ns/new {}) (assoc :strategies (conj default-strategies strategies/exclude-edn))) (-> (linters.eastwood/new {}) (assoc :strategies (conj default-strategies strategies/exclude-cljs strategies/jvm-requirable-files - strategies/namespaces-within-refresh-dirs-only))) - (-> (linters.kondo/new) - (assoc :strategies (conj default-strategies - strategies/exclude-edn - strategies/exclude-clj - strategies/exclude-cljc))) - (-> (linters.one-resource-per-ns/new {}) - (assoc :strategies (conj default-strategies - strategies/files-with-a-namespace)))]) + strategies/namespaces-within-refresh-dirs-only)))]) (def default-processors [(processors.cider/new {:third-party-indent-specs third-party-indent-specs})]) -(defn format-and-lint-project! [& {:keys [in-background?] - :or {in-background? false}}] +(defn format-and-lint-project! [& {:keys [in-background? reporter] + :or {in-background? false + reporter default-reporter}}] (formatting-stack.core/format! :strategies default-strategies :formatters default-formatters :linters default-linters + :reporter reporter :processors default-processors - :in-background? in-background? - :intersperse-newlines? true)) + :in-background? in-background?)) -(defn lint-project! [& {:keys [in-background?] - :or {in-background? false}}] +(defn lint-project! [& {:keys [in-background? reporter] + :or {in-background? false + reporter default-reporter}}] (formatting-stack.core/format! :strategies default-strategies :formatters [] :processors default-processors + :reporter reporter :linters default-linters - :in-background? in-background? - :intersperse-newlines? true)) + :in-background? in-background?)) diff --git a/src/formatting_stack/project_parsing.clj b/src/formatting_stack/project_parsing.clj index 77b7d8e0..29fc6322 100644 --- a/src/formatting_stack/project_parsing.clj +++ b/src/formatting_stack/project_parsing.clj @@ -2,9 +2,9 @@ (:require [clojure.java.classpath :as classpath] [clojure.java.io :as io] - [clojure.tools.namespace.file :as file] [clojure.tools.namespace.find :as find] [clojure.tools.namespace.parse :as parse] + [formatting-stack.formatters.clean-ns.impl :refer [ns-form-of]] [nedap.speced.def :as speced] [nedap.utils.collections.eager :refer [partitioning-pmap]]) (:import @@ -28,11 +28,12 @@ Includes third-party dependencies." [] (->> (find-files (classpath/classpath-directories) find/clj) - (partitioning-pmap (fn [file] - (let [decl (-> file file/read-file-ns-decl) - n (-> decl parse/name-from-ns-decl) - deps (-> decl parse/deps-from-ns-decl)] - (conj deps n)))) + + (partitioning-pmap (speced/fn [^File file] + (let [decl (-> file str ns-form-of) + n (some-> decl parse/name-from-ns-decl) + deps (some-> decl parse/deps-from-ns-decl)] + (some-> deps (conj n))))) (apply concat) (distinct) (filter identity) diff --git a/src/formatting_stack/protocols/formatter.clj b/src/formatting_stack/protocols/formatter.clj index 2c0f9fdb..fc1b0f7f 100644 --- a/src/formatting_stack/protocols/formatter.clj +++ b/src/formatting_stack/protocols/formatter.clj @@ -8,5 +8,5 @@ Normally it's a wrapper around a formatting library, with extra configuration, performance improvements, etc." - (format! [this, ^::protocols.spec/filenames filenames] + (^nil? format! [this, ^::protocols.spec/filenames filenames] "Formats `filenames` according to a formatter of your choice.")) diff --git a/src/formatting_stack/protocols/linter.clj b/src/formatting_stack/protocols/linter.clj index 70d36238..98d7c104 100644 --- a/src/formatting_stack/protocols/linter.clj +++ b/src/formatting_stack/protocols/linter.clj @@ -8,5 +8,5 @@ Normally it's a wrapper around a linting library, with extra configuration, performance improvements, etc." - (lint! [this, ^::protocols.spec/filenames filenames] + (^::protocols.spec/reports lint! [this, ^::protocols.spec/filenames filenames] "Lints `filenames` according to a linter of your choice: e.g. Eastwood, or Kibit, lein-dependency-check, etc.")) diff --git a/src/formatting_stack/protocols/processor.clj b/src/formatting_stack/protocols/processor.clj index 3e5a0f52..c29c79e0 100644 --- a/src/formatting_stack/protocols/processor.clj +++ b/src/formatting_stack/protocols/processor.clj @@ -6,6 +6,6 @@ (speced/defprotocol Processor "Any file-processing component that isn't a formatter or a linter." - (process! [this, ^::protocols.spec/filenames filenames] + (^nil? process! [this, ^::protocols.spec/filenames filenames] "Performs a compilation according to a processor of your choice: e.g. the ClojureScript processor, or Garden, Stefon, etc. You are free to ignore `filenames`, compiling the whole project instead.")) diff --git a/src/formatting_stack/protocols/reporter.clj b/src/formatting_stack/protocols/reporter.clj new file mode 100644 index 00000000..2bd2f5db --- /dev/null +++ b/src/formatting_stack/protocols/reporter.clj @@ -0,0 +1,10 @@ +(ns formatting-stack.protocols.reporter + (:require + [formatting-stack.protocols.spec :as protocols.spec] + [nedap.speced.def :as speced])) + +(speced/defprotocol Reporter + "A Reporter prints (or writes) info coming from other members, such as Formatters, Linters, etc." + + (report [this, ^::protocols.spec/reports reports] + "Emits a report, out of the `reports` argument (namely a collection of discrete actionable items).")) diff --git a/src/formatting_stack/protocols/spec.clj b/src/formatting_stack/protocols/spec.clj index e4606a7b..059698ac 100644 --- a/src/formatting_stack/protocols/spec.clj +++ b/src/formatting_stack/protocols/spec.clj @@ -3,4 +3,41 @@ [clojure.spec.alpha :as spec] [nedap.utils.spec.predicates :refer [present-string?]])) -(spec/def ::filenames (spec/coll-of present-string?)) +(spec/def ::filename present-string?) + +(spec/def ::filenames (spec/coll-of ::filename)) + +(spec/def ::msg present-string?) + +(spec/def ::msg-extra-data (spec/coll-of present-string?)) + +(spec/def ::source qualified-keyword?) + +(spec/def ::column nat-int?) + +(spec/def ::line ::column) + +(spec/def ::level #{:warning :error :exception}) + +(defmulti reportmm :level) + +(defmethod reportmm :exception [_] + (spec/keys :req-un [::msg + ::exception + ::source + ::level] + :opt-un [::filename])) + +(defmethod reportmm :default [_] + (spec/keys :req-un [::filename + ::source + ::msg + ::level + ::column + ::line] + :opt-un [::msg-extra-data])) + +(spec/def ::report + (spec/multi-spec reportmm :level)) + +(spec/def ::reports (spec/coll-of ::report)) diff --git a/src/formatting_stack/reporters/file_writer.clj b/src/formatting_stack/reporters/file_writer.clj new file mode 100644 index 00000000..ea6505d5 --- /dev/null +++ b/src/formatting_stack/reporters/file_writer.clj @@ -0,0 +1,18 @@ +(ns formatting-stack.reporters.file-writer + "Writes the output to a file which can be observed with e.g. `watch --color -n 1 cat .formatting-stack-report`." + (:require + [formatting-stack.protocols.reporter :as protocols.reporter] + [formatting-stack.reporters.pretty-printer :as pretty-printer] + [nedap.utils.modular.api :refer [implement]])) + +(defn write-report [{:keys [printer filename]} reports] + (->> (with-out-str + (protocols.reporter/report printer reports)) + (spit filename))) + +(defn new [{:keys [printer filename] + :or {printer (pretty-printer/new {}) + filename ".formatting-stack-report"}}] + (implement {:printer printer + :filename filename} + protocols.reporter/--report write-report)) diff --git a/src/formatting_stack/reporters/passthrough.clj b/src/formatting_stack/reporters/passthrough.clj new file mode 100644 index 00000000..e22b82fc --- /dev/null +++ b/src/formatting_stack/reporters/passthrough.clj @@ -0,0 +1,13 @@ +(ns formatting-stack.reporters.passthrough + (:require + [formatting-stack.protocols.reporter :as protocols.reporter] + [nedap.utils.modular.api :refer [implement]])) + +(defn report [_ _] + []) + +(defn new + "Does not perform a report. Apt for the test suite." + [] + (implement {} + protocols.reporter/--report report)) diff --git a/src/formatting_stack/reporters/pretty_printer.clj b/src/formatting_stack/reporters/pretty_printer.clj new file mode 100644 index 00000000..77c14527 --- /dev/null +++ b/src/formatting_stack/reporters/pretty_printer.clj @@ -0,0 +1,106 @@ +(ns formatting-stack.reporters.pretty-printer + "Prints an optionally colorized, indented, possibly truncated output of the reports." + (:require + [clojure.stacktrace :refer [print-stack-trace]] + [clojure.string :as string] + [formatting-stack.protocols.reporter :as reporter] + [formatting-stack.protocols.spec :as protocols.spec] + [formatting-stack.util :refer [colorize]] + [medley.core :refer [map-vals]] + [nedap.speced.def :as speced] + [nedap.utils.modular.api :refer [implement]])) + +(speced/defn truncate-line-wise [^string? s, length] + (if (= s "\n") + s + (->> (string/split s #"\n") + (map (fn [s] + (let [suffix "…" + string-length (count s) + suffix-length (count suffix)] + (if (<= string-length length) + s + (str (subs s + 0 + (- length suffix-length)) + suffix))))) + (string/join "\n")))) + +(defn print-summary [{:keys [summary?]} reports] + (when summary? + (->> reports + (group-by :level) + (map-vals count) + (into (sorted-map-by compare)) ;; print summary in order + (run! (fn [[report-type n]] + (-> (str n (case report-type + :exception " exceptions occurred" + :error " errors found" + :warning " warnings found")) + (colorize (case type + :exception :red + :error :red + :warning :yellow)) + (println))))))) + +(defn print-exceptions [{:keys [print-stacktraces?]} reports] + (->> reports + (filter (speced/fn [{:keys [^::protocols.spec/level level]}] + (#{:exception} level))) + (group-by :filename) + (run! (fn [[title reports]] + (println (colorize title :cyan)) + (doseq [{:keys [^Throwable exception]} reports] + (if print-stacktraces? + (print-stack-trace exception) + (println (ex-message exception))) + (println)))))) + +(speced/defn print-warnings [{:keys [max-msg-length + ^boolean? colorize?]} + ^::protocols.spec/reports reports] + (->> reports + (filter (speced/fn [{:keys [^::protocols.spec/level level]}] + (#{:error :warning} level))) + (group-by :filename) + (into (sorted-map-by compare)) ;; sort filenames for consistent output + (run! (fn [[title reports]] + (println (cond-> title + colorize? (colorize :cyan))) + (doseq [[source-group group-entries] (->> reports + (group-by :source)) + :let [_ (println " " (cond-> source-group + colorize? (colorize (case (-> group-entries first :level) + :error :red + :warning :yellow)))) + _ (when-let [url (->> group-entries + (keep :warning-details-url) + first)] + (cond-> (str " See: " url) + colorize? (colorize :grey) + true println))] + {:keys [msg column line source level msg-extra-data warning-details-url]} (->> group-entries + (sort-by :line))] + + (println (cond-> (str " " line ":" column) + colorize? (colorize :grey)) + (truncate-line-wise msg max-msg-length)) + (doseq [entry msg-extra-data] + (println " " + (truncate-line-wise entry max-msg-length)))) + (println))))) + +(defn print-report [this reports] + (print-exceptions this reports) + (print-warnings this reports) + (print-summary this reports)) + +(defn new [{:keys [max-msg-length print-stacktraces? summary? colorize?] + :or {max-msg-length 200 + print-stacktraces? true + summary? true + colorize? true}}] + (implement {:max-msg-length max-msg-length + :print-stacktraces? print-stacktraces? + :colorize? colorize?} + reporter/--report print-report)) diff --git a/src/formatting_stack/strategies/impl.clj b/src/formatting_stack/strategies/impl.clj index 60d821c4..f191dd1a 100644 --- a/src/formatting_stack/strategies/impl.clj +++ b/src/formatting_stack/strategies/impl.clj @@ -46,9 +46,9 @@ (-> decl parse/deps-from-ns-decl) ;; no exceptions thrown true) true))) - (catch Exception e + (catch Exception _ false) - (catch AssertionError e + (catch AssertionError _ false)))) (defn extract-clj-files [files] diff --git a/src/formatting_stack/util.clj b/src/formatting_stack/util.clj index 45149ed2..ad50fda7 100644 --- a/src/formatting_stack/util.clj +++ b/src/formatting_stack/util.clj @@ -1,5 +1,6 @@ (ns formatting-stack.util (:require + [clojure.spec.alpha :as spec] [clojure.tools.namespace.file :as file] [clojure.tools.namespace.parse :as parse] [medley.core :refer [find-first]] @@ -8,6 +9,7 @@ [nedap.utils.collections.seq :refer [distribute-evenly-by]] [nedap.utils.spec.predicates :refer [present-string?]]) (:import + (clojure.lang IBlockingDeref IPending) (java.io File))) (defmacro rcomp @@ -54,20 +56,20 @@ *flush-on-newline* true] ~@forms)) -(defn report-processing-error [^Throwable e filename] - (let [s (->> e - .getStackTrace - (map (fn [x] - (str " " x))) - (interpose "\n"))] - (println (apply str - "Encountered an exception, processing file: " - filename - ". The exception will be printed in the next line. " - "formatting-stack execution has *not* been aborted.\n" - (-> e .getMessage) - "\n" - s)))) +(speced/defn report-processing-error [^Throwable e, filename] + {:level :exception + :source :formatting-stack/report-processing-error + :filename filename + :msg "Encountered an exception" + :exception e}) + +(spec/def ::non-lazy-result + (fn [x] + (cond + (sequential? x) (vector? x) + (instance? IBlockingDeref x) false + (instance? IPending x) false + true true))) (defn process-in-parallel! [f files] (->> files @@ -75,7 +77,11 @@ (-> (File. filename) .length))}) (partitioning-pmap (bound-fn [filename] (try - (f filename) + (let [v (f filename)] + (assert (spec/valid? ::non-lazy-result v) + (pr-str "Parallel processing shouldn't return lazy computations" + f)) + v) (catch Exception e (report-processing-error e filename)) (catch AssertionError e @@ -104,3 +110,20 @@ (if (coll? x) x [x])) + +;; Rationale: https://github.com/nedap/formatting-stack/pull/109/files#r376891779 +(speced/defn ensure-sequential [^some? x] + (if (sequential? x) + x + [x])) + +(def ansi-colors + {:reset "[0m" + :red "[031m" + :green "[032m" + :yellow "[033m" + :cyan "[036m" + :grey "[037m"}) + +(defn colorize [s color] + (str \u001b (ansi-colors color) s \u001b (ansi-colors :reset))) diff --git a/test-resources/eastwood_warning.clj b/test-resources/eastwood_warning.clj new file mode 100644 index 00000000..e3d1f501 --- /dev/null +++ b/test-resources/eastwood_warning.clj @@ -0,0 +1,3 @@ +(ns eastwood-warning) + +(def x (def y ::z)) diff --git a/test-resources/invalid_syntax.clj b/test-resources/invalid_syntax.clj new file mode 100644 index 00000000..6893e8da --- /dev/null +++ b/test-resources/invalid_syntax.clj @@ -0,0 +1 @@ +#{] diff --git a/test-resources/kondo_warning.clj b/test-resources/kondo_warning.clj new file mode 100644 index 00000000..5d94f1b8 --- /dev/null +++ b/test-resources/kondo_warning.clj @@ -0,0 +1,4 @@ +(ns kondo-warning) + +(let [unused ::unused] + ::return) diff --git a/test-resources/ns_aliases_warning.clj b/test-resources/ns_aliases_warning.clj new file mode 100644 index 00000000..d3c1e771 --- /dev/null +++ b/test-resources/ns_aliases_warning.clj @@ -0,0 +1,3 @@ +(ns kondo-warning + (:require + [clojure.string :as foo])) diff --git a/test/functional/formatting_stack/component.clj b/test/functional/formatting_stack/component.clj index fe593e20..c26a50fa 100644 --- a/test/functional/formatting_stack/component.clj +++ b/test/functional/formatting_stack/component.clj @@ -1,8 +1,9 @@ (ns functional.formatting-stack.component (:require - [clojure.test :refer :all] + [clojure.test :refer [deftest is testing]] [com.stuartsierra.component :as component] - [formatting-stack.component :as sut])) + [formatting-stack.component :as sut] + [formatting-stack.reporters.passthrough :as reporters.passthrough])) (deftest works (testing "It can be started/stopped without errors" @@ -13,7 +14,7 @@ :linters [] :processors [] :in-background? false - :intersperse-newlines? false} + :reporter (reporters.passthrough/new)} instance (sut/new opts)] (is (= instance (component/start instance))) diff --git a/test/functional/formatting_stack/formatters/clean_ns.clj b/test/functional/formatting_stack/formatters/clean_ns.clj index cef4e659..dc2a6476 100644 --- a/test/functional/formatting_stack/formatters/clean_ns.clj +++ b/test/functional/formatting_stack/formatters/clean_ns.clj @@ -1,6 +1,6 @@ (ns functional.formatting-stack.formatters.clean-ns (:require - [clojure.test :refer :all] + [clojure.test :refer [are deftest is]] [formatting-stack.formatters.clean-ns :as sut] [formatting-stack.formatters.clean-ns.impl :as impl :refer [ns-form-of]] [formatting-stack.formatters.how-to-ns] diff --git a/test/functional/formatting_stack/formatters/clean_ns/impl.clj b/test/functional/formatting_stack/formatters/clean_ns/impl.clj index 90f9cf37..7f66061a 100644 --- a/test/functional/formatting_stack/formatters/clean_ns/impl.clj +++ b/test/functional/formatting_stack/formatters/clean_ns/impl.clj @@ -1,7 +1,7 @@ (ns functional.formatting-stack.formatters.clean-ns.impl (:require [clojure.java.io :as io] - [clojure.test :refer :all] + [clojure.test :refer [are deftest]] [formatting-stack.formatters.clean-ns.impl :as sut])) (deftest has-duplicate-requires? diff --git a/test/functional/formatting_stack/linters/eastwood.clj b/test/functional/formatting_stack/linters/eastwood.clj new file mode 100644 index 00000000..3bddf70f --- /dev/null +++ b/test/functional/formatting_stack/linters/eastwood.clj @@ -0,0 +1,18 @@ +(ns functional.formatting-stack.linters.eastwood + (:require + [clojure.test :refer [are deftest]] + [formatting-stack.linters.eastwood :as sut] + [formatting-stack.protocols.linter :as linter] + [matcher-combinators.matchers :as matchers] + [matcher-combinators.test :refer [match?]])) + +(deftest lint! + (let [linter (sut/new {})] + (are [filename expected] (match? expected + (linter/lint! linter [filename])) + "test-resources/eastwood_warning.clj" + (matchers/embeds + [{:source :eastwood/def-in-def + :line 3 + :column 13 + :filename "test-resources/eastwood_warning.clj"}])))) diff --git a/test/functional/formatting_stack/linters/kondo.clj b/test/functional/formatting_stack/linters/kondo.clj new file mode 100644 index 00000000..d58c66c4 --- /dev/null +++ b/test/functional/formatting_stack/linters/kondo.clj @@ -0,0 +1,28 @@ +(ns functional.formatting-stack.linters.kondo + (:require + [clojure.test :refer [are deftest]] + [formatting-stack.linters.kondo :as sut] + [formatting-stack.protocols.linter :as linter] + [matcher-combinators.matchers :as matchers] + [matcher-combinators.test :refer [match?]])) + +(deftest lint! + (let [linter (sut/new {:kondo-clj-options {:output {:exclude-files []} + :linters {:unused-binding {:level :warning}}}})] + (are [filename expected] (match? expected + (linter/lint! linter [filename])) + "test-resources/invalid_syntax.clj" + (matchers/embeds + [{:level :error, + :filename "test-resources/invalid_syntax.clj", + :line 1, + :column 2, + :source :kondo/syntax}]) + + "test-resources/kondo_warning.clj" + (matchers/embeds + [{:source :kondo/unused-binding + :level :warning + :line 3 + :column 7 + :filename "test-resources/kondo_warning.clj"}])))) diff --git a/test/functional/formatting_stack/linters/line_length.clj b/test/functional/formatting_stack/linters/line_length.clj new file mode 100644 index 00000000..36adb174 --- /dev/null +++ b/test/functional/formatting_stack/linters/line_length.clj @@ -0,0 +1,20 @@ +(ns functional.formatting-stack.linters.line-length + (:require + [clojure.test :refer [are deftest]] + [formatting-stack.linters.line-length :as sut] + [formatting-stack.protocols.linter :as linter] + [matcher-combinators.test :refer [match?]])) + +(deftest lint! + (let [linter (sut/new {:max-line-length 22})] + (are [filename expected] (match? expected + (linter/lint! linter [filename])) + "test-resources/invalid_syntax.clj" + [] + + "test-resources/sample_clj_ns.clj" + [{:source :formatting-stack/line-length + :line 3 + :column 25 + :msg "Line exceeding 22 columns." + :filename "test-resources/sample_clj_ns.clj"}]))) diff --git a/test/functional/formatting_stack/linters/loc_per_ns.clj b/test/functional/formatting_stack/linters/loc_per_ns.clj new file mode 100644 index 00000000..9e6e165f --- /dev/null +++ b/test/functional/formatting_stack/linters/loc_per_ns.clj @@ -0,0 +1,20 @@ +(ns functional.formatting-stack.linters.loc-per-ns + (:require + [clojure.test :refer [are deftest]] + [formatting-stack.linters.loc-per-ns :as sut] + [formatting-stack.protocols.linter :as linter] + [matcher-combinators.test :refer [match?]])) + +(deftest lint! + (let [linter (sut/new {:max-lines-per-ns 4})] + (are [filename expected] (match? expected + (linter/lint! linter [filename])) + "test-resources/invalid_syntax.clj" + [] + + "test-resources/sample_clj_ns.clj" + [{:source :formatting-stack/loc-per-ns + :line 5 + :column 0 + :msg "Longer than 4 LOC." + :filename "test-resources/sample_clj_ns.clj"}]))) diff --git a/test/functional/formatting_stack/linters/ns_aliases.clj b/test/functional/formatting_stack/linters/ns_aliases.clj new file mode 100644 index 00000000..68321083 --- /dev/null +++ b/test/functional/formatting_stack/linters/ns_aliases.clj @@ -0,0 +1,25 @@ +(ns functional.formatting-stack.linters.ns-aliases + (:require + [clojure.test :refer [are deftest]] + [formatting-stack.linters.ns-aliases :as sut] + [formatting-stack.protocols.linter :as linter] + [matcher-combinators.test :refer [match?]])) + +(deftest lint! + (let [linter (sut/new {:max-lines-per-ns 4})] + (are [filename expected] (match? expected + (linter/lint! linter [filename])) + "test-resources/invalid_syntax.clj" + [{:source :formatting-stack/report-processing-error + :filename "test-resources/invalid_syntax.clj" + :msg "Encountered an exception" + :level :exception + :exception #(instance? Throwable %)}] + + "test-resources/ns_aliases_warning.clj" + [{:source :formatting-stack/ns-aliases + :line 3 + :column 4 + :warning-details-url "https://stuartsierra.com/2015/05/10/clojure-namespace-aliases" + :msg "[clojure.string :as foo] is not a derived alias." + :filename "test-resources/ns_aliases_warning.clj"}]))) diff --git a/test/integration/formatting_stack/linters/one_resource_per_ns.clj b/test/integration/formatting_stack/linters/one_resource_per_ns.clj index 41e37241..1f1b8c55 100644 --- a/test/integration/formatting_stack/linters/one_resource_per_ns.clj +++ b/test/integration/formatting_stack/linters/one_resource_per_ns.clj @@ -1,6 +1,6 @@ (ns integration.formatting-stack.linters.one-resource-per-ns (:require - [clojure.test :refer :all] + [clojure.test :refer [are deftest is testing]] [formatting-stack.linters.one-resource-per-ns :as sut] [formatting-stack.test-helpers :as test-helpers] [formatting-stack.util :refer [rcomp]])) diff --git a/test/integration/formatting_stack/strategies/impl.clj b/test/integration/formatting_stack/strategies/impl.clj index 6e6dcedf..0a2349f6 100644 --- a/test/integration/formatting_stack/strategies/impl.clj +++ b/test/integration/formatting_stack/strategies/impl.clj @@ -1,6 +1,6 @@ (ns integration.formatting-stack.strategies.impl (:require - [clojure.test :refer :all] + [clojure.test :refer [are deftest]] [formatting-stack.strategies.impl :as sut]) (:import (java.io File))) diff --git a/test/unit/formatting_stack/component/impl.clj b/test/unit/formatting_stack/component/impl.clj index 2dd1ab47..ea9fdba9 100644 --- a/test/unit/formatting_stack/component/impl.clj +++ b/test/unit/formatting_stack/component/impl.clj @@ -1,6 +1,6 @@ (ns unit.formatting-stack.component.impl (:require - [clojure.test :refer :all] + [clojure.test :refer [are deftest is testing]] [formatting-stack.component.impl :as sut])) (deftest parse-options diff --git a/test/unit/formatting_stack/formatters/cljfmt/impl.clj b/test/unit/formatting_stack/formatters/cljfmt/impl.clj index 9a10af08..0c76731d 100644 --- a/test/unit/formatting_stack/formatters/cljfmt/impl.clj +++ b/test/unit/formatting_stack/formatters/cljfmt/impl.clj @@ -1,7 +1,7 @@ (ns unit.formatting-stack.formatters.cljfmt.impl (:require [clojure.java.io :as io] - [clojure.test :refer :all] + [clojure.test :refer [are deftest is testing]] [formatting-stack.formatters.cljfmt.impl :as sut] [unit.formatting-stack.formatters.cljfmt.impl.sample-data :refer [foo]]) (:import diff --git a/test/unit/formatting_stack/formatters/cljfmt/impl/magic_symbols.clj b/test/unit/formatting_stack/formatters/cljfmt/impl/magic_symbols.clj index 13a4949e..480ac3c5 100644 --- a/test/unit/formatting_stack/formatters/cljfmt/impl/magic_symbols.clj +++ b/test/unit/formatting_stack/formatters/cljfmt/impl/magic_symbols.clj @@ -1,7 +1,7 @@ (ns unit.formatting-stack.formatters.cljfmt.impl.magic-symbols (:require [clojure.java.io :as io] - [clojure.test :refer :all] + [clojure.test :refer [are deftest is]] [formatting-stack.formatters.cljfmt.impl :as sut])) (deftest works diff --git a/test/unit/formatting_stack/formatters/no_extra_blank_lines.clj b/test/unit/formatting_stack/formatters/no_extra_blank_lines.clj index 2739fd56..b8c569e4 100644 --- a/test/unit/formatting_stack/formatters/no_extra_blank_lines.clj +++ b/test/unit/formatting_stack/formatters/no_extra_blank_lines.clj @@ -1,6 +1,6 @@ (ns unit.formatting-stack.formatters.no-extra-blank-lines (:require - [clojure.test :refer :all] + [clojure.test :refer [are deftest]] [formatting-stack.formatters.no-extra-blank-lines :as sut])) (deftest without-extra-newlines diff --git a/test/unit/formatting_stack/formatters/trivial_ns_duplicates.clj b/test/unit/formatting_stack/formatters/trivial_ns_duplicates.clj index 61d48590..63e54057 100644 --- a/test/unit/formatting_stack/formatters/trivial_ns_duplicates.clj +++ b/test/unit/formatting_stack/formatters/trivial_ns_duplicates.clj @@ -1,6 +1,6 @@ (ns unit.formatting-stack.formatters.trivial-ns-duplicates (:require - [clojure.test :refer :all] + [clojure.test :refer [are deftest is testing]] [formatting-stack.formatters.trivial-ns-duplicates :as sut] [formatting-stack.util.ns :as util.ns])) @@ -30,8 +30,10 @@ '[[a :refer [b c]] [a :refer [b c d]]] '[[a :refer [b c d]]])) (deftest remove-exact-duplicates - (are [desc input expected] (= expected - (sut/remove-exact-duplicates input)) + (are [desc input expected] (testing desc + (is (= expected + (sut/remove-exact-duplicates input))) + true) "returns nil when there's nothing to fix" '(ns foo) diff --git a/test/unit/formatting_stack/indent_specs.clj b/test/unit/formatting_stack/indent_specs.clj index 23fff490..3983a93d 100644 --- a/test/unit/formatting_stack/indent_specs.clj +++ b/test/unit/formatting_stack/indent_specs.clj @@ -1,6 +1,6 @@ (ns unit.formatting-stack.indent-specs (:require - [clojure.test :refer :all] + [clojure.test :refer [deftest is testing]] [formatting-stack.formatters.cljfmt.impl :as cljfmt.impl] [formatting-stack.indent-specs :as sut] [formatting-stack.processors.cider :as processors.cider] diff --git a/test/unit/formatting_stack/linters/ns_aliases.clj b/test/unit/formatting_stack/linters/ns_aliases.clj index 2ea756a2..a449e773 100644 --- a/test/unit/formatting_stack/linters/ns_aliases.clj +++ b/test/unit/formatting_stack/linters/ns_aliases.clj @@ -1,6 +1,6 @@ (ns unit.formatting-stack.linters.ns-aliases (:require - [clojure.test :refer :all] + [clojure.test :refer [are deftest]] [formatting-stack.linters.ns-aliases :as sut])) (deftest name-and-alias diff --git a/test/unit/formatting_stack/linters/one_resource_per_ns.clj b/test/unit/formatting_stack/linters/one_resource_per_ns.clj index e9ae3191..c1800187 100644 --- a/test/unit/formatting_stack/linters/one_resource_per_ns.clj +++ b/test/unit/formatting_stack/linters/one_resource_per_ns.clj @@ -1,6 +1,6 @@ (ns unit.formatting-stack.linters.one-resource-per-ns (:require - [clojure.test :refer :all] + [clojure.test :refer [are deftest is testing]] [formatting-stack.linters.one-resource-per-ns :as sut] [formatting-stack.test-helpers :as test-helpers])) diff --git a/test/unit/formatting_stack/processors/test_runner/impl.clj b/test/unit/formatting_stack/processors/test_runner/impl.clj index e88fb1c9..db17acd7 100644 --- a/test/unit/formatting_stack/processors/test_runner/impl.clj +++ b/test/unit/formatting_stack/processors/test_runner/impl.clj @@ -1,7 +1,7 @@ (ns unit.formatting-stack.processors.test-runner.impl (:require [clojure.string :as string] - [clojure.test :refer :all] + [clojure.test :refer [are deftest is testing]] [formatting-stack.processors.test-runner.impl :as sut] [formatting-stack.project-parsing :refer [project-namespaces]] [nedap.speced.def :as speced])) diff --git a/test/unit/formatting_stack/reporters/pretty_printer.clj b/test/unit/formatting_stack/reporters/pretty_printer.clj new file mode 100644 index 00000000..c3eceadc --- /dev/null +++ b/test/unit/formatting_stack/reporters/pretty_printer.clj @@ -0,0 +1,55 @@ +(ns unit.formatting-stack.reporters.pretty-printer + (:require + [clojure.test :refer [are deftest is testing]] + [formatting-stack.reporters.pretty-printer :as sut])) + +(deftest truncate-line-wise + (are [input expected] (testing input + (is (= expected + (sut/truncate-line-wise input 5))) + true) + "" "" + "a" "a" + "aaaaa" "aaaaa" + "aaaaaEXCEEDING" "aaaa…" + "\n" "\n" + "a\na" "a\na" + "a\naaaaaEXCEEDING" "a\naaaa…" + "aaaaaEXCEEDING\na" "aaaa…\na")) + +(deftest print-warnings + (are [desc input expected] (testing [desc input] + (is (= expected + (with-out-str + (sut/print-warnings {:colorize? false + :max-msg-length 20} + input)))) + true) + "Empty" + [] + "" + + "Basic" + [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0}] + "filename\n :unit.formatting-stack.reporters.pretty-printer/source\n 0:0 message\n\n" + + "Sorts by `:line`" + [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0} + {:filename "filename", :msg "message", :source ::source, :level :warning, :line 2 :column 2} + {:filename "filename", :msg "message", :source ::source, :level :warning, :line 1 :column 1}] + "filename\n :unit.formatting-stack.reporters.pretty-printer/source\n 0:0 message\n 1:1 message\n 2:2 message\n\n" + + "Groups by `:source`" + [{:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 0 :column 0} + {:filename "filename", :msg "message", :source ::source-B, :level :warning, :line 2 :column 2} + {:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 1 :column 1}] + "filename\n :unit.formatting-stack.reporters.pretty-printer/source-A\n 0:0 message\n 1:1 message\n :unit.formatting-stack.reporters.pretty-printer/source-B\n 2:2 message\n\n" + + "Can print a given `:warning-details-url`, once at most per `:source` group" + [{:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 0 :column 0} + {:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 1 :column 1 :warning-details-url "http://example.test/foo"}] + "filename\n :unit.formatting-stack.reporters.pretty-printer/source-A\n See: http://example.test/foo\n 0:0 message\n 1:1 message\n\n" + + "Can print `:msg-extra-data` (at the correct indentation level)" + [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0, :msg-extra-data ["Foo" "Bar"]}] + "filename\n :unit.formatting-stack.reporters.pretty-printer/source\n 0:0 message\n Foo\n Bar\n\n")) diff --git a/test/unit/formatting_stack/strategies.clj b/test/unit/formatting_stack/strategies.clj index eb83a4f4..38ba777c 100644 --- a/test/unit/formatting_stack/strategies.clj +++ b/test/unit/formatting_stack/strategies.clj @@ -1,7 +1,7 @@ (ns unit.formatting-stack.strategies (:require [clojure.string :as str] - [clojure.test :refer :all] + [clojure.test :refer [deftest is use-fixtures]] [formatting-stack.strategies :as sut] [formatting-stack.strategies.impl :as sut.impl])) diff --git a/worker/formatting_stack/kondo_classpath_cache.clj b/worker/formatting_stack/kondo_classpath_cache.clj new file mode 100644 index 00000000..c4efa2ed --- /dev/null +++ b/worker/formatting_stack/kondo_classpath_cache.clj @@ -0,0 +1,25 @@ +(ns formatting-stack.kondo-classpath-cache + "Holds a cache for the entire classpath, to make better use of the project-wide analysis capabilities. + + Only needs to be created once, as the classpath never changes." + (:require + [clj-kondo.core :as kondo] + [clojure.string :as string]) + (:import + (java.io File))) + +;; Use kondo's official default config dir, so that we don't bloat consumers' project layouts: +(def cache-parent-dir ".clj-kondo") + +;; Don't use .clj-kondo directly since it can be accessed concurrently (e.g. f-s + a second Kondo from VS Code): +(def cache-subdir "formatting-stack-cache") + +(def cache-dir (str cache-parent-dir File/separator cache-subdir)) + +(def classpath-cache + (future + (let [files (-> (System/getProperty "java.class.path") + (string/split #"\:"))] + (-> (File. cache-parent-dir cache-subdir) .mkdirs) + (kondo/run! {:lint files + :cache-dir cache-dir}))))