├── .circleci ├── config.yml └── e2e.sh ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature.md │ └── technical_improvement.md ├── PULL_REQUEST_TEMPLATE │ └── ncrw.md ├── contributing.md └── pull_request_template.md ├── .gitignore ├── LICENSE ├── README.md ├── dev ├── dev.clj └── user.clj ├── e2e └── monorepo-support │ ├── project.clj │ ├── src │ └── .keep │ └── test │ └── monorepo_support │ └── core_test.clj ├── nedap.lein-template.properties ├── project.clj ├── src └── formatting_stack │ ├── branch_formatter.clj │ ├── component.clj │ ├── component │ └── impl.clj │ ├── core.clj │ ├── defaults.clj │ ├── formatters │ ├── clean_ns.clj │ ├── clean_ns │ │ └── impl.clj │ ├── cljfmt.clj │ ├── cljfmt │ │ └── impl.clj │ ├── how_to_ns.clj │ ├── newlines.clj │ ├── no_extra_blank_lines.clj │ └── trivial_ns_duplicates.clj │ ├── indent_specs.clj │ ├── integrant.clj │ ├── linters │ ├── eastwood.clj │ ├── eastwood │ │ └── impl.clj │ ├── kondo.clj │ ├── line_length.clj │ ├── loc_per_ns.clj │ ├── ns_aliases.clj │ ├── ns_aliases │ │ └── impl.clj │ └── one_resource_per_ns.clj │ ├── processors │ ├── cider.clj │ ├── test_runner.clj │ └── test_runner │ │ └── impl.clj │ ├── project_formatter.clj │ ├── project_parsing.clj │ ├── protocols │ ├── formatter.clj │ ├── linter.clj │ ├── processor.clj │ ├── reporter.clj │ └── spec.clj │ ├── reporters │ ├── file_writer.clj │ ├── impl.clj │ ├── passthrough.clj │ ├── pretty_line_printer.clj │ └── pretty_printer.clj │ ├── strategies.clj │ ├── strategies │ ├── impl.clj │ └── impl │ │ ├── git_diff.clj │ │ └── git_status.clj │ ├── util.clj │ └── util │ ├── diff.clj │ └── ns.clj ├── test-resources-extra ├── orpn.clj ├── orpn.cljc └── orpn.cljs ├── test-resources ├── component_repl.clj ├── customization_example.clj ├── deletable.clj ├── diffs │ ├── 1.diff │ ├── 2.diff │ ├── 3.diff │ ├── 4.diff │ ├── 5.diff │ ├── 6.diff │ ├── 7.diff │ ├── 8.diff │ ├── files │ │ ├── 1.diff │ │ ├── 1.txt │ │ ├── 1_reversed.diff │ │ ├── 1_revised.txt │ │ ├── 2.diff │ │ ├── 2.txt │ │ ├── 2_reversed.diff │ │ └── 2_revised.txt │ └── incorrect.diff ├── eastwood_warning.clj ├── extra_newlines_warning.clj ├── integrant_repl.clj ├── invalid_syntax.clj ├── kondo_warning.clj ├── linelength_sample.clj ├── linting_formatters_example.clj ├── magic.clj ├── ns_aliases_warning.clj ├── ns_unordered.clj ├── orpn.clj ├── orpn.cljc ├── orpn.cljs ├── sample_clj_ns.clj ├── sample_cljc_ns.cljc ├── sample_cljs_ns.cljs ├── valid_syntax.clj └── wrong_indent.clj ├── test ├── formatting_stack │ ├── global_test_setup.clj │ └── test_helpers.clj ├── functional │ └── formatting_stack │ │ ├── component.clj │ │ ├── core.clj │ │ ├── formatters │ │ ├── clean_ns.clj │ │ ├── clean_ns │ │ │ ├── does_not_have_duplicates.clj │ │ │ ├── has_duplicates.clj │ │ │ ├── has_duplicates_2.clj │ │ │ ├── has_duplicates_3.clj │ │ │ ├── impl.clj │ │ │ ├── should_be_cleaned.clj │ │ │ ├── should_not_be_cleaned.clj │ │ │ ├── should_not_be_cleaned_2.clj │ │ │ ├── should_not_be_cleaned_3.clj │ │ │ ├── should_not_be_cleaned_4.clj │ │ │ ├── should_not_be_cleaned_5.clj │ │ │ ├── should_not_be_partially_cleaned.clj │ │ │ ├── specs.clj │ │ │ └── specs │ │ │ │ └── foo.clj │ │ ├── cljfmt.clj │ │ ├── how_to_ns.clj │ │ ├── newlines.clj │ │ └── no_extra_blank_lines.clj │ │ ├── integrant.clj │ │ ├── kondo_classpath_cache.clj │ │ └── linters │ │ ├── eastwood.clj │ │ ├── kondo.clj │ │ ├── line_length.clj │ │ ├── loc_per_ns.clj │ │ └── ns_aliases.clj ├── integration │ └── formatting_stack │ │ ├── linters │ │ └── one_resource_per_ns.clj │ │ ├── processors │ │ └── test_runner.clj │ │ ├── strategies.clj │ │ └── strategies │ │ ├── default_branch_name.clj │ │ └── impl.clj ├── specs │ └── foo.clj └── unit │ └── formatting_stack │ ├── component │ └── impl.clj │ ├── formatters │ ├── clean_ns.clj │ ├── cljfmt │ │ ├── impl.clj │ │ └── impl │ │ │ ├── magic_symbols.clj │ │ │ └── sample_data.clj │ ├── no_extra_blank_lines.clj │ └── trivial_ns_duplicates.clj │ ├── indent_specs.clj │ ├── linters │ ├── ns_aliases.clj │ ├── ns_aliases │ │ └── impl.clj │ └── one_resource_per_ns.clj │ ├── processors │ └── test_runner │ │ └── impl.clj │ ├── reporters │ ├── impl.clj │ ├── pretty_line_printer.clj │ └── pretty_printer.clj │ ├── strategies.clj │ ├── strategies │ └── impl │ │ ├── git_diff.clj │ │ └── git_status.clj │ ├── util.clj │ └── util │ └── diff.clj └── worker └── formatting_stack ├── background.clj └── kondo_classpath_cache.clj /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | setup-env: 5 | steps: 6 | - checkout 7 | - restore_cache: 8 | keys: 9 | - v2-dependencies-{{ checksum "project.clj" }} 10 | # fallback to using the latest cache if no exact match is found 11 | - v2-dependencies- 12 | - run: lein with-profile -user,-dev,+ci,+test,+refactor-nrepl deps 13 | - save_cache: 14 | paths: 15 | - ~/.m2 16 | key: v2-dependencies-{{ checksum "project.clj" }} 17 | 18 | executor_defaults: &executor_defaults 19 | working_directory: ~/repo 20 | 21 | executors: 22 | openjdk8: 23 | docker: 24 | - image: circleci/clojure:openjdk-8-lein-2.9.1 25 | environment: 26 | LEIN_ROOT: "true" 27 | JVM_OPTS: -Xmx3200m 28 | <<: *executor_defaults 29 | openjdk8-parallel: 30 | docker: 31 | - image: circleci/clojure:openjdk-8-lein-2.9.1 32 | # NOTE: the following is good to shrink to `large` after a couple weeks: 33 | resource_class: xlarge 34 | environment: 35 | LEIN_ROOT: "true" 36 | JVM_OPTS: -Xmx3200m 37 | <<: *executor_defaults 38 | openjdk11: 39 | docker: 40 | - image: circleci/clojure:openjdk-11-lein-2.9.1 41 | environment: 42 | LEIN_ROOT: "true" 43 | JVM_OPTS: -Xmx3200m --illegal-access=deny 44 | <<: *executor_defaults 45 | openjdk11-parallel: 46 | docker: 47 | - image: circleci/clojure:openjdk-11-lein-2.9.1 48 | # NOTE: the following is good to shrink to `large` after a couple weeks: 49 | resource_class: xlarge 50 | environment: 51 | LEIN_ROOT: "true" 52 | JVM_OPTS: -Xmx3200m --illegal-access=deny 53 | <<: *executor_defaults 54 | 55 | jobs: 56 | test_code: 57 | description: | 58 | Runs tests against given version of the JDK 59 | parameters: 60 | jdk_version: 61 | description: Version of the JDK to test against 62 | type: string 63 | lein_test_command: 64 | description: A Leiningen command that will run a test suite 65 | type: string 66 | executor: << parameters.jdk_version >> 67 | steps: 68 | - setup-env 69 | - run: 70 | command: << parameters.lein_test_command >> 71 | - run: 72 | command: .circleci/e2e.sh 73 | - run: 74 | command: lein with-profile -user,+test eastwood 75 | deploy: 76 | executor: openjdk8 77 | steps: 78 | - setup-env 79 | - run: 80 | name: import GPG key 81 | command: | 82 | export GPG_TTY=$(tty) 83 | echo -e "$GPG_KEY_V2" | gpg --no-tty --batch --passphrase "$GPG_KEY_V2_PASSPHRASE" --pinentry-mode loopback --import 84 | - run: 85 | name: Perform pre-release sanity check 86 | command: lein with-profile -user,-dev,+ci,+ncrw run -m nedap.ci.release-workflow.api sanity-check 87 | - run: 88 | name: release to JFrog 89 | command: lein with-profile -user deploy 90 | - run: 91 | name: release to Clojars 92 | command: lein with-profile -user deploy clojars 93 | 94 | test_code_filters: &test_code_filters 95 | filters: 96 | branches: 97 | only: /.*/ 98 | tags: 99 | only: /^v\d+\.\d+\.\d+(-alpha\d+)?$/ 100 | 101 | workflows: 102 | version: 2.1 103 | ci-test-matrix: 104 | jobs: 105 | - test_code: 106 | name: "JDK 8 including refactor-nrepl" 107 | jdk_version: openjdk8 108 | lein_test_command: lein with-profile -user,-dev,+ci,+refactor-nrepl do clean, test 109 | <<: *test_code_filters 110 | - test_code: 111 | name: "JDK 8 excluding refactor-nrepl" 112 | jdk_version: openjdk8 113 | lein_test_command: lein with-profile -user,-dev,+ci do clean, test 114 | <<: *test_code_filters 115 | - test_code: 116 | name: "JDK 8, with an old clojurescript dependency on the classpath" 117 | jdk_version: openjdk8 118 | lein_test_command: lein with-profile -user,-dev,+ci,+cljs-old do clean, test 119 | <<: *test_code_filters 120 | - test_code: 121 | name: "JDK 11 including refactor-nrepl" 122 | jdk_version: openjdk11 123 | lein_test_command: lein with-profile -user,-dev,+ci,+refactor-nrepl do clean, test 124 | <<: *test_code_filters 125 | - test_code: 126 | name: "JDK 11 excluding refactor-nrepl" 127 | jdk_version: openjdk11 128 | lein_test_command: lein with-profile -user,-dev,+ci do clean, test 129 | <<: *test_code_filters 130 | - deploy: 131 | context: JFrog 132 | requires: 133 | - "JDK 8, with an old clojurescript dependency on the classpath" 134 | - "JDK 8 including refactor-nrepl" 135 | - "JDK 8 excluding refactor-nrepl" 136 | - "JDK 11 including refactor-nrepl" 137 | - "JDK 11 excluding refactor-nrepl" 138 | filters: 139 | branches: 140 | ignore: /.*/ 141 | tags: 142 | only: /^v\d+\.\d+\.\d+(-alpha\d+)?$/ 143 | -------------------------------------------------------------------------------- /.circleci/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | project_dir=$PWD 4 | set -Eeuxo pipefail 5 | cd e2e/monorepo-support 6 | 7 | 8 | if [[ ! -e $project_dir/.git ]]; then 9 | echo "Expected formatting-stack to be a Git repo" 10 | exit 1 11 | fi 12 | 13 | if [[ -e .git ]]; then 14 | echo "monorepo-support is meant to emulate monorepos. It should not have its own Git repository; that would alter formatting-stack output" 15 | exit 1 16 | fi 17 | 18 | mkdir checkouts 19 | cd checkouts 20 | ln -s $project_dir formatting-stack 21 | cd .. 22 | echo "(ns foo)" > src/foo.clj 23 | echo "(ns bar)" > src/bar.clj 24 | git add src/foo.clj 25 | lein with-profile -dev do clean, test 26 | git reset src/foo.clj 27 | rm src/foo.clj 28 | rm src/bar.clj 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | title: '' 5 | labels: 'Bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Brief 11 | 12 | 13 | 14 | ## Expected behavior 15 | 16 | ## Actual behavior 17 | 18 | ## Reproduction steps 19 | 20 | ## Suspected cause 21 | 22 | ## Screenshot, stacktrace, etc 23 | 24 | ## Environment info 25 | 26 | 27 | 28 | ## Additional links 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: Reflect or suggest a desired feature 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Context 11 | 12 | 13 | 14 | ## Task 15 | 16 | 17 | 18 | ## Acceptance criteria 19 | 20 | ## Additional resources 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/technical_improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Technical improvement 3 | about: Propose a technical improvement, such as a refactoring 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Problem statement 11 | 12 | 13 | 14 | ## Proposal 15 | 16 | 17 | 18 | ## Alternatives and comparison 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/ncrw.md: -------------------------------------------------------------------------------- 1 | Delivers: 2 | 3 | ## Release checklist (author) 4 | 5 | * [ ] All PRs / relevant commits since the previous release are listed in this PR's description 6 | * [ ] The new proposed version follows semver 7 | * [ ] The build passes 8 | * [ ] New features are (briefly) reflected in the README 9 | 10 | ## Release checklist (reviewer) 11 | 12 | * [ ] All PRs / relevant commits since the previous release are listed in this PR's description 13 | * [ ] The new proposed version follows semver 14 | * [ ] New features are (briefly) reflected in the README 15 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | * This library is OSS. Since it is developer tooling, we can accept PRs improving it. 2 | * Please create an issue first, following its template carefully and seeking agreement in advance. 3 | * Note that PRs have an extensive checklist. If you foresee contributing, please refer to a few past PRs so there are no surprises. 4 | * It seems reasonable to follow the [Clojure Etiquette](https://clojure.org/community/etiquette). 5 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Brief 2 | 3 | 4 | 5 | ## QA plan 6 | 7 | 8 | 9 | ## Author checklist 10 | 11 | 12 | 13 | * [ ] I have QAed the functionality 14 | * [ ] The PR has a reasonably reviewable size and a meaningful commit history 15 | * [ ] I have run the [branch formatter](https://github.com/nedap/formatting-stack/blob/332a419034ab46fad526a5592f4257353bd695b6/src/formatting_stack/branch_formatter.clj) and observed no new/significative warnings 16 | * [ ] The build passes 17 | * [ ] I have self-reviewed the PR prior to assignment 18 | * Additionally, I have code-reviewed iteratively the PR considering the following aspects in isolation: 19 | * [ ] Correctness 20 | * [ ] Robustness (red paths, failure handling etc) 21 | * [ ] Test coverage 22 | * [ ] Spec coverage 23 | * [ ] Documentation 24 | * [ ] Security 25 | * [ ] Performance 26 | * [ ] Breaking API changes 27 | * [ ] Cross-compatibility (Clojure/ClojureScript) 28 | 29 | ## Reviewer checklist 30 | 31 | * [ ] I have checked out this branch and reviewed it locally, running it 32 | * [ ] I have QAed the functionality 33 | * [ ] I have reviewed the PR 34 | * Additionally, I have code-reviewed iteratively the PR considering the following aspects in isolation: 35 | * [ ] Correctness 36 | * [ ] Robustness (red paths, failure handling etc) 37 | * [ ] Test coverage 38 | * [ ] Spec coverage 39 | * [ ] Documentation 40 | * [ ] Security 41 | * [ ] Performance 42 | * [ ] Breaking API changes 43 | * [ ] Cross-compatibility (Clojure/ClojureScript) 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.class 3 | *.iml 4 | *.jar 5 | *.log 6 | *.orig 7 | *.tmp 8 | *~ 9 | .*.swo 10 | .*.swp 11 | .DS_Store 12 | .eastwood 13 | .history 14 | .idea/ 15 | .nrepl-port 16 | .rebl/ 17 | .repl 18 | /.clj-kondo 19 | /.lein-* 20 | /checkouts 21 | /classes 22 | /git-integration-testing 23 | /logs/ 24 | /target 25 | build/ 26 | coverage/ 27 | out/ 28 | pom.xml 29 | pom.xml.asc 30 | profiles.clj 31 | repl-port 32 | -------------------------------------------------------------------------------- /dev/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:require 3 | [clj-java-decompiler.core :refer [decompile]] 4 | [clojure.java.javadoc :refer [javadoc]] 5 | [clojure.pprint :refer [pprint]] 6 | [clojure.repl :refer [apropos dir doc find-doc pst source]] 7 | [clojure.test :refer [run-all-tests run-tests]] 8 | [clojure.tools.namespace.repl :refer [clear refresh refresh-dirs set-refresh-dirs]] 9 | [criterium.core :refer [quick-bench]] 10 | [lambdaisland.deep-diff])) 11 | 12 | ;; * the "worker" source-path must be excluded. 13 | ;; * if updating this, please check if `formatting-stack.global-test-setup` also needs updating. 14 | (set-refresh-dirs "src" "test" "dev") 15 | 16 | (defn prepare-tests [] 17 | (clear) 18 | (alter-var-root #'clojure.test/*load-tests* (constantly true)) 19 | (refresh)) 20 | 21 | (defn suite [] 22 | (prepare-tests) 23 | (run-all-tests #".*\.formatting-stack.*")) 24 | 25 | (defn unit [] 26 | (prepare-tests) 27 | (run-all-tests #"unit\.formatting-stack.*")) 28 | 29 | (defn slow [] 30 | (prepare-tests) 31 | (run-all-tests #"integration\.formatting-stack.*")) 32 | 33 | (defn diff [x y] 34 | (-> x 35 | (lambdaisland.deep-diff/diff y) 36 | (lambdaisland.deep-diff/pretty-print))) 37 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clojure.java.io :as io])) 4 | 5 | ;; This ns exists to demonstrate that would-be-cleaned requires aren't, because of the special case of #{'user 'dev} 6 | -------------------------------------------------------------------------------- /e2e/monorepo-support/project.clj: -------------------------------------------------------------------------------- 1 | (defproject monorepo-support "unreleased" 2 | :dependencies [[org.clojure/clojure "1.10.1"] 3 | ;; the version doesn't matter a lot - it will be overriden via Lein checkouts: 4 | [formatting-stack "4.3.0"]]) 5 | -------------------------------------------------------------------------------- /e2e/monorepo-support/src/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedap/formatting-stack/6fc23a538aea3fd256b39647b412b70973091c9d/e2e/monorepo-support/src/.keep -------------------------------------------------------------------------------- /e2e/monorepo-support/test/monorepo_support/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns monorepo-support.core-test 2 | (:require 3 | [clojure.set :as set] 4 | [clojure.test :refer [are deftest is testing use-fixtures]] 5 | [formatting-stack.strategies]) 6 | (:import 7 | (java.io File))) 8 | 9 | (deftest e2e 10 | (let [all (set (formatting-stack.strategies/all-files :files [])) 11 | staged (set (formatting-stack.strategies/git-completely-staged :files [])) 12 | unstaged (set (formatting-stack.strategies/git-not-completely-staged :files [])) 13 | staged+unstaged (into staged unstaged)] 14 | (is (seq all)) 15 | (is (seq staged)) 16 | (is (seq unstaged)) 17 | 18 | (is (not= staged unstaged)) 19 | (is (> (count all) 20 | (count staged+unstaged))) 21 | (is (set/superset? all staged+unstaged)) 22 | 23 | (doseq [filename all 24 | :let [file (File. filename) 25 | absolute (-> file .getAbsolutePath)]] 26 | (is (= filename 27 | absolute) 28 | "Returns absolutized filenames, which is important for monorepo support") 29 | 30 | (is (-> file .exists) 31 | "Said absolutized filenames reflect files that actually exist")))) 32 | -------------------------------------------------------------------------------- /nedap.lein-template.properties: -------------------------------------------------------------------------------- 1 | nedap.lein-template.github_username=nedap 2 | nedap.lein-template.copyright_holder=Nedap 3 | nedap.lein-template.group_id=com.nedap.staffing-solutions 4 | nedap.lein-template.lib_prefix=formatting-stack 5 | nedap.lein-template.gpg_email=releases-staffingsolutions@nedap.com 6 | nedap.lein-template.use_system=false 7 | nedap.lein-template.use_cljs=false 8 | nedap.lein-template.use_clojars=true 9 | nedap.lein-template.do_license=true 10 | nedap.lein-template.use_cloverage=false 11 | nedap.lein-template.use_nvd=false 12 | nedap.lein-template.use_pedestal=false 13 | nedap.lein-template.use_ssr=false 14 | -------------------------------------------------------------------------------- /src/formatting_stack/component.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.component 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [formatting-stack.component.impl :refer [parse-options]] 5 | [formatting-stack.core :refer [format!]] 6 | [nedap.speced.def :as speced] 7 | [nedap.utils.modular.api :refer [implement]])) 8 | 9 | (speced/defn start [^map? this] 10 | (->> this 11 | parse-options 12 | (apply format!)) 13 | this) 14 | 15 | (defn new 16 | "Accepts a map of options to be passed to `formatting-stack.core/format!`. 17 | 18 | Returns a Component that invokes `#'format!` with said options on each `#'component/start` invocation." 19 | [this] 20 | (implement this 21 | component/start start)) 22 | -------------------------------------------------------------------------------- /src/formatting_stack/component/impl.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.component.impl 2 | (:require 3 | [formatting-stack.util :refer [rcomp]] 4 | [nedap.speced.def :as speced])) 5 | 6 | (speced/defn parse-options [^map? this] 7 | (->> this 8 | (remove (rcomp val nil?)) 9 | (apply concat))) 10 | -------------------------------------------------------------------------------- /src/formatting_stack/core.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.core 2 | (:require 3 | [clojure.main] 4 | [formatting-stack.background] 5 | [formatting-stack.defaults :refer [default-formatters default-linters default-processors default-strategies]] 6 | [formatting-stack.indent-specs :refer [default-third-party-indent-specs]] 7 | [formatting-stack.protocols.formatter :as protocols.formatter] 8 | [formatting-stack.protocols.linter :as protocols.linter] 9 | [formatting-stack.protocols.processor :as protocols.processor] 10 | [formatting-stack.protocols.reporter :refer [report]] 11 | [formatting-stack.reporters.impl :refer [normalize-filenames]] 12 | [formatting-stack.reporters.pretty-printer :as reporters.pretty-printer] 13 | [formatting-stack.util :refer [with-serialized-output]])) 14 | 15 | (defn files-from-strategies [strategies] 16 | (->> strategies 17 | (reduce (fn [files strategy] 18 | (strategy :files files)) 19 | []) 20 | distinct)) 21 | 22 | (defn process! [method members category-strategies default-strategies] 23 | ;; `memoize` rationale: results are cached not for performance, 24 | ;; but for avoiding the scenario where one `member` alters the git status, 25 | ;; so the subsequent `member`s' strategies won't perceive the same set of files than the first one. 26 | ;; e.g. cljfmt may operate upon `strategies/git-completely-staged`, formatting some files accordingly. 27 | ;; Then `how-to-ns`, which follows the same strategy, would perceive a dirty git status. 28 | ;; Accordingly it would do nothing, which is undesirable. 29 | (let [files (memoize (fn [strategies] 30 | (files-from-strategies strategies)))] 31 | (with-serialized-output 32 | (->> members 33 | (mapcat (fn [member] 34 | (let [{specific-strategies :strategies} member 35 | strategies (or specific-strategies category-strategies default-strategies)] 36 | (try 37 | (->> strategies files (method member)) 38 | (catch Exception e 39 | [{:exception e 40 | :source :formatting-stack/process! 41 | :msg (str "Exception during " member) 42 | :level :exception}]) 43 | (catch AssertionError e 44 | [{:exception e 45 | :source :formatting-stack/process! 46 | :msg (str "Exception during " member) 47 | :level :exception}]))))) 48 | (doall))))) 49 | 50 | (defn format! [& {:keys [strategies 51 | third-party-indent-specs 52 | formatters 53 | linters 54 | processors 55 | reporter 56 | in-background?]}] 57 | ;; the following `or` clauses ensure that Components don't pass nil values 58 | (let [strategies (or strategies default-strategies) 59 | third-party-indent-specs (or third-party-indent-specs default-third-party-indent-specs) 60 | formatters (or formatters (default-formatters third-party-indent-specs)) 61 | linters (or linters default-linters) 62 | processors (or processors (default-processors third-party-indent-specs)) 63 | reporter (or reporter (reporters.pretty-printer/new {})) 64 | in-background? (if (some? in-background?) 65 | in-background? 66 | true) 67 | {formatters-strategies :formatters 68 | linters-strategies :linters 69 | processors-strategies :processors} strategies 70 | impl (bound-fn [] ;; important that it's a bound-fn (for an undetermined reason) 71 | (->> [(process! protocols.formatter/format! formatters formatters-strategies strategies) 72 | (process! protocols.linter/lint! linters linters-strategies strategies) 73 | (process! protocols.processor/process! processors processors-strategies strategies)] 74 | (apply concat) 75 | (mapv normalize-filenames) 76 | (report reporter)))] 77 | (if in-background? 78 | (do 79 | (reset! formatting-stack.background/workload impl) 80 | 'Running...) 81 | (do 82 | (impl) 83 | 'Done)))) 84 | -------------------------------------------------------------------------------- /src/formatting_stack/defaults.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.defaults 2 | "A set of defaults apt for frequent usage. 3 | 4 | See also: `formatting-stack.branch-formatter`, `formatting-stack.project-formatter`" 5 | (:require 6 | [formatting-stack.formatters.clean-ns :as formatters.clean-ns] 7 | [formatting-stack.formatters.cljfmt :as formatters.cljfmt] 8 | [formatting-stack.formatters.how-to-ns :as formatters.how-to-ns] 9 | [formatting-stack.formatters.newlines :as formatters.newlines] 10 | [formatting-stack.formatters.no-extra-blank-lines :as formatters.no-extra-blank-lines] 11 | [formatting-stack.formatters.trivial-ns-duplicates :as formatters.trivial-ns-duplicates] 12 | [formatting-stack.linters.eastwood :as linters.eastwood] 13 | [formatting-stack.linters.kondo :as linters.kondo] 14 | [formatting-stack.linters.line-length :as linters.line-length] 15 | [formatting-stack.linters.loc-per-ns :as linters.loc-per-ns] 16 | [formatting-stack.linters.ns-aliases :as linters.ns-aliases] 17 | [formatting-stack.linters.one-resource-per-ns :as linters.one-resource-per-ns] 18 | [formatting-stack.processors.cider :as processors.cider] 19 | [formatting-stack.strategies :as strategies])) 20 | 21 | (def default-strategies [strategies/git-completely-staged]) 22 | 23 | (def extended-strategies [strategies/git-completely-staged 24 | strategies/git-not-completely-staged]) 25 | 26 | (defn default-formatters [third-party-indent-specs] 27 | (let [;; the following exists (for now) to guarantee that how-to-ns uses cached git results from cljfmt. 28 | ;; ideally the how-to-ns formatter would have an extra `files-with-a-namespace` strategy but that would break git caching, 29 | ;; making usage more awkward. 30 | ;; the strategies mechanism needs some rework to avoid this limitation. 31 | cached-strategies default-strategies] 32 | (->> [(-> (formatters.cljfmt/new {:third-party-indent-specs third-party-indent-specs}) 33 | (assoc :strategies cached-strategies)) 34 | (-> (formatters.how-to-ns/new {}) 35 | (assoc :strategies cached-strategies)) 36 | (formatters.no-extra-blank-lines/new) 37 | (formatters.newlines/new {}) 38 | (-> (formatters.trivial-ns-duplicates/new {}) 39 | (assoc :strategies (conj default-strategies 40 | strategies/files-with-a-namespace 41 | strategies/exclude-edn))) 42 | (when (strategies/refactor-nrepl-available?) 43 | (-> (formatters.clean-ns/new {}) 44 | (assoc :strategies (conj default-strategies 45 | strategies/files-with-a-namespace 46 | strategies/exclude-cljc 47 | strategies/exclude-cljs 48 | strategies/exclude-edn 49 | strategies/namespaces-within-refresh-dirs-only 50 | strategies/do-not-use-cached-results!))))] 51 | (filterv some?)))) 52 | 53 | (def default-linters [(-> (linters.kondo/new {}) 54 | (assoc :strategies (conj extended-strategies 55 | strategies/exclude-edn))) 56 | (-> (linters.one-resource-per-ns/new {}) 57 | (assoc :strategies (conj extended-strategies 58 | strategies/files-with-a-namespace))) 59 | (-> (linters.ns-aliases/new {}) 60 | (assoc :strategies (conj extended-strategies 61 | strategies/files-with-a-namespace 62 | ;; reader conditionals may confuse `linters.ns-aliases` 63 | strategies/exclude-cljc 64 | ;; string requires may confuse clojure.tools.* 65 | strategies/exclude-cljs))) 66 | (-> (linters.line-length/new {}) 67 | (assoc :strategies (conj extended-strategies 68 | strategies/exclude-edn))) 69 | (-> (linters.loc-per-ns/new {}) 70 | (assoc :strategies (conj extended-strategies 71 | strategies/exclude-edn))) 72 | (-> (linters.eastwood/new {}) 73 | (assoc :strategies (conj extended-strategies 74 | strategies/exclude-cljs 75 | strategies/jvm-requirable-files 76 | strategies/namespaces-within-refresh-dirs-only)))]) 77 | 78 | (defn default-processors [third-party-indent-specs] 79 | [(-> (processors.cider/new {:third-party-indent-specs third-party-indent-specs}) 80 | (assoc :strategies extended-strategies))]) 81 | -------------------------------------------------------------------------------- /src/formatting_stack/formatters/clean_ns/impl.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.formatters.clean-ns.impl 2 | (:require 3 | [clojure.tools.namespace.parse :as parse] 4 | [clojure.tools.reader :as tools.reader] 5 | [clojure.tools.reader.reader-types :refer [indexing-push-back-reader push-back-reader]] 6 | [clojure.walk :as walk] 7 | [formatting-stack.util :refer [ensure-coll rcomp]] 8 | [nedap.speced.def :as speced]) 9 | (:import 10 | (clojure.lang Namespace))) 11 | 12 | (speced/defn safely-read-ns-contents [^string? buffer, ^Namespace ns-obj] 13 | (binding [tools.reader/*alias-map* (ns-aliases ns-obj)] 14 | (tools.reader/read-string {:read-cond :allow 15 | :features #{:clj}} 16 | (str "[ " buffer " ]")))) 17 | 18 | (defn used-namespace-names 19 | "NOTE: this returns the set of namespace _names_ that are used, not the set of namespaces that are used. 20 | 21 | e.g. a namespace which is exclusively used through `:refer` has a 'unused namespace name', 22 | but it is not unused (because it is referred). 23 | 24 | Use with caution accordingly, and not as a exclusive source of truth. 25 | 26 | `namespaces-that-should-never-cleaned` refers to the namespaces that are requiring libs - not the required libs themselves." 27 | [filename namespaces-that-should-never-cleaned] 28 | {:pre [(string? filename) 29 | (set? namespaces-that-should-never-cleaned)]} 30 | (let [buffer (slurp filename) 31 | ns-obj (-> filename formatting-stack.util/read-ns-decl parse/name-from-ns-decl the-ns) 32 | _ (assert ns-obj) 33 | [ns-form & contents] (safely-read-ns-contents buffer ns-obj) 34 | _ (assert (and (list? ns-form) 35 | (= 'ns (first ns-form))) 36 | (str "Filename " filename ": expected the first form to be of `(ns ...)` type.")) 37 | ns-name (-> ns-form parse/name-from-ns-decl) 38 | requires (-> ns-form parse/deps-from-ns-decl set) 39 | result (atom #{}) 40 | aliases-keys (-> ns-obj ns-aliases keys set) 41 | expand-ident (fn [ident] 42 | (when-let [n (some-> ident namespace symbol)] 43 | (cond 44 | (requires n) 45 | n 46 | 47 | (aliases-keys n) 48 | (-> ns-obj ns-aliases (get n) str symbol))))] 49 | (if (namespaces-that-should-never-cleaned ns-name) 50 | (reset! result #{'.*}) 51 | (walk/postwalk (fn traverse [x] 52 | (some->> x meta (walk/postwalk traverse)) 53 | (when-let [n (and (ident? x) (expand-ident x))] 54 | (when (requires n) 55 | (swap! result conj n))) 56 | x) 57 | contents)) 58 | @result)) 59 | 60 | (defn clean-ns-form [{:keys [how-to-ns-opts 61 | refactor-nrepl-opts 62 | filename 63 | original-ns-form 64 | namespaces-that-should-never-cleaned 65 | libspec-whitelist]}] 66 | {:pre [how-to-ns-opts 67 | refactor-nrepl-opts 68 | filename 69 | original-ns-form 70 | namespaces-that-should-never-cleaned 71 | libspec-whitelist]} 72 | 73 | (require ;; lazy-loading in order to support unconfigured consumers 74 | '[refactor-nrepl.config] 75 | '[refactor-nrepl.ns.clean-ns]) 76 | 77 | (let [whitelist (into libspec-whitelist (map str) (used-namespace-names filename namespaces-that-should-never-cleaned)) 78 | config-var (resolve 'refactor-nrepl.config/*config*) 79 | clean-ns (resolve 'refactor-nrepl.ns.clean-ns/clean-ns)] 80 | (with-bindings* {config-var (-> refactor-nrepl-opts 81 | (update :libspec-whitelist into whitelist))} 82 | (fn [] 83 | (clean-ns {:path filename}))))) 84 | 85 | (defn has-duplicate-requires? [filename] 86 | (->> filename 87 | formatting-stack.util/read-ns-decl 88 | formatting-stack.util/require-from-ns-decl 89 | rest 90 | (map ensure-coll) 91 | (group-by first) 92 | (vals) 93 | (some (rcomp count (complement #{1}))))) 94 | -------------------------------------------------------------------------------- /src/formatting_stack/formatters/cljfmt.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.formatters.cljfmt 2 | (:require 3 | [cljfmt.core :as cljfmt] 4 | [cljfmt.main] 5 | [clojure.string :as string] 6 | [formatting-stack.formatters.cljfmt.impl :as impl] 7 | [formatting-stack.indent-specs :refer [default-third-party-indent-specs]] 8 | [formatting-stack.protocols.formatter :as formatter] 9 | [formatting-stack.protocols.linter :as linter] 10 | [formatting-stack.util :refer [ensure-sequential process-in-parallel!]] 11 | [formatting-stack.util.diff :as diff :refer [diff->line-numbers]] 12 | [nedap.speced.def :as speced] 13 | [nedap.utils.modular.api :refer [implement]])) 14 | 15 | (defn format! [{:keys [third-party-indent-specs]} files] 16 | (->> files 17 | (process-in-parallel! (fn [filename] 18 | (let [indents (impl/cljfmt-indents-for filename third-party-indent-specs)] 19 | (cljfmt.main/fix [filename] {:indents indents}))))) 20 | nil) 21 | 22 | (defn lint! [{:keys [third-party-indent-specs]} files] 23 | (->> files 24 | (process-in-parallel! 25 | (fn [filename] 26 | (let [indents (impl/cljfmt-indents-for filename third-party-indent-specs) 27 | original (slurp filename) 28 | revised ((cljfmt/wrap-normalize-newlines #(cljfmt/reformat-string % {:indents indents})) original)] ;; taken from https://git.io/Jkot7 29 | (when (not= original revised) 30 | (let [diff (diff/unified-diff filename original revised)] 31 | (->> (diff->line-numbers diff) 32 | (mapv (fn [{:keys [start end]}] 33 | {:filename filename 34 | :diff diff 35 | :level :warning 36 | :column 0 37 | :line start 38 | :msg (str "Indentation or whitespace is off on line " (->> (dedupe [start end]) 39 | (string/join "-"))) 40 | :source :cljfmt/indent})))))))) 41 | (filter some?) 42 | (mapcat ensure-sequential))) 43 | 44 | (speced/defn new [{:keys [third-party-indent-specs] 45 | :or {third-party-indent-specs default-third-party-indent-specs}}] 46 | (implement {:id ::id 47 | :third-party-indent-specs third-party-indent-specs} 48 | formatter/--format! format! 49 | linter/--lint! lint!)) 50 | -------------------------------------------------------------------------------- /src/formatting_stack/formatters/how_to_ns.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.formatters.how-to-ns 2 | (:require 3 | [com.gfredericks.how-to-ns :as how-to-ns] 4 | [com.gfredericks.how-to-ns.main :as how-to-ns.main] 5 | [formatting-stack.protocols.formatter :as formatter] 6 | [formatting-stack.protocols.linter :as linter] 7 | [formatting-stack.strategies :as strategies] 8 | [formatting-stack.util :refer [ensure-sequential process-in-parallel!]] 9 | [formatting-stack.util.diff :as diff :refer [diff->line-numbers]] 10 | [medley.core :refer [deep-merge]] 11 | [nedap.utils.modular.api :refer [implement]])) 12 | 13 | (def default-how-to-ns-opts {:require-docstring? false 14 | :sort-clauses? true 15 | ;; should be false, but https://git.io/fhMLm can break code: 16 | :allow-refer-all? true 17 | :allow-extra-clauses? false 18 | :align-clauses? false 19 | :import-square-brackets? false}) 20 | 21 | (defn format! [{:keys [how-to-ns-options]} files] 22 | (->> (strategies/exclude-edn :files files) 23 | (process-in-parallel! (fn [filename] 24 | (how-to-ns.main/fix [filename] how-to-ns-options)))) 25 | nil) 26 | 27 | (defn lint! [{:keys [how-to-ns-options]} files] 28 | (->> (strategies/exclude-edn :files files) 29 | (process-in-parallel! (fn [filename] 30 | (let [contents (slurp filename) 31 | formatted (how-to-ns/format-initial-ns-str contents how-to-ns-options)] 32 | (when-not (= contents formatted) 33 | (let [diff (diff/unified-diff filename contents formatted)] 34 | (->> (diff->line-numbers diff) 35 | (mapv (fn [{:keys [start]}] 36 | {:filename filename 37 | :diff diff 38 | :line start 39 | :column 0 40 | :level :warning 41 | :msg "Detected unsorted, renamed or extra clauses in the ns format" 42 | :warning-details-url "https://stuartsierra.com/2016/clojure-how-to-ns.html" 43 | :source :how-to-ns/ns})))))))) 44 | (filter some?) 45 | (mapcat ensure-sequential))) 46 | 47 | (defn new [{:keys [how-to-ns-options] 48 | :or {how-to-ns-options {}}}] 49 | (implement {:id ::id 50 | :how-to-ns-options (deep-merge default-how-to-ns-opts how-to-ns-options)} 51 | linter/--lint! lint! 52 | formatter/--format! format!)) 53 | -------------------------------------------------------------------------------- /src/formatting_stack/formatters/newlines.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.formatters.newlines 2 | (:require 3 | [clojure.string :as str] 4 | [com.gfredericks.all-my-files-should-end-with-exactly-one-newline-character :as impl] 5 | [formatting-stack.protocols.formatter :as formatter] 6 | [formatting-stack.protocols.linter :as linter] 7 | [formatting-stack.util :refer [process-in-parallel! silence]] 8 | [nedap.speced.def :as speced] 9 | [nedap.utils.modular.api :refer [implement]])) 10 | 11 | (defn format! [{:keys [expected-newline-count]} files] 12 | (->> files 13 | (process-in-parallel! (fn [filename] 14 | (silence ;; Supress "All newlines are good, nothing to fix." 15 | (impl/so-fix-them [filename] :expected-newline-count expected-newline-count))))) 16 | nil) 17 | 18 | (defn lint! [{:keys [expected-newline-count]} files] 19 | (->> files 20 | (process-in-parallel! (fn [filename] 21 | (when-not (zero? 22 | (silence (impl/but-do-they? [filename] :expected-newline-count expected-newline-count))) 23 | {:filename filename 24 | :source :formatting-stack/newlines 25 | :level :warning 26 | :line (-> filename slurp str/split-lines count) 27 | :msg (str "File should end in " expected-newline-count " newlines") 28 | :column 1}))) 29 | (filterv some?))) 30 | 31 | (speced/defn new [{:keys [^{::speced/spec #{0 1}} expected-newline-count] 32 | :or {expected-newline-count 1}}] 33 | (implement {:id ::id 34 | :expected-newline-count expected-newline-count} 35 | linter/--lint! lint! 36 | formatter/--format! format!)) 37 | -------------------------------------------------------------------------------- /src/formatting_stack/formatters/no_extra_blank_lines.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.formatters.no-extra-blank-lines 2 | "Ensures that no three consecutive newlines happen in a given file. 3 | 4 | That can happen naturally, or because of other formatters' intricacies." 5 | (:require 6 | [clojure.spec.alpha :as spec] 7 | [clojure.string :as string] 8 | [formatting-stack.protocols.formatter :as formatter] 9 | [formatting-stack.protocols.linter :as linter] 10 | [formatting-stack.util :refer [ensure-sequential process-in-parallel! rcomp]] 11 | [nedap.speced.def :as speced] 12 | [nedap.utils.modular.api :refer [implement]])) 13 | 14 | (defn without-extra-newlines [s] 15 | (-> s (string/replace #"(\n\n)(\n)+" "$1"))) 16 | 17 | (defn format! [this files] 18 | (->> files 19 | (process-in-parallel! (fn [filename] 20 | (let [contents (-> filename slurp) 21 | formatted (without-extra-newlines contents)] 22 | (when-not (= contents formatted) 23 | (println "Removing extra blank lines:" filename) 24 | (spit filename formatted)))))) 25 | nil) 26 | 27 | (speced/defn ^{::speced/spec (spec/coll-of pos-int?)} report-extra-newlines [^string? s] 28 | (->> (string/split-lines s) 29 | (map-indexed (fn [idx line] {:lineNumber idx :line line})) 30 | (partition 2 1) 31 | (filter (fn [[{left :line} {right :line}]] (= "" left right))) 32 | (map (rcomp first :lineNumber inc)) 33 | (reduce (fn [ret line-number] 34 | (if (= (last ret) 35 | (dec line-number)) 36 | ret ;; group consecutive warnings 37 | (conj ret line-number))) 38 | []))) 39 | 40 | (defn lint! [this files] 41 | (->> files 42 | (process-in-parallel! (fn [filename] 43 | (->> (slurp filename) 44 | (report-extra-newlines) 45 | (mapv (fn [line-number] 46 | {:column 1 47 | :line line-number 48 | :filename filename 49 | :msg "File has two or more consecutive blank lines" 50 | :level :warning 51 | :source :formatting-stack/no-extra-blank-lines}))))) 52 | (mapcat ensure-sequential))) 53 | 54 | (defn new [] 55 | (implement {:id ::id} 56 | linter/--lint! lint! 57 | formatter/--format! format!)) 58 | -------------------------------------------------------------------------------- /src/formatting_stack/indent_specs.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.indent-specs) 2 | 3 | (def default-third-party-indent-specs 4 | '{cats.core/mlet {:style/indent 1} 5 | cats.monad.either/branch {:style/indent 1} 6 | cljs.test/async {:style/indent 1} 7 | clojure.core/delay {:style/indent 0} 8 | clojure.core/time {:style/indent 0} 9 | clojure.test.check.properties/for-all {:style/indent 1} 10 | datomic.client.api/q {:style/indent 1} 11 | datomic.api/q {:style/indent 1} 12 | hiccup.form/form-to {:style/indent 1} 13 | hiccup.page/html5 {:style/indent 1} 14 | fulcrologic.semantic-ui.factories/ui-button {:style/indent 1} 15 | fulcrologic.semantic-ui.factories/ui-card-content {:style/indent 1} 16 | fulcrologic.semantic-ui.factories/ui-dropdown {:style/indent 1} 17 | fulcrologic.semantic-ui.factories/ui-dropdown-menu {:style/indent 1} 18 | fulcrologic.semantic-ui.factories/ui-form {:style/indent 1} 19 | fulcrologic.semantic-ui.factories/ui-form-button {:style/indent 1} 20 | fulcrologic.semantic-ui.factories/ui-form-group {:style/indent 1} 21 | fulcrologic.semantic-ui.factories/ui-grid {:style/indent 1} 22 | fulcrologic.semantic-ui.factories/ui-grid-column {:style/indent 1} 23 | fulcrologic.semantic-ui.factories/ui-header {:style/indent 1} 24 | fulcrologic.semantic-ui.factories/ui-item-content {:style/indent 1} 25 | fulcrologic.semantic-ui.factories/ui-item-group {:style/indent 1} 26 | fulcrologic.semantic-ui.factories/ui-label {:style/indent 1} 27 | fulcrologic.semantic-ui.factories/ui-label-group {:style/indent 1} 28 | fulcrologic.semantic-ui.factories/ui-list {:style/indent 1} 29 | fulcrologic.semantic-ui.factories/ui-list-content {:style/indent 1} 30 | fulcrologic.semantic-ui.factories/ui-list-item {:style/indent 1} 31 | fulcrologic.semantic-ui.factories/ui-menu-item {:style/indent 1} 32 | fulcrologic.semantic-ui.factories/ui-modal {:style/indent 1} 33 | fulcrologic.semantic-ui.factories/ui-modal-actions {:style/indent 1} 34 | fulcrologic.semantic-ui.factories/ui-modal-content {:style/indent 1} 35 | fulcrologic.semantic-ui.factories/ui-modal-header {:style/indent 1} 36 | fulcrologic.semantic-ui.factories/ui-segment {:style/indent 1} 37 | fulcrologic.semantic-ui.factories/ui-segment-group {:style/indent 1} 38 | fulcrologic.semantic-ui.factories/ui-sidebar {:style/indent 1} 39 | fulcrologic.semantic-ui.factories/ui-sidebar-pushable {:style/indent 1} 40 | fulcrologic.semantic-ui.factories/ui-sidebar-pusher {:style/indent 1} 41 | fulcrologic.semantic-ui.factories/ui-table {:style/indent 1} 42 | fulcrologic.semantic-ui.factories/ui-table-body {:style/indent 1} 43 | fulcrologic.semantic-ui.factories/ui-table-cell {:style/indent 1} 44 | fulcrologic.semantic-ui.factories/ui-table-row {:style/indent 1} 45 | fulcro.client.dom/div {:style/indent 1} 46 | fulcro.client.dom/span {:style/indent 1} 47 | fulcro-spec.core/assertions {:style/indent 0 48 | :style.cljfmt/type :inner} 49 | fulcro-spec.core/behavior {:style/indent 1} 50 | fulcro-spec.core/component {:style/indent 1} 51 | fulcro-spec.core/specification {:style/indent 1} 52 | fulcro-spec.core/when-mocking! {:style/indent 0 53 | :style.cljfmt/type :inner} 54 | fulcro-spec.core/when-mocking {:style/indent 0 55 | :style.cljfmt/type :inner} 56 | garden.stylesheet/at-media {:style/indent 1} 57 | let* {:style/indent 1}}) 58 | 59 | (def magic-symbol-mappings 60 | "If a given ns requires e.g. `fulcro.server`, then `action` will have the specified indent rule. 61 | 62 | Apt for macros such as `fulcro.server/defmutation` which inject `action` into the local environment." 63 | '{fulcro.server {action {:style/indent 0 64 | :style.cljfmt/type :inner}} 65 | fulcro.client.mutations {action {:style/indent 0 66 | :style.cljfmt/type :inner}}}) 67 | -------------------------------------------------------------------------------- /src/formatting_stack/integrant.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.integrant 2 | (:require 3 | [formatting-stack.component.impl :refer [parse-options]] 4 | [formatting-stack.core :refer [format!]] 5 | [integrant.core :as integrant] 6 | [nedap.speced.def :as speced])) 7 | 8 | (speced/defn start [^map? this] 9 | (->> this 10 | parse-options 11 | (apply format!)) 12 | this) 13 | 14 | (defmethod integrant/init-key ::component [_ this] 15 | (start this)) 16 | 17 | (defmethod integrant/halt-key! ::component [_ this] 18 | this) 19 | -------------------------------------------------------------------------------- /src/formatting_stack/linters/eastwood.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.linters.eastwood 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.string :as str] 5 | [eastwood.lint] 6 | [formatting-stack.linters.eastwood.impl :as impl] 7 | [formatting-stack.protocols.linter :as linter] 8 | [formatting-stack.util :refer [ns-name-from-filename silence]] 9 | [medley.core :refer [assoc-some deep-merge]] 10 | [nedap.utils.modular.api :refer [implement]]) 11 | (:import 12 | (java.io File))) 13 | 14 | (def default-eastwood-options 15 | (-> eastwood.lint/default-opts 16 | (assoc :rethrow-exceptions? true))) 17 | 18 | (defn lint! [{:keys [options]} filenames] 19 | (let [namespaces (->> filenames 20 | (remove #(str/ends-with? % ".edn")) 21 | (keep ns-name-from-filename)) 22 | reports (atom nil) 23 | exceptions (atom nil)] 24 | 25 | (silence 26 | (try 27 | (-> options 28 | (assoc :namespaces namespaces) 29 | (eastwood.lint/eastwood (impl/->TrackingReporter reports))) 30 | (catch Exception e 31 | (swap! exceptions conj e)))) 32 | (->> @reports 33 | :warnings 34 | (map :warn-data) 35 | (map (fn [{:keys [uri-or-file-name linter] :strs [warning-details-url] :as m}] 36 | (assoc-some m 37 | :level :warning 38 | :source (keyword "eastwood" (name linter)) 39 | :warning-details-url warning-details-url 40 | :filename (if (string? uri-or-file-name) 41 | uri-or-file-name 42 | (-> ^File uri-or-file-name .getCanonicalPath))))) 43 | (into (impl/exceptions->reports @exceptions))))) 44 | 45 | (defn new [{:keys [eastwood-options] 46 | :or {eastwood-options {}}}] 47 | (implement {:id ::id 48 | :options (deep-merge default-eastwood-options eastwood-options)} 49 | linter/--lint! lint!)) 50 | -------------------------------------------------------------------------------- /src/formatting_stack/linters/eastwood/impl.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.linters.eastwood.impl 2 | (:require 3 | [clojure.string :as string] 4 | [eastwood.reporting-callbacks :as reporting-callbacks] 5 | [formatting-stack.protocols.spec :as protocols.spec] 6 | [nedap.speced.def :as speced])) 7 | 8 | (speced/defn ^::protocols.spec/reports exceptions->reports 9 | [exceptions] 10 | (->> exceptions 11 | (map (fn [exception] 12 | {:level :exception 13 | :source :formatting-stack/report-processing-error 14 | :msg (str "Encountered an exception while running Eastwood") 15 | :exception exception})))) 16 | 17 | (def vconj (fnil conj [])) 18 | 19 | (defrecord TrackingReporter [reports]) 20 | 21 | (defmethod reporting-callbacks/lint-warning TrackingReporter [{:keys [reports]} warning] 22 | (swap! reports update :warnings vconj warning) 23 | nil) 24 | 25 | (defmethod reporting-callbacks/analyzer-exception TrackingReporter [{:keys [reports]} exception] 26 | (swap! reports update :errors vconj exception) 27 | nil) 28 | 29 | (defmethod reporting-callbacks/note TrackingReporter [{:keys [reports]} msg] 30 | (swap! reports update :note vconj msg) 31 | nil) 32 | -------------------------------------------------------------------------------- /src/formatting_stack/linters/kondo.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.linters.kondo 2 | (:require 3 | [clj-kondo.core :as kondo] 4 | [clojure.set :as set] 5 | [formatting-stack.kondo-classpath-cache] 6 | [formatting-stack.protocols.linter :as protocols.linter] 7 | [medley.core :refer [deep-merge]] 8 | [nedap.utils.modular.api :refer [implement]])) 9 | 10 | (def off {:level :off}) 11 | 12 | (def warn {:level :warning}) 13 | 14 | (def default-options 15 | {:cache true 16 | :cache-dir formatting-stack.kondo-classpath-cache/cache-dir 17 | ;; https://github.com/clj-kondo/clj-kondo/blob/v2021.12.19/doc/linters.md 18 | :linters {:docstring-leading-trailing-whitespace warn ;; disabled-by-default 19 | :reduce-without-init warn ;; disabled-by-default 20 | :single-key-in warn ;; disabled-by-default 21 | :used-underscored-binding warn ;; disabled-by-default 22 | :cond-else off ;; undesired 23 | :missing-docstring off ;; undesired 24 | :unused-binding off ;; undesired 25 | :private-call off ;; undesired 26 | :unresolved-symbol off ;; can give false positives 27 | :unused-symbol off ;; can give false positives 28 | :unused-private-var off ;; can give false positives 29 | :unresolved-var off ;; already offered by clj 30 | :consistent-alias off ;; already offered by how-to-ns 31 | :duplicate-require off ;; already offered by clean-ns 32 | :unused-import off ;; already offered by clean-ns 33 | :unused-namespace off ;; already offered by clean-ns 34 | :unused-referred-var off ;; already offered by clean-ns 35 | :unresolved-namespace off ;; already offered by clean-ns 36 | } 37 | :lint-as '{nedap.speced.def/def-with-doc clojure.core/defonce 38 | nedap.speced.def/defn clojure.core/defn 39 | nedap.speced.def/defprotocol clojure.core/defprotocol 40 | nedap.speced.def/doc clojure.repl/doc 41 | nedap.speced.def/fn clojure.core/fn 42 | nedap.speced.def/let clojure.core/let 43 | nedap.speced.def/letfn clojure.core/letfn 44 | nedap.utils.reverse/r-> clojure.core/-> 45 | nedap.utils.reverse/rcomp clojure.core/comp 46 | nedap.utils.reverse/rcond-> clojure.core/cond->} 47 | :output {:exclude-files ["test-resources/*" 48 | "test/unit/formatting_stack/formatters/cljfmt/impl/sample_data.clj"]}}) 49 | 50 | (def clj-options 51 | ;; .clj files are also linted by Eastwood, so we disable duplicate linters: 52 | {:linters {:misplaced-docstring off 53 | :invalid-arity off 54 | :deprecated-var off 55 | :inline-def off 56 | :redefined-var off}}) 57 | 58 | (defn lint! [{:keys [kondo-clj-options kondo-cljs-options]} 59 | filenames] 60 | 61 | @formatting-stack.kondo-classpath-cache/classpath-cache 62 | 63 | (let [kondo-clj-options (or kondo-clj-options {}) 64 | kondo-cljs-options (or kondo-cljs-options {}) 65 | {cljs-files true 66 | clj-files false} (->> filenames 67 | (group-by (fn [f] 68 | (-> (re-find #"\.cljs$" f) 69 | boolean))))] 70 | (->> [(kondo/run! {:lint clj-files 71 | :config (deep-merge default-options clj-options kondo-clj-options) 72 | ;; :lang is unset here, so as not to particularly prefer .clj over .cljc 73 | }) 74 | (kondo/run! {:lint cljs-files 75 | :config (deep-merge default-options kondo-cljs-options) 76 | :lang :cljs})] 77 | (mapcat :findings) 78 | (map (fn [{source-type :type :as m}] 79 | (-> (set/rename-keys m {:row :line 80 | :message :msg 81 | :col :column}) 82 | (assoc :source (keyword "kondo" (name source-type))))))))) 83 | 84 | (defn new [{:keys [kondo-clj-options 85 | kondo-cljs-options]}] 86 | (implement {:id ::id 87 | :kondo-clj-options kondo-clj-options 88 | :kondo-cljs-options kondo-cljs-options} 89 | protocols.linter/--lint! lint!)) 90 | -------------------------------------------------------------------------------- /src/formatting_stack/linters/line_length.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.linters.line-length 2 | (:require 3 | [clojure.string :as string] 4 | [formatting-stack.protocols.linter :as linter] 5 | [formatting-stack.util :refer [ensure-sequential partition-between process-in-parallel!]] 6 | [nedap.utils.modular.api :refer [implement]] 7 | [medley.core :refer [assoc-some]])) 8 | 9 | (defn exceeding-lines [{:keys [max-line-length merge-threshold]} filename] 10 | (->> (-> filename slurp string/split-lines) 11 | (map count) 12 | (keep-indexed (fn [i column] 13 | (when (< max-line-length column) 14 | {:column column 15 | :line (inc i)}))) 16 | (partition-between (fn [{first-line :line} {second-line :line}] 17 | (< merge-threshold 18 | (- second-line first-line)))) 19 | (mapv (fn [reports] 20 | (let [{first-line :line 21 | first-column :column} (first reports) 22 | {last-line :line} (last reports)] 23 | {:filename filename 24 | :source :formatting-stack/line-length 25 | :level :warning 26 | :line first-line 27 | :column first-column 28 | :msg (str "Line exceeding " max-line-length " columns" 29 | (when (not= last-line first-line) 30 | (str " (spanning " (inc (- last-line first-line)) " lines)")) 31 | ".")}))))) 32 | 33 | (defn lint! [options filenames] 34 | (->> filenames 35 | (process-in-parallel! (partial exceeding-lines options)) 36 | (mapcat ensure-sequential))) 37 | 38 | (defn new [{:keys [max-line-length merge-threshold] 39 | :or {max-line-length 130 40 | merge-threshold 10}}] 41 | (implement {:id ::id 42 | :merge-threshold merge-threshold 43 | :max-line-length max-line-length} 44 | linter/--lint! lint!)) 45 | -------------------------------------------------------------------------------- /src/formatting_stack/linters/loc_per_ns.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.linters.loc-per-ns 2 | (:require 3 | [clojure.string :as string] 4 | [formatting-stack.protocols.linter :as linter] 5 | [formatting-stack.util :refer [process-in-parallel!]] 6 | [nedap.utils.modular.api :refer [implement]])) 7 | 8 | (defn count-lines [filename] 9 | (-> filename 10 | slurp 11 | (string/split-lines) 12 | (count))) 13 | 14 | (defn lint! [{:keys [max-lines-per-ns]} filenames] 15 | (->> filenames 16 | (process-in-parallel! (fn [filename] 17 | (let [lines (count-lines filename)] 18 | (when (> lines max-lines-per-ns) 19 | {:filename filename 20 | :source :formatting-stack/loc-per-ns 21 | :level :warning 22 | :msg (str "Longer than " max-lines-per-ns " LOC.") 23 | :line lines 24 | :column 0})))) 25 | (filterv some?))) 26 | 27 | (defn new [{:keys [max-lines-per-ns] 28 | :or {max-lines-per-ns 350}}] 29 | (implement {:id ::id 30 | :max-lines-per-ns max-lines-per-ns} 31 | linter/--lint! lint!)) 32 | -------------------------------------------------------------------------------- /src/formatting_stack/linters/ns_aliases.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.linters.ns-aliases 2 | "Observes these guidelines: https://stuartsierra.com/2015/05/10/clojure-namespace-aliases" 3 | (:require 4 | [clojure.string :as string] 5 | [formatting-stack.linters.ns-aliases.impl :as impl] 6 | [formatting-stack.protocols.linter :as linter] 7 | [formatting-stack.strategies :as strategies] 8 | [formatting-stack.util :refer [ensure-sequential process-in-parallel!]] 9 | [nedap.utils.modular.api :refer [implement]])) 10 | 11 | (defn clause= [a b] 12 | (->> [a b] 13 | (map (fn [s] 14 | (-> s 15 | (string/replace "-clj" "") 16 | (string/replace "clj-" "") 17 | (string/replace "-cljs" "") 18 | (string/replace "cljs-" "") 19 | (string/replace "-clojure" "") 20 | (string/replace "clojure-" "")))) 21 | (apply =))) 22 | 23 | (defn name-and-alias [[ns-name :as require-clause]] 24 | [ns-name 25 | (->> require-clause 26 | (reduce (fn [{:keys [found-as? alias] :as result} member] 27 | (cond 28 | alias result 29 | found-as? {:found-as? true, :alias member} 30 | (= :as member) {:found-as? true, :alias nil})) 31 | {:found-as? false 32 | :alias nil}) 33 | :alias)]) 34 | 35 | (defn derived? [alias _from ns-name] 36 | {:pre [(#{:from} _from)]} 37 | (let [[alias-fragments ns-fragments] (->> [alias ns-name] 38 | (map (fn [x] 39 | (-> x 40 | name 41 | (string/split #"\.") 42 | (reverse))))) 43 | ns-fragments-without-core (->> ns-fragments (remove #{"core" "alpha" "api" "kws"}))] 44 | (->> [ns-fragments ns-fragments-without-core] 45 | (distinct) 46 | (some #(->> (map clause= alias-fragments %) 47 | (every? true?))) 48 | (boolean)))) 49 | 50 | (def default-acceptable-aliases-whitelist 51 | '{d [datomic.api datomic.client.api] 52 | impl [::anything] 53 | log [::anything] 54 | s [clojure.spec.alpha cljs.spec.alpha] 55 | spec [clojure.spec.alpha cljs.spec.alpha] 56 | speced [nedap.speced.def] 57 | str [clojure.string] 58 | sut [::anything] 59 | sut.impl [::anything] 60 | t [clojure.test cljs.test]}) 61 | 62 | (defn acceptable-require-clause? [whitelist require-clause] 63 | (if-not (vector? require-clause) 64 | true 65 | (let [[ns-name alias] (name-and-alias require-clause) 66 | whitelisted-namespaces (get whitelist alias)] 67 | (if-not alias 68 | true 69 | (-> (or (some #{::anything ns-name} whitelisted-namespaces) 70 | (derived? alias :from ns-name)) 71 | (boolean)))))) 72 | 73 | (defn lint! [{:keys [acceptable-aliases-whitelist]} filenames] 74 | (->> filenames 75 | (process-in-parallel! (fn [filename] 76 | (->> filename 77 | formatting-stack.util/read-ns-decl 78 | formatting-stack.util/require-from-ns-decl 79 | (rest) 80 | (remove (partial acceptable-require-clause? 81 | acceptable-aliases-whitelist)) 82 | (filter some?) 83 | (mapv (fn [bad-alias] 84 | {:filename filename 85 | :line (-> bad-alias meta :line) 86 | :column (-> bad-alias meta :column) 87 | :level :warning 88 | :warning-details-url "https://stuartsierra.com/2015/05/10/clojure-namespace-aliases" 89 | :msg (str bad-alias " is not a derived alias.") 90 | :source :formatting-stack/ns-aliases}))))) 91 | (mapcat ensure-sequential))) 92 | 93 | (defn new 94 | "If `:augment-acceptable-aliases-whitelist?` is true, 95 | all aliases already used in your current project (as Git status and branch info indicates) will be deemed acceptable." 96 | [{:keys [acceptable-aliases-whitelist 97 | augment-acceptable-aliases-whitelist?] 98 | :or {acceptable-aliases-whitelist default-acceptable-aliases-whitelist 99 | augment-acceptable-aliases-whitelist? true}}] 100 | (implement {:id ::id 101 | :acceptable-aliases-whitelist 102 | (cond-> acceptable-aliases-whitelist 103 | (and augment-acceptable-aliases-whitelist? 104 | impl/namespace-aliases-for*) 105 | (impl/merge-aliases (impl/project-aliases {:cache-key (strategies/current-branch-name)})))} 106 | linter/--lint! lint!)) 107 | -------------------------------------------------------------------------------- /src/formatting_stack/linters/ns_aliases/impl.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.linters.ns-aliases.impl 2 | (:require 3 | [clojure.set :as set] 4 | [clojure.spec.alpha :as spec] 5 | [formatting-stack.strategies :as strategies] 6 | [nedap.speced.def :as speced]) 7 | (:import 8 | (java.io File))) 9 | 10 | (spec/def ::alias (spec/or :simple-alias symbol? 11 | :directive keyword?)) 12 | 13 | (spec/def ::aliases (spec/coll-of ::alias :kind vector?)) 14 | 15 | (spec/def ::project-aliases (spec/map-of symbol? ::aliases)) 16 | 17 | (speced/defn ^::project-aliases merge-aliases [^::project-aliases m1, ^::project-aliases m2] 18 | (merge-with (fn [x y] 19 | (vec (into #{} cat [x y]))) 20 | m1 21 | m2)) 22 | 23 | (def namespace-aliases-for* 24 | (when (strategies/refactor-nrepl-3-4-1-available?) 25 | @(requiring-resolve 'refactor-nrepl.ns.libspecs/namespace-aliases-for))) 26 | 27 | (defn namespace-aliases-for [files] 28 | (when namespace-aliases-for* 29 | (let [{:keys [clj cljs]} (namespace-aliases-for* files true)] 30 | (merge-aliases clj cljs)))) 31 | 32 | ;; NOTE: this isn't necessarily a "strategy" (which would reside in the `strategies` ns), 33 | ;; since it's composed of other strategy calls. 34 | ;; This is more of a handy helper at the moment. 35 | (defn stable-files 36 | "Files that already existed as of the default branch, 37 | and that haven't been touched in the current branch" 38 | [] 39 | (let [with (set (strategies/all-files :files [])) 40 | without (into #{} cat [(strategies/git-diff-against-default-branch :files []) 41 | (strategies/git-completely-staged :files []) 42 | (strategies/git-not-completely-staged :files [])]) 43 | corpus (set/difference with without)] 44 | (->> corpus 45 | (mapv (speced/fn [^String s] 46 | (File. s)))))) 47 | 48 | (def project-aliases 49 | (memoize 50 | (fn [{_cache-key :cache-key}] ;; there's a cache key for correct memoization 51 | 52 | ;; note that memoizing results is correct - 53 | ;; results don't have to be recomputed as the git status changes: 54 | ;; touching more files doesn't alter the fact that these aliases already were existing. 55 | (namespace-aliases-for (stable-files))))) 56 | -------------------------------------------------------------------------------- /src/formatting_stack/linters/one_resource_per_ns.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.linters.one-resource-per-ns 2 | "This linter ensures that there's exactly once classpath resource per namespace and extension. 3 | 4 | Note that generally it's fine to define identically-named Clojure/Script namespaces with _different_ extensions, 5 | so that is allowed." 6 | (:require 7 | [clojure.spec.alpha :as spec] 8 | [clojure.string :as string] 9 | [clojure.tools.namespace.file :as file] 10 | [formatting-stack.protocols.linter :as linter] 11 | [formatting-stack.util :refer [process-in-parallel!]] 12 | [formatting-stack.util.ns :as util.ns] 13 | [nedap.speced.def :as speced] 14 | [nedap.utils.modular.api :refer [implement]] 15 | [nedap.utils.spec.predicates :refer [present-string?]])) 16 | 17 | (spec/def ::resource-path (spec/and present-string? 18 | (complement #{\. \! \? \-}) 19 | (fn [x] 20 | (re-find #"\.clj([cs])?$" x)))) 21 | 22 | (speced/defn ^::resource-path ns-decl->resource-path [^::util.ns/ns-form ns-decl, extension] 23 | (-> ns-decl 24 | second 25 | str 26 | munge 27 | (string/replace "." "/") 28 | (str extension))) 29 | 30 | (speced/defn resource-path->filenames [^::resource-path resource-path] 31 | (->> (-> (Thread/currentThread) 32 | (.getContextClassLoader) 33 | (.getResources resource-path)) 34 | (enumeration-seq) 35 | (distinct) ;; just in case 36 | (mapv str))) 37 | 38 | (speced/defn analyze [^present-string? filename] 39 | (for [extension (->> [".clj" ".cljs" ".cljc"] 40 | (filter (fn [x] 41 | (string/ends-with? filename x)))) 42 | :let [decl (-> filename file/read-file-ns-decl) 43 | resource-path (ns-decl->resource-path decl extension) 44 | filenames (resource-path->filenames resource-path)] 45 | :when (-> filenames count (> 1))] 46 | {:extension extension 47 | :ns-name (-> decl second) 48 | :filenames filenames})) 49 | 50 | (defn lint! [this filenames] 51 | (->> filenames 52 | (process-in-parallel! (fn [filename] 53 | (->> filename 54 | analyze 55 | (mapv (speced/fn [{:keys [^symbol? ns-name, ^coll? filenames]}] 56 | {:filename filename 57 | :level :warning 58 | :line 0 59 | :column 0 60 | :msg (str "The namespace " 61 | "`" ns-name "`" 62 | " is defined over more than one file. Found:") 63 | :msg-extra-data (->> filenames 64 | (mapv (fn [s] 65 | (string/replace s #"^file:" "")))) 66 | :source :formatting-stack/one-resource-per-ns}))))) 67 | (apply concat))) 68 | 69 | (speced/defn new [^map? opts] 70 | (implement (assoc opts :id ::id) 71 | linter/--lint! lint!)) 72 | -------------------------------------------------------------------------------- /src/formatting_stack/processors/cider.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.processors.cider 2 | (:require 3 | [formatting-stack.protocols.processor :as processor] 4 | [nedap.speced.def :as speced] 5 | [nedap.utils.modular.api :refer [implement]] 6 | [nedap.utils.spec.api :refer [check!]])) 7 | 8 | (speced/defn process! [{:keys [^map? third-party-indent-specs]} _files] 9 | (doseq [[var-sym metadata] third-party-indent-specs] 10 | (check! symbol? var-sym 11 | map? metadata) 12 | (some-> var-sym resolve (alter-meta! merge metadata)))) 13 | 14 | (defn new 15 | "This processor alters var metadata from third-party libs, 16 | so that runtime-based tooling such as CIDER can work more accurately." 17 | [{:keys [third-party-indent-specs] :as options}] 18 | (implement (assoc options :id ::id) 19 | processor/--process! process!)) 20 | -------------------------------------------------------------------------------- /src/formatting_stack/processors/test_runner.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.processors.test-runner 2 | "A test runner meant to be integrated with VCSs. JVM-only, and only `clojure.test` is targeted. 3 | 4 | This test runner gathers Clojure ns's out of filenames, derives _even more_ testing ns's out of them 5 | (via naming variations, project-wide `:require` analysis, and metadata analysis), 6 | and invokes `#'clojure.test/run-tests` out of that result." 7 | (:require 8 | [clojure.test] 9 | [formatting-stack.processors.test-runner.impl :refer [ns->sym testable-namespaces]] 10 | [formatting-stack.protocols.processor :as processor] 11 | [formatting-stack.strategies :as strategies :refer [git-completely-staged git-diff-against-default-branch git-not-completely-staged]] 12 | [nedap.utils.modular.api :refer [implement]])) 13 | 14 | ;; Not provided into any default stack, as it would be overly assuming about users' practices 15 | (defn process! [_ filenames] 16 | (assert clojure.test/*load-tests*) 17 | (when-let [test-namespaces (->> filenames 18 | (testable-namespaces) 19 | (map ns->sym) 20 | (seq))] 21 | (apply clojure.test/run-tests test-namespaces)) 22 | nil) 23 | 24 | (defn test! 25 | "Convenience function provided in case it is desired to leverage this ns's functionality, 26 | without adding its component into your 'stack'. 27 | 28 | It gathers files from: 29 | * the `git diff` between the current Git branch and the `:target-branch` argument; plus 30 | * any files returned by `git status`. 31 | 32 | Out of those files, namespaces are derived (1:N, using smart heuristics), 33 | and those namespaces are run via `#'clojure.test/run-tests`." 34 | [& {:keys [target-branch] 35 | :or {target-branch (strategies/default-branch-name)}}] 36 | (let [filenames (->> (git-diff-against-default-branch :target-branch target-branch) 37 | (concat (git-completely-staged :files [])) 38 | (concat (git-not-completely-staged :files [])) 39 | (distinct))] 40 | (process! {} filenames))) 41 | 42 | (defn new [] 43 | (implement {:id ::id} 44 | processor/--process! process!)) 45 | -------------------------------------------------------------------------------- /src/formatting_stack/processors/test_runner/impl.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.processors.test-runner.impl 2 | (:require 3 | [clojure.spec.alpha :as spec] 4 | [clojure.string :as string] 5 | [formatting-stack.project-parsing :refer [project-namespaces]] 6 | [formatting-stack.strategies.impl :refer [filename->ns]] 7 | [nedap.speced.def :as speced] 8 | [nedap.utils.collections.eager :refer [partitioning-pmap]]) 9 | (:import 10 | (clojure.lang Namespace))) 11 | 12 | (spec/def ::namespaces (spec/coll-of (partial instance? Namespace))) 13 | 14 | (speced/defn testable-namespace? [^Namespace n] 15 | (->> n 16 | ns-publics 17 | vals 18 | (some (fn [var-ref] 19 | (-> var-ref meta :test ifn?))) 20 | boolean)) 21 | 22 | (speced/defn ^string? add-t 23 | "Some projects have the naming convention of prefixing the last segment with `t-` 24 | to denote a testing namespace." 25 | [^Namespace n] 26 | (let [segments (-> n str (string/split #"\.")) 27 | first-segments (butlast segments) 28 | last-segment (->> segments last (str "t-"))] 29 | (->> [last-segment] 30 | (concat first-segments) 31 | (string/join ".")))) 32 | 33 | (speced/defn insert-at [^int? i, ^string? s, ^vector? v] 34 | (reduce into [(subvec v 0 i) 35 | [s] 36 | (subvec v i)])) 37 | 38 | (speced/defn permutations [^Namespace n, categorizations] 39 | (let [s (str n) 40 | ns-fragments (string/split s #"\.") 41 | index-count (-> ns-fragments count inc) 42 | indices (->> (range) (take index-count))] 43 | (->> (for [i indices] 44 | (->> categorizations 45 | (map (fn [c] 46 | (->> ns-fragments 47 | (insert-at i c) 48 | (string/join ".")))))) 49 | (apply concat)))) 50 | 51 | (speced/defn ^::namespaces possible-derived-testing-namespaces [^Namespace n] 52 | (let [s (str n) 53 | categorizations #{"test" "unit" "integration" "acceptance" "functional"} 54 | derivations [(str s "-test") 55 | (str s "-spec") 56 | (add-t n)]] 57 | (->> derivations 58 | (into (permutations n categorizations)) 59 | (map symbol) 60 | (keep find-ns)))) 61 | 62 | (speced/defn ^::namespaces sut-consumers 63 | "Returns the set of namespaces which require `n` under the `sut` alias (or similar)." 64 | [^::namespaces corpus, ^Namespace n] 65 | (->> corpus 66 | (partitioning-pmap (speced/fn [^Namespace project-namespace] 67 | (when (->> project-namespace 68 | ns-aliases 69 | (filter (speced/fn [[^symbol? k, ^Namespace v]] 70 | (and (#{'sut 'subject 'system-under-test} k) 71 | (= n v)))) 72 | (seq)) 73 | project-namespace))) 74 | (keep identity) 75 | (vec))) 76 | 77 | (speced/defn ^::namespaces testable-namespaces [filenames] 78 | (->> filenames 79 | (keep filename->ns) 80 | (keep (speced/fn [^Namespace n] 81 | (let [derived-from-name (->> n 82 | (possible-derived-testing-namespaces) 83 | (filterv testable-namespace?)) 84 | consumers (-> (project-namespaces) (sut-consumers n))] 85 | (cond-> [] 86 | (testable-namespace? n) (conj n) 87 | (seq consumers) (into consumers) 88 | (seq derived-from-name) (into derived-from-name))))) 89 | (apply concat) 90 | (distinct))) 91 | 92 | (speced/defn ns->sym [^Namespace n] 93 | (-> n str symbol)) 94 | -------------------------------------------------------------------------------- /src/formatting_stack/project_parsing.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.project-parsing 2 | (:require 3 | [clojure.java.classpath :as classpath] 4 | [clojure.java.io :as io] 5 | [clojure.spec.alpha :as spec] 6 | [clojure.tools.namespace.find :as find] 7 | [clojure.tools.namespace.parse :as parse] 8 | [formatting-stack.util :refer [read-ns-decl]] 9 | [nedap.speced.def :as speced] 10 | [nedap.utils.collections.eager :refer [partitioning-pmap]]) 11 | (:import 12 | (java.io File))) 13 | 14 | (defn find-files [dirs platform] 15 | (->> dirs 16 | (map io/file) 17 | (map (speced/fn [^File f] 18 | (-> f .getCanonicalFile))) 19 | (filter (speced/fn [^File f] 20 | (-> f .exists))) 21 | (mapcat (speced/fn [^File f] 22 | (-> f (find/find-sources-in-dir platform)))) 23 | (map (speced/fn [^File f] 24 | (-> f .getCanonicalFile))))) 25 | 26 | (speced/defn ^{::speced/spec (spec/coll-of any? :min-count 1)} classpath-directories 27 | "A replacement for `#'classpath/classpath-directories` with which external tooling cannot interfere. 28 | 29 | See: https://github.com/clojure-emacs/cider-nrepl/pull/668" 30 | [] 31 | (->> (classpath/system-classpath) 32 | (filter (speced/fn [^File f] 33 | (-> f .isDirectory))))) 34 | 35 | (defn project-namespaces 36 | "Returns all the namespaces contained or required in the current project. 37 | 38 | Includes third-party dependencies." 39 | [] 40 | (->> (find-files (classpath-directories) find/clj) 41 | 42 | (partitioning-pmap (speced/fn [^File file] 43 | (let [decl (-> file str read-ns-decl) 44 | n (some-> decl parse/name-from-ns-decl) 45 | deps (some-> decl parse/deps-from-ns-decl)] 46 | (some-> deps (conj n))))) 47 | (apply concat) 48 | (distinct) 49 | (filter identity) 50 | (keep find-ns))) 51 | -------------------------------------------------------------------------------- /src/formatting_stack/protocols/formatter.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.protocols.formatter 2 | (:require 3 | [formatting-stack.protocols.spec :as protocols.spec] 4 | [nedap.speced.def :as speced])) 5 | 6 | (speced/defprotocol Formatter 7 | "A specific member of the \"stack\", that format files. 8 | 9 | Normally it's a wrapper around a formatting library, with extra configuration, performance improvements, etc." 10 | 11 | (^nil? format! [^::protocols.spec/member this 12 | ^::protocols.spec/filenames filenames] 13 | "Formats `filenames` according to a formatter of your choice.")) 14 | -------------------------------------------------------------------------------- /src/formatting_stack/protocols/linter.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.protocols.linter 2 | (:require 3 | [formatting-stack.protocols.spec :as protocols.spec] 4 | [nedap.speced.def :as speced])) 5 | 6 | (speced/defprotocol Linter 7 | "A specific member of the \"stack\", that lints files. 8 | 9 | Normally it's a wrapper around a linting library, with extra configuration, performance improvements, etc." 10 | 11 | (^::protocols.spec/reports lint! [^::protocols.spec/member this 12 | ^::protocols.spec/filenames filenames] 13 | "Lints `filenames` according to a linter of your choice: e.g. Eastwood, or Kibit, lein-dependency-check, etc.")) 14 | -------------------------------------------------------------------------------- /src/formatting_stack/protocols/processor.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.protocols.processor 2 | (:require 3 | [formatting-stack.protocols.spec :as protocols.spec] 4 | [nedap.speced.def :as speced])) 5 | 6 | (speced/defprotocol Processor 7 | "Any file-processing component that isn't a formatter or a linter." 8 | 9 | (^nil? process! [^::protocols.spec/member this 10 | ^::protocols.spec/filenames filenames] 11 | "Performs a compilation according to a processor of your choice: e.g. the ClojureScript processor, or Garden, Stefon, etc. 12 | You are free to ignore `filenames`, compiling the whole project instead.")) 13 | -------------------------------------------------------------------------------- /src/formatting_stack/protocols/reporter.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.protocols.reporter 2 | (:require 3 | [formatting-stack.protocols.spec :as protocols.spec] 4 | [nedap.speced.def :as speced])) 5 | 6 | (speced/defprotocol Reporter 7 | "A Reporter prints (or writes) info coming from other members, such as Formatters, Linters, etc." 8 | 9 | (report [this, ^::protocols.spec/reports reports] 10 | "Emits a report, out of the `reports` argument (namely a collection of discrete actionable items).")) 11 | -------------------------------------------------------------------------------- /src/formatting_stack/protocols/spec.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.protocols.spec 2 | (:require 3 | [clojure.spec.alpha :as spec] 4 | [nedap.speced.def :as speced] 5 | [nedap.utils.spec.api :refer [check!]] 6 | [nedap.utils.spec.predicates :refer [present-string?]])) 7 | 8 | (speced/def-with-doc :formatting-stack.protocols.spec.member/id 9 | "Members (formatters, linters, etc) identify themselves, 10 | so that final users can locate them and configure them." 11 | keyword?) 12 | 13 | (speced/def-with-doc ::member 14 | "A 'member' of the stack that does something useful: a formatter, linter or processor. 15 | 16 | 'Strategies' and 'Reporters' are not members - instead they help members accomplish their purpose." 17 | (fn [x] 18 | (if-not (map? x) 19 | ;; we are facing a `reify`, which means that formatting-stack is being customized 20 | ;; In those cases, an :id is practically useless (since the point of :id is overriding f-s), so no validation needed: 21 | true 22 | (check! (spec/keys :req-un [:formatting-stack.protocols.spec.member/id]) 23 | x)))) 24 | 25 | (spec/def ::filename present-string?) 26 | 27 | (spec/def ::filenames (spec/coll-of ::filename)) 28 | 29 | (spec/def ::msg present-string?) 30 | 31 | (spec/def ::msg-extra-data (spec/coll-of present-string?)) 32 | 33 | (spec/def ::source qualified-keyword?) 34 | 35 | (spec/def ::column nat-int?) 36 | 37 | (spec/def ::line nat-int?) 38 | 39 | (spec/def ::warning-details-url present-string?) 40 | 41 | (spec/def ::diff present-string?) 42 | 43 | (spec/def ::level #{:warning :error :exception}) 44 | 45 | (defmulti reportmm :level) 46 | 47 | (defmethod reportmm :exception [_] 48 | (spec/keys :req-un [::msg 49 | ::exception 50 | ::source 51 | ::level] 52 | :opt-un [::filename])) 53 | 54 | (defmethod reportmm :default [_] 55 | (spec/keys :req-un [::filename 56 | ::source 57 | ::msg 58 | ::level] 59 | :opt-un [::column ;; not every linter reports column/line, see https://git.io/JfuoJ 60 | ::line 61 | ::msg-extra-data 62 | ::warning-details-url 63 | ::diff])) 64 | 65 | (spec/def ::report 66 | (spec/multi-spec reportmm :level)) 67 | 68 | (spec/def ::reports (spec/coll-of ::report)) 69 | -------------------------------------------------------------------------------- /src/formatting_stack/reporters/file_writer.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.reporters.file-writer 2 | "Writes the output to a file which can be observed with e.g. `watch --color -n 1 cat .formatting-stack-report`." 3 | (:require 4 | [formatting-stack.protocols.reporter :as protocols.reporter] 5 | [formatting-stack.reporters.pretty-printer :as pretty-printer] 6 | [nedap.utils.modular.api :refer [implement]])) 7 | 8 | (defn write-report [{:keys [printer filename]} reports] 9 | (->> (with-out-str 10 | (protocols.reporter/report printer reports)) 11 | (spit filename))) 12 | 13 | (defn new [{:keys [printer filename] 14 | :or {printer (pretty-printer/new {}) 15 | filename ".formatting-stack-report"}}] 16 | (implement {:printer printer 17 | :filename filename} 18 | protocols.reporter/--report write-report)) 19 | -------------------------------------------------------------------------------- /src/formatting_stack/reporters/impl.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.reporters.impl 2 | (:require 3 | [clojure.spec.alpha :as spec] 4 | [clojure.string :as string] 5 | [formatting-stack.protocols.spec :as protocols.spec] 6 | [medley.core :refer [assoc-some]] 7 | [nedap.speced.def :as speced])) 8 | 9 | (speced/def-with-doc ::filename 10 | "Filename which can be nil when an exception occurred" 11 | (spec/nilable ::protocols.spec/filename)) 12 | 13 | (speced/defn normalize-filenames 14 | "Removes the CWD from the filenames, for more concise output, and also for ensuring correct grouping." 15 | [{:keys [^::filename filename] :as report}] 16 | (assoc-some report :filename (some-> filename 17 | (string/replace (re-pattern (str "^" (System/getProperty "user.dir"))) 18 | "") 19 | (string/replace #"^/" "")))) 20 | 21 | (speced/defn truncate-line-wise [^string? s, length] 22 | (if (= s "\n") 23 | s 24 | (->> (string/split s #"\n") 25 | (map (fn [s] 26 | (let [suffix "…" 27 | string-length (count s) 28 | suffix-length (count suffix)] 29 | (if (<= string-length length) 30 | s 31 | (str (subs s 32 | 0 33 | (- length suffix-length)) 34 | suffix))))) 35 | (string/join "\n")))) 36 | -------------------------------------------------------------------------------- /src/formatting_stack/reporters/passthrough.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.reporters.passthrough 2 | (:require 3 | [formatting-stack.protocols.reporter :as protocols.reporter] 4 | [nedap.utils.modular.api :refer [implement]])) 5 | 6 | (defn report [_ _] 7 | []) 8 | 9 | (defn new 10 | "Does not perform a report. Apt for the test suite." 11 | [] 12 | (implement {} 13 | protocols.reporter/--report report)) 14 | -------------------------------------------------------------------------------- /src/formatting_stack/reporters/pretty_line_printer.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.reporters.pretty-line-printer 2 | "Outputs report similar to github.com/xojs/xo" 3 | (:require 4 | [clojure.stacktrace :refer [print-stack-trace]] 5 | [formatting-stack.protocols.reporter :as reporter] 6 | [formatting-stack.protocols.spec :as protocols.spec] 7 | [formatting-stack.reporters.impl :refer [truncate-line-wise]] 8 | [formatting-stack.util :refer [colorize]] 9 | [medley.core :refer [map-vals]] 10 | [nedap.speced.def :as speced] 11 | [nedap.utils.modular.api :refer [implement]])) 12 | 13 | (speced/defn print-summary [{:keys [^boolean? summary? 14 | ^boolean? colorize?]} reports] 15 | (when summary? 16 | (->> reports 17 | (group-by :level) 18 | (map-vals count) 19 | (into (sorted-map-by compare)) ;; print summary in order 20 | (run! (fn [[report-type n]] 21 | (cond-> (str n (case report-type 22 | :exception " exceptions occurred" 23 | :error " errors found" 24 | :warning " warnings found")) 25 | colorize? (colorize (case report-type 26 | :exception :red 27 | :error :red 28 | :warning :yellow)) 29 | true println)))))) 30 | 31 | (speced/defn print-exceptions [{:keys [^boolean? print-stacktraces?]} reports] 32 | (->> reports 33 | (filter (speced/fn [{:keys [^::protocols.spec/level level]}] 34 | (#{:exception} level))) 35 | (group-by :filename) 36 | (run! (fn [[title reports]] 37 | (println (colorize title :cyan)) 38 | (doseq [{:keys [^Throwable exception]} reports] 39 | (if print-stacktraces? 40 | (print-stack-trace exception) 41 | (println (ex-message exception))) 42 | (println)))))) 43 | 44 | (speced/defn print-warnings [{:keys [max-msg-length 45 | ^boolean? colorize?]} 46 | ^::protocols.spec/reports reports] 47 | (->> (filter (fn [{:keys [level]}] (#{:error :warning} level)) reports) 48 | (group-by :filename) 49 | (into (sorted-map-by compare)) 50 | (run! (fn [[title reports]] 51 | (println (cond-> title colorize? (colorize :cyan))) 52 | (doseq [{:keys [msg msg-extra-data column line source level]} (sort-by :line reports)] 53 | (println (case level 54 | :error " ❌" 55 | :warning " ⚠️") 56 | (if (or column line) 57 | (cond-> (format "%3s:%-3s" (or line "?") (or column "?")) colorize? (colorize :grey)) 58 | " ") 59 | (format (str "%-" max-msg-length "." max-msg-length "s") msg) 60 | (cond-> (str " " source) colorize? (colorize :grey))) 61 | (doseq [entry msg-extra-data] 62 | (println " " (truncate-line-wise entry max-msg-length)))))))) 63 | 64 | (defn print-report 65 | [options reports] 66 | (print-exceptions options reports) 67 | (print-warnings options reports) 68 | (print-summary options reports)) 69 | 70 | (defn new [{:keys [max-msg-length print-stacktraces? summary? colorize?] 71 | :or {max-msg-length 100 72 | print-stacktraces? true 73 | summary? true 74 | colorize? true}}] 75 | (implement {:max-msg-length max-msg-length 76 | :print-stacktraces? print-stacktraces? 77 | :summary? summary? 78 | :colorize? colorize?} 79 | reporter/--report print-report)) 80 | -------------------------------------------------------------------------------- /src/formatting_stack/reporters/pretty_printer.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.reporters.pretty-printer 2 | "Prints an optionally colorized, indented, possibly truncated output of the reports." 3 | (:require 4 | [clojure.stacktrace :refer [print-stack-trace]] 5 | [formatting-stack.protocols.reporter :as reporter] 6 | [formatting-stack.protocols.spec :as protocols.spec] 7 | [formatting-stack.reporters.impl :refer [truncate-line-wise]] 8 | [formatting-stack.util :refer [colorize colorize-diff]] 9 | [medley.core :refer [map-vals]] 10 | [nedap.speced.def :as speced] 11 | [nedap.utils.modular.api :refer [implement]])) 12 | 13 | (speced/defn print-summary [{:keys [^boolean? summary? 14 | ^boolean? colorize?]} reports] 15 | (when summary? 16 | (->> reports 17 | (group-by :level) 18 | (map-vals count) 19 | (into (sorted-map-by compare)) ;; print summary in order 20 | (run! (fn [[report-type n]] 21 | (cond-> (str n (case report-type 22 | :exception " exceptions occurred" 23 | :error " errors found" 24 | :warning " warnings found")) 25 | colorize? (colorize (case report-type 26 | :exception :red 27 | :error :red 28 | :warning :yellow)) 29 | true println)))))) 30 | 31 | (speced/defn print-exceptions [{:keys [^boolean? print-stacktraces?]} reports] 32 | (->> reports 33 | (filter (speced/fn [{:keys [^::protocols.spec/level level]}] 34 | (#{:exception} level))) 35 | (group-by :filename) 36 | (run! (fn [[title reports]] 37 | (println (colorize title :cyan)) 38 | (doseq [{:keys [^Throwable exception]} reports] 39 | (if print-stacktraces? 40 | (print-stack-trace exception) 41 | (println (ex-message exception))) 42 | (println)))))) 43 | 44 | (speced/defn print-warnings [{:keys [max-msg-length 45 | ^boolean print-diff? 46 | ^boolean? colorize?]} 47 | ^::protocols.spec/reports reports] 48 | (->> reports 49 | (filter (speced/fn [{:keys [^::protocols.spec/level level]}] 50 | (#{:error :warning} level))) 51 | (group-by :filename) 52 | (into (sorted-map-by compare)) ;; sort filenames for consistent output 53 | (run! (fn [[title reports]] 54 | (println (cond-> title 55 | colorize? (colorize :cyan))) 56 | (doseq [[source-group group-entries] (->> reports 57 | (group-by :source)) 58 | :let [_ (println " " (cond-> source-group 59 | colorize? (colorize (case (-> group-entries first :level) 60 | :error :red 61 | :warning :yellow)))) 62 | _ (when-let [url (->> group-entries 63 | (keep :warning-details-url) 64 | first)] 65 | (cond-> (str " See: " url) 66 | colorize? (colorize :grey) 67 | true println))] 68 | {:keys [msg column line msg-extra-data msg-extra-data warning-details-url diff] 69 | :or {column "?", line "?"}} (->> group-entries 70 | (sort-by :line))] 71 | 72 | (println (cond-> (str " " line ":" column) 73 | colorize? (colorize :grey)) 74 | (truncate-line-wise msg max-msg-length)) 75 | (when (and diff print-diff?) 76 | (println (cond-> diff 77 | colorize? colorize-diff))) 78 | (doseq [entry msg-extra-data] 79 | (println " " 80 | (truncate-line-wise entry max-msg-length)))) 81 | (println))))) 82 | 83 | (defn print-report [this reports] 84 | (print-exceptions this reports) 85 | (print-warnings this reports) 86 | (print-summary this reports)) 87 | 88 | (defn new [{:keys [max-msg-length print-diff? print-stacktraces? summary? colorize?] 89 | :or {max-msg-length 200 90 | print-diff? false 91 | print-stacktraces? true 92 | summary? true 93 | colorize? true}}] 94 | (implement {:max-msg-length max-msg-length 95 | :print-diff? print-diff? 96 | :print-stacktraces? print-stacktraces? 97 | :summary? summary? 98 | :colorize? colorize?} 99 | reporter/--report print-report)) 100 | -------------------------------------------------------------------------------- /src/formatting_stack/strategies/impl.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.strategies.impl 2 | (:require 3 | [clojure.java.shell :refer [sh]] 4 | [clojure.spec.alpha :as spec] 5 | [clojure.string :as string] 6 | [clojure.tools.namespace.file :as file] 7 | [clojure.tools.namespace.parse :as parse] 8 | [formatting-stack.formatters.clean-ns.impl :refer [safely-read-ns-contents]] 9 | [formatting-stack.util :refer [read-ns-decl]] 10 | [nedap.speced.def :as speced]) 11 | (:import 12 | (clojure.lang Namespace) 13 | (java.io File))) 14 | 15 | ;; modified, added, renamed 16 | (def git-completely-staged-regex #"^(M|A|R) ") 17 | 18 | ;; modified, added 19 | (def git-not-completely-staged-regex #"^( M|AM|MM|AD| D|\?\?|) ") 20 | 21 | (speced/def-with-doc ::file-entry 22 | "Not necessarily a filename, 23 | e.g. the string \"M src/formatting_stack/strategies/impl.clj\" is a valid output." 24 | string?) 25 | 26 | (spec/def ::file-entries (spec/coll-of ::file-entry)) 27 | 28 | (speced/defn ^::file-entries file-entries 29 | [& args] 30 | (->> args (apply sh) :out string/split-lines (filter seq))) 31 | 32 | (def separator-pattern (re-pattern File/separator)) 33 | 34 | (def ^:dynamic *skip-existing-files-check?* false) 35 | 36 | (spec/def ::existing-files (spec/coll-of (speced/fn [^string? s] 37 | (if *skip-existing-files-check?* 38 | true 39 | (-> s File. .exists))))) 40 | 41 | (speced/defn ^::existing-files absolutize [command, ^::file-entries file-entries] 42 | (let [toplevel-fragments (case command 43 | "git" (-> (sh "git" "rev-parse" "--show-toplevel") 44 | (:out) 45 | (string/split #"\n") 46 | (first) 47 | (string/split separator-pattern)))] 48 | (->> file-entries 49 | (map (fn [filename] 50 | (->> (string/split filename separator-pattern) 51 | (concat toplevel-fragments) 52 | (string/join File/separator) 53 | File. 54 | .getCanonicalPath)))))) 55 | 56 | (def ^:dynamic *filter-existing-files?* true) 57 | 58 | (defn safe-the-ns [ns-name] 59 | (try 60 | (the-ns ns-name) 61 | (catch Exception _))) 62 | 63 | (speced/defn ^::speced/nilable ^Namespace filename->ns [^String filename] 64 | ;; sometimes, cider-nrepl or other tooling can create (essentially empty) `Namespace` objects for .cljs files. 65 | ;; These are not only useless, but prevent us from processing .cljs namespaces properly 66 | ;; (i.e. parse their contents instead of inspecting the JVM runtime). 67 | ;; So, we disregard .cljs files: 68 | (when-not (-> filename (.endsWith ".cljs")) 69 | (some-> filename read-ns-decl parse/name-from-ns-decl safe-the-ns))) 70 | 71 | (defn readable? 72 | "Is this file readable to clojure.tools.reader? (given custom reader tags, unbalanced parentheses or such)" 73 | [^String filename] 74 | (if-not (-> filename File. .exists) 75 | true ;; undecidable 76 | (try 77 | (let [ns-obj (filename->ns filename)] 78 | (and (do 79 | (if-not ns-obj 80 | true 81 | (-> filename slurp (safely-read-ns-contents ns-obj))) 82 | true) 83 | (if-let [decl (-> filename file/read-file-ns-decl)] 84 | (do 85 | (-> decl parse/deps-from-ns-decl) ;; no exceptions thrown 86 | true) 87 | true))) 88 | (catch Exception _ 89 | false) 90 | (catch AssertionError _ 91 | false)))) 92 | 93 | (defn extract-clj-files [files] 94 | (cond->> files 95 | true (filter #(re-find #"\.(clj|cljc|cljs|edn)$" %)) 96 | *filter-existing-files?* (filter (fn [^String f] 97 | (-> f File. .exists))) 98 | true (remove #(string/ends-with? % "project.clj")) 99 | true (filter readable?))) 100 | 101 | (speced/defn ^boolean? dir-contains? 102 | [^string? dirname, ^File file] 103 | (->> (file-seq (File. dirname)) 104 | (map (speced/fn [^File f] 105 | (-> f .getCanonicalPath))) 106 | (some #{(-> file .getCanonicalPath)}) 107 | (boolean))) 108 | 109 | (speced/defn ^boolean? git-ref-exists? 110 | "Does `ref` denote an existing git branch, tag or commit sha?" 111 | [^string? ref] 112 | (-> (sh "git" "rev-parse" ref) 113 | :exit 114 | zero?)) 115 | -------------------------------------------------------------------------------- /src/formatting_stack/strategies/impl/git_diff.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.strategies.impl.git-diff 2 | "Functions for `git diff --name-status` parsing. 3 | 4 | Note that commands such as `git status`, `git ls-files` do not share its output format." 5 | (:require 6 | [clojure.string :as string] 7 | [nedap.speced.def :as speced])) 8 | 9 | (speced/defn deletion? [^string? s] 10 | (-> s (string/starts-with? "D\t"))) 11 | 12 | ;; See: `git diff --help`, `diff-filter` section 13 | (speced/defn remove-markers [^string? s] 14 | (-> s (string/replace-first #"^(A|C|D|M|R|T|U|X|B)\t" ""))) 15 | -------------------------------------------------------------------------------- /src/formatting_stack/strategies/impl/git_status.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.strategies.impl.git-status 2 | "Functions for `git status` parsing. Note that commands such as `git diff`, `git ls-files` do not share its output format." 3 | (:require 4 | [clojure.string :as string] 5 | [nedap.speced.def :as speced])) 6 | 7 | (def deletion-markers 8 | ;; NOTE: order matters (there are tests covering this), particularly for the last two members. 9 | ["DD " 10 | "DM " 11 | "DU " 12 | "AD " 13 | "CD " 14 | "MD " 15 | "RD " 16 | "UD " 17 | " D " 18 | "D "]) 19 | 20 | (speced/defn deleted-file? [^string? s] 21 | (->> deletion-markers 22 | (some (fn [marker] 23 | (string/starts-with? s marker))) 24 | (boolean))) 25 | 26 | (speced/defn remove-deletion-markers [^string? s] 27 | (->> deletion-markers 28 | (reduce (fn [result marker] 29 | (string/replace-first result marker "")) 30 | s))) 31 | -------------------------------------------------------------------------------- /src/formatting_stack/util/diff.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.util.diff 2 | (:require 3 | [clojure.java.data :as data] 4 | [clojure.java.io :as io] 5 | [clojure.spec.alpha :as spec] 6 | [formatting-stack.protocols.spec :as protocols.spec] 7 | [clojure.string :as string] 8 | [formatting-stack.util :refer [rcomp]] 9 | [nedap.speced.def :as speced] 10 | [nedap.utils.spec.predicates :refer [present-string?]]) 11 | (:import 12 | (difflib DiffUtils) 13 | (io.reflectoring.diffparser.api UnifiedDiffParser) 14 | (java.io File) 15 | (java.util.regex Pattern))) 16 | 17 | (spec/def ::start ::protocols.spec/line) 18 | (spec/def ::end ::protocols.spec/line) 19 | (spec/def ::filename present-string?) 20 | 21 | (speced/def-with-doc ::line-numbers 22 | "maps of consecutive removals/changes (as `:start` to `:end`) per `:filename`." 23 | (spec/coll-of (spec/keys :req-un [::start ::end ::filename]))) 24 | 25 | (speced/defn ^::line-numbers diff->line-numbers 26 | [^string? diff] 27 | (->> (.parse (UnifiedDiffParser.) (io/input-stream (.getBytes diff))) 28 | (data/from-java) 29 | (mapcat (speced/fn [{:keys [^present-string? toFileName ^coll? hunks]}] 30 | (->> hunks 31 | (mapcat (speced/fn [{:keys [^coll? lines] {:keys [^::start lineStart]} :fromFileRange}] 32 | (->> lines 33 | (remove (rcomp :lineType #{"TO"})) 34 | (map-indexed (fn [idx line] (assoc line :lineNumber (+ idx lineStart))))))) 35 | (reduce (speced/fn [ret {:keys [^{::speced/spec #{"FROM" "NEUTRAL"}} lineType 36 | ^::protocols.spec/line lineNumber]}] 37 | (let [{:keys [end] :as current} (last ret)] 38 | (cond 39 | (#{"NEUTRAL"} lineType) 40 | ret 41 | 42 | (= end (dec lineNumber)) 43 | (assoc ret (dec (count ret)) (assoc current :end lineNumber)) ;; update last map 44 | 45 | :else 46 | (conj ret {:filename (string/replace-first toFileName "b/" "") 47 | :start lineNumber 48 | :end lineNumber})))) 49 | [])))))) 50 | 51 | (def diff-context-size 3) 52 | 53 | (def ^:dynamic *to-absolute-path-fn* 54 | (fn [filename] 55 | (->> (string/split filename (re-pattern (Pattern/quote File/separator))) 56 | ^File (apply io/file) 57 | .getCanonicalPath))) 58 | 59 | (speced/defn ^string? unified-diff 60 | "derives a patch derived from the original and revised file contents in a Unified Diff format" 61 | ([^string? filename ^string? original ^string? revised] 62 | (letfn [(lines [s] 63 | (string/split s #"\n")) 64 | (unlines [ss] 65 | (string/join "\n" ss))] 66 | (unlines (DiffUtils/generateUnifiedDiff 67 | (->> filename *to-absolute-path-fn* (str "a")) 68 | (->> filename *to-absolute-path-fn* (str "b")) 69 | (lines original) 70 | (DiffUtils/diff (lines original) (lines revised)) 71 | diff-context-size))))) 72 | -------------------------------------------------------------------------------- /src/formatting_stack/util/ns.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.util.ns 2 | (:require 3 | [clojure.spec.alpha :as spec] 4 | [clojure.string :as string] 5 | [clojure.tools.reader :as tools.reader] 6 | [com.gfredericks.how-to-ns :as how-to-ns] 7 | [formatting-stack.util :refer [rcomp]] 8 | [nedap.speced.def :as speced] 9 | [nedap.utils.spec.api :refer [check!]])) 10 | 11 | (spec/def ::ns-form (spec/and sequential? 12 | (rcomp first #{'ns `ns}))) 13 | 14 | (spec/def ::ns-form-str (spec/and string? 15 | (complement string/blank?))) 16 | 17 | (spec/def ::clean-ns-form (spec/nilable string?)) 18 | 19 | (speced/defn formatted= [^string? original-ns-form 20 | ^::clean-ns-form clean-ns-form 21 | ^map? how-to-ns-opts] 22 | (if-not clean-ns-form 23 | true 24 | (->> [original-ns-form clean-ns-form] 25 | (map (fn [form] 26 | (how-to-ns/format-ns-str form how-to-ns-opts))) 27 | (apply =)))) 28 | 29 | (speced/defn safely-read-ns-form [^::ns-form-str ns-form-str] 30 | (-> (tools.reader/read-string {:read-cond :preserve 31 | :features #{:clj :cljs}} 32 | (str "[ " ns-form-str " ]")) 33 | first)) 34 | 35 | (speced/defn replaceable-ns-form 36 | [^string? filename, ^ifn? ns-cleaner, ^map? how-to-ns-opts] 37 | (let [buffer (slurp filename) 38 | original-ns-form-str (-> buffer how-to-ns/slurp-ns-from-string) 39 | original-ns-form (-> original-ns-form-str safely-read-ns-form) 40 | clean-ns-form (ns-cleaner original-ns-form)] 41 | (check! ::ns-form-str original-ns-form-str 42 | ::ns-form original-ns-form 43 | ::clean-ns-form clean-ns-form) 44 | (cond 45 | (not clean-ns-form) 46 | nil 47 | 48 | (= original-ns-form clean-ns-form) 49 | nil 50 | 51 | (formatted= original-ns-form-str clean-ns-form how-to-ns-opts) 52 | nil 53 | 54 | true 55 | {:buffer buffer 56 | :original-ns-form-str original-ns-form-str 57 | :final-ns-form-str (how-to-ns/format-ns-str clean-ns-form how-to-ns-opts)}))) 58 | 59 | (speced/defn ^nil? write-ns-replacement! 60 | [^string? filename {:keys [^string? final-ns-form-str 61 | ^string? original-ns-form-str 62 | ^string? buffer]}] 63 | (->> original-ns-form-str 64 | count 65 | (subs buffer) 66 | (str final-ns-form-str) 67 | (spit filename))) 68 | 69 | (speced/defn replace-ns-form! 70 | [^string? filename, ^ifn? ns-cleaner, ^map? how-to-ns-opts] 71 | (some->> (replaceable-ns-form filename ns-cleaner how-to-ns-opts) 72 | (write-ns-replacement! filename))) 73 | -------------------------------------------------------------------------------- /test-resources-extra/orpn.clj: -------------------------------------------------------------------------------- 1 | (ns orpn 2 | "This namespace aides testing `formatting-stack.linters.one-resource-per-ns`") 3 | -------------------------------------------------------------------------------- /test-resources-extra/orpn.cljc: -------------------------------------------------------------------------------- 1 | (ns orpn 2 | "This namespace aides testing `formatting-stack.linters.one-resource-per-ns`") 3 | -------------------------------------------------------------------------------- /test-resources-extra/orpn.cljs: -------------------------------------------------------------------------------- 1 | (ns orpn 2 | "This namespace aides testing `formatting-stack.linters.one-resource-per-ns`") 3 | -------------------------------------------------------------------------------- /test-resources/component_repl.clj: -------------------------------------------------------------------------------- 1 | (ns component-repl 2 | "Documents a working setup for Component, as a runnable example. 3 | 4 | Not a part of the test suite. 5 | 6 | See also: 7 | 8 | * https://github.com/nedap/formatting-stack/blob/master/README.md 9 | * https://github.com/nedap/formatting-stack/wiki/FAQ 10 | * The `customization-example` sibling namespace." 11 | (:require 12 | [com.stuartsierra.component :as component] 13 | [com.stuartsierra.component.repl :as component.repl] 14 | [formatting-stack.component] 15 | [formatting-stack.defaults])) 16 | 17 | (def sample-linters 18 | (conj formatting-stack.defaults/default-linters 19 | (reify formatting-stack.protocols.linter/Linter 20 | (--lint! [this filenames] 21 | [{:source ::my-linter 22 | :level :warning 23 | :column 40 24 | :line 6 25 | :msg "Hello, I am a sample linter!" 26 | :filename "path.clj"}])))) 27 | 28 | (defn init [_] 29 | (component/system-map :formatting-stack (formatting-stack.component/new {:linters sample-linters}))) 30 | 31 | (component.repl/set-init init) 32 | 33 | (comment 34 | (component.repl/reset)) 35 | -------------------------------------------------------------------------------- /test-resources/customization_example.clj: -------------------------------------------------------------------------------- 1 | (ns customization-example 2 | "Documents how users can customize formatting-stack, as a runnable example. 3 | 4 | Not a part of the test suite. 5 | 6 | See also: 7 | 8 | * https://github.com/nedap/formatting-stack/blob/master/README.md 9 | * https://github.com/nedap/formatting-stack/wiki/FAQ" 10 | (:require 11 | [formatting-stack.core] 12 | [formatting-stack.defaults] 13 | [formatting-stack.linters.kondo :as kondo] 14 | [formatting-stack.linters.line-length :as line-length] 15 | [formatting-stack.linters.ns-aliases :as ns-aliases])) 16 | 17 | ;; You an implement your own linters: 18 | (def custom-linters 19 | [(reify formatting-stack.protocols.linter/Linter 20 | (--lint! [this filenames] 21 | [{:source ::my-linter 22 | :level :warning 23 | :column 40 24 | :line 6 25 | :msg "Hello, I am a sample linter!" 26 | :filename "path.clj"}]))]) 27 | 28 | ;; You can tweak the default linters' configuration: 29 | (def tweaked-linters 30 | (->> formatting-stack.defaults/default-linters 31 | (keep (fn [{:keys [id] :as linter}] 32 | ;; all formatters and linters have an `:id`. 33 | (case id 34 | ;; change :max-line-length from 130 to 80: 35 | ::line-length/id (assoc linter :max-line-length 80) 36 | 37 | ;; remove an undesired linter: 38 | ::ns-aliases/id nil 39 | 40 | ;; override some kondo defaults. They will be deep-merged against formatting-stack's kondo config: 41 | ::kondo/id (assoc linter 42 | :kondo-clj-options {:linters {:cond-else {:level :warning}}} 43 | ;; remember there are different options, for clj and cljs. 44 | :kondo-cljs-options {:linters {:duplicate-require {:level :warning}}}) 45 | linter))) 46 | (vec))) 47 | 48 | (def all-linters 49 | (into custom-linters tweaked-linters)) 50 | 51 | (comment 52 | (formatting-stack.core/format! :linters all-linters 53 | :formatters [] ;; disable all formatters (as an example of how to do that) 54 | :in-background? false)) 55 | -------------------------------------------------------------------------------- /test-resources/deletable.clj: -------------------------------------------------------------------------------- 1 | (ns deletable 2 | "A namespace that can be deleted by the test suite, for exercising `strategies`. 3 | 4 | If doing so, remember to restore that file, within your test.") 5 | -------------------------------------------------------------------------------- /test-resources/diffs/1.diff: -------------------------------------------------------------------------------- 1 | diff --git a/test/unit/formatting_stack/util.clj b/test/unit/formatting_stack/util.clj 2 | index 0649d14..af8a181 100644 3 | --- a/test/unit/formatting_stack/util.clj 4 | +++ b/test/unit/formatting_stack/util.clj 5 | @@ -15,3 +15,7 @@ 6 | "test-resources/sample_cljs_ns.cljs" '(ns sample-cljs-ns 7 | (:require [foo.bar.baz :as baz]) 8 | (:require-macros [sample-cljs-ns :refer [the-macro]])))) 9 | +(deftest diff->line-numbers 10 | + (are [input expected] (= expected 11 | + (sut/diff->line-numbers input)))) 12 | + 13 | -------------------------------------------------------------------------------- /test-resources/diffs/2.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/formatting_stack/formatters/trivial_ns_duplicates.clj b/src/formatting_stack/formatters/trivial_ns_duplicates.clj 2 | index a7f6024..687ea61 100644 3 | --- a/src/formatting_stack/formatters/trivial_ns_duplicates.clj 4 | +++ b/src/formatting_stack/formatters/trivial_ns_duplicates.clj 5 | @@ -11,7 +11,7 @@ 6 | [formatting-stack.protocols.formatter :as formatter] 7 | [formatting-stack.protocols.linter :as linter] 8 | [formatting-stack.util :refer [diff->line-numbers ensure-coll ensure-sequential process-in-parallel! rcomp read-ns-decl]] 9 | - [formatting-stack.util.ns :as util.ns :refer [replace-ns-form! replaceable-ns-form]] 10 | + [formatting-stack.util.ns :as util.ns :refer [write-ns-replacement! replace-ns-form!]] 11 | [medley.core :refer [deep-merge]] 12 | [nedap.speced.def :as speced] 13 | [nedap.utils.modular.api :refer [implement]] 14 | @@ -138,12 +138,18 @@ 15 | (speced/defn ^{::speced/spec (complement #{"nil"})} duplicate-cleaner [ns-form] 16 | (some-> ns-form remove-exact-duplicates pr-str)) 17 | 18 | +(defn replaceable-ns-form 19 | + [how-to-ns-opts filename] 20 | + (when (read-ns-decl filename) 21 | + (util.ns/replaceable-ns-form filename duplicate-cleaner how-to-ns-opts))) 22 | + 23 | + 24 | (defn format! [{:keys [how-to-ns-opts]} files] 25 | (->> files 26 | (process-in-parallel! (fn [filename] 27 | - (when (read-ns-decl filename) 28 | + (when-let [replacement (replaceable-ns-form how-to-ns-opts filename)] 29 | (println "Removing trivial duplicates in `ns` form:" filename) 30 | - (replace-ns-form! filename duplicate-cleaner how-to-ns-opts))))) 31 | + (write-ns-replacement! filename replacement))))) 32 | nil) 33 | 34 | (defn lint! [{:keys [how-to-ns-opts]} files] 35 | @@ -151,7 +157,7 @@ 36 | (process-in-parallel! (fn [filename] 37 | (when-let [{:keys [final-ns-form-str 38 | original-ns-form-str]} 39 | - (replaceable-ns-form filename duplicate-cleaner how-to-ns-opts)] 40 | + (replaceable-ns-form how-to-ns-opts filename)] 41 | (let [diff (#'cljfmt.diff/unified-diff filename original-ns-form-str final-ns-form-str)] 42 | (->> (diff->line-numbers diff) 43 | (mapv (fn [{:keys [begin]}] 44 | -------------------------------------------------------------------------------- /test-resources/diffs/3.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/formatting_stack/formatters/no_extra_blank_lines.clj b/src/formatting_stack/formatters/no_extra_blank_lines.clj 2 | index 0cd19b8..28bc5b9 100644 3 | --- a/src/formatting_stack/formatters/no_extra_blank_lines.clj 4 | +++ b/src/formatting_stack/formatters/no_extra_blank_lines.clj 5 | @@ -9,7 +9,7 @@ 6 | [formatting-stack.util :refer [ensure-sequential process-in-parallel!]] 7 | [nedap.utils.modular.api :refer [implement]])) 8 | 9 | -(def ^:const extra-newline-pattern 10 | +(def extra-newline-pattern 11 | #"(\n\n)(\n)+") 12 | 13 | (defn without-extra-newlines [s] 14 | @@ -27,10 +27,15 @@ 15 | 16 | (defn lint! [this files] 17 | (letfn [(extra-line-seq [content] 18 | - (let [m (re-matcher extra-newline-pattern content)] 19 | - ((fn step [] 20 | - (when (. m (find)) ;; fixme might need some cleanup 21 | - (cons (+ 2 (count (re-seq #"\n" (subs content 0 (.start m))))) (lazy-seq (step))))))))] 22 | + (let [matcher (re-matcher extra-newline-pattern content) 23 | + current-offset (fn [] (->> (.start matcher) 24 | + (subs content 0) ;; remove content after cursor 25 | + (re-seq #"\n") 26 | + (count) ;; count all newlines from 0 -> cursor 27 | + (+ 2)))] ;; account for offset 28 | + ((fn next-offset [] 29 | + (when (.find matcher) 30 | + (cons (current-offset) (lazy-seq (next-offset))))))))] 31 | (->> files 32 | (process-in-parallel! (fn [filename] 33 | (->> (extra-line-seq (slurp filename)) 34 | diff --git a/src/formatting_stack/protocols/spec.clj b/src/formatting_stack/protocols/spec.clj 35 | index a705ff7..64fab6d 100644 36 | --- a/src/formatting_stack/protocols/spec.clj 37 | +++ b/src/formatting_stack/protocols/spec.clj 38 | @@ -38,7 +38,7 @@ so that final users can locate them and configure them." 39 | 40 | (spec/def ::warning-details-url present-string?) 41 | 42 | -(spec/def ::diff string?) 43 | +(spec/def ::diff present-string?) 44 | 45 | (spec/def ::level #{:warning :error :exception}) 46 | -------------------------------------------------------------------------------- /test-resources/diffs/4.diff: -------------------------------------------------------------------------------- 1 | diff --git a/test/unit/formatting_stack/strategies.clj b/test/unit/formatting_stack/strategies.clj 2 | index 78b6a35..4f5695e 100644 3 | --- a/test/unit/formatting_stack/strategies.clj 4 | +++ b/test/unit/formatting_stack/strategies.clj 5 | @@ -2,8 +2,8 @@ 6 | (:require 7 | [clojure.string :as str] 8 | [clojure.test :refer [deftest is use-fixtures]] 9 | - [formatting-stack.strategies :as sut] 10 | [formatting-stack.strategies.impl :as sut.impl] 11 | + [formatting-stack.strategies :as sut] 12 | [formatting-stack.strategies.impl.git-status :as git-status])) 13 | 14 | (use-fixtures :once (fn [tests] 15 | -------------------------------------------------------------------------------- /test-resources/diffs/5.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/formatting_stack/linters/line_length.clj b/src/formatting_stack/linters/line_length2.clj 2 | similarity index 100% 3 | rename from src/formatting_stack/linters/line_length.clj 4 | rename to src/formatting_stack/linters/line_length2.clj 5 | -------------------------------------------------------------------------------- /test-resources/diffs/6.diff: -------------------------------------------------------------------------------- 1 | diff --git a/test/unit/formatting_stack/util/diff.clj b/test/unit/formatting_stack/util/diff.clj 2 | deleted file mode 100644 3 | index 86f31a1..0000000 4 | --- a/test/unit/formatting_stack/util/diff.clj 5 | +++ /dev/null 6 | @@ -1,82 +0,0 @@ 7 | -(ns unit.formatting-stack.util.diff 8 | - (:require 9 | - [clojure.test :refer [are deftest is testing use-fixtures]] 10 | - [formatting-stack.test-helpers :refer [with-mocked-diff-path]] 11 | - [formatting-stack.util.diff :as sut])) 12 | - 13 | -(use-fixtures :once with-mocked-diff-path) 14 | - 15 | -(deftest diff->line-numbers 16 | - (are [description filename expected] (testing description 17 | - (is (= expected 18 | - (sut/diff->line-numbers (slurp filename)))) 19 | - true) 20 | - 21 | - "Additions do not report a line number" 22 | - "test-resources/diffs/1.diff" 23 | - [] 24 | - 25 | - "Multiple sections are reported individually" 26 | - "test-resources/diffs/2.diff" 27 | - [{:start 14 28 | - :end 14 29 | - :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 30 | - {:start 144 31 | - :end 144 32 | - :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 33 | - {:start 146 34 | - :end 146 35 | - :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 36 | - {:start 154 37 | - :end 154 38 | - :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"}] 39 | - 40 | - "consecutive removed lines are grouped" 41 | - "test-resources/diffs/3.diff" 42 | - [{:start 12 43 | - :end 12 44 | - :filename "src/formatting_stack/formatters/no_extra_blank_lines.clj"} 45 | - {:start 30 46 | - :end 33 47 | - :filename "src/formatting_stack/formatters/no_extra_blank_lines.clj"} 48 | - {:start 41 49 | - :end 41 50 | - :filename "src/formatting_stack/protocols/spec.clj"}] 51 | - 52 | - "Moving code reports on the removed line" 53 | - "test-resources/diffs/4.diff" 54 | - [{:start 5 55 | - :end 5 56 | - :filename "test/unit/formatting_stack/strategies.clj"}] 57 | - 58 | - "Renaming does not report a line number" 59 | - "test-resources/diffs/5.diff" 60 | - [])) 61 | - 62 | -(deftest unified-diff 63 | - (are [description filename revised-filename expected] (testing description 64 | - (is (= (slurp expected) 65 | - (sut/unified-diff filename 66 | - (slurp filename) 67 | - (slurp revised-filename)))) 68 | - true) 69 | - 70 | - "Adding to an empty file" 71 | - "test-resources/diffs/files/1.txt" 72 | - "test-resources/diffs/files/1_revised.txt" 73 | - "test-resources/diffs/files/1.diff" 74 | - 75 | - "Removing all contents" 76 | - "test-resources/diffs/files/1_revised.txt" 77 | - "test-resources/diffs/files/1.txt" 78 | - "test-resources/diffs/files/1_reversed.diff" 79 | - 80 | - "Removing newlines" 81 | - "test-resources/diffs/files/2.txt" 82 | - "test-resources/diffs/files/2_revised.txt" 83 | - "test-resources/diffs/files/2.diff" 84 | - 85 | - "Adding newlines" 86 | - "test-resources/diffs/files/2_revised.txt" 87 | - "test-resources/diffs/files/2.txt" 88 | - "test-resources/diffs/files/2_reversed.diff")) 89 | -------------------------------------------------------------------------------- /test-resources/diffs/7.diff: -------------------------------------------------------------------------------- 1 | diff --git a/test-resources/diffs/6.patch b/test-resources/diffs/6.patch 2 | new file mode 100644 3 | index 0000000..7aed28e 4 | --- /dev/null 5 | +++ b/test-resources/diffs/6.patch 6 | @@ -0,0 +1,88 @@ 7 | +diff --git a/test/unit/formatting_stack/util/diff.clj b/test/unit/formatting_stack/util/diff.clj 8 | +deleted file mode 100644 9 | +index 86f31a1..0000000 10 | +--- a/test/unit/formatting_stack/util/diff.clj 11 | ++++ /dev/null 12 | +@@ -1,82 +0,0 @@ 13 | +-(ns unit.formatting-stack.util.diff 14 | +- (:require 15 | +- [clojure.test :refer [are deftest is testing use-fixtures]] 16 | +- [formatting-stack.test-helpers :refer [with-mocked-diff-path]] 17 | +- [formatting-stack.util.diff :as sut])) 18 | +- 19 | +-(use-fixtures :once with-mocked-diff-path) 20 | +- 21 | +-(deftest diff->line-numbers 22 | +- (are [description filename expected] (testing description 23 | +- (is (= expected 24 | +- (sut/diff->line-numbers (slurp filename)))) 25 | +- true) 26 | +- 27 | +- "Additions do not report a line number" 28 | +- "test-resources/diffs/1.diff" 29 | +- [] 30 | +- 31 | +- "Multiple sections are reported individually" 32 | +- "test-resources/diffs/2.diff" 33 | +- [{:start 14 34 | +- :end 14 35 | +- :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 36 | +- {:start 144 37 | +- :end 144 38 | +- :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 39 | +- {:start 146 40 | +- :end 146 41 | +- :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 42 | +- {:start 154 43 | +- :end 154 44 | +- :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"}] 45 | +- 46 | +- "consecutive removed lines are grouped" 47 | +- "test-resources/diffs/3.diff" 48 | +- [{:start 12 49 | +- :end 12 50 | +- :filename "src/formatting_stack/formatters/no_extra_blank_lines.clj"} 51 | +- {:start 30 52 | +- :end 33 53 | +- :filename "src/formatting_stack/formatters/no_extra_blank_lines.clj"} 54 | +- {:start 41 55 | +- :end 41 56 | +- :filename "src/formatting_stack/protocols/spec.clj"}] 57 | +- 58 | +- "Moving code reports on the removed line" 59 | +- "test-resources/diffs/4.diff" 60 | +- [{:start 5 61 | +- :end 5 62 | +- :filename "test/unit/formatting_stack/strategies.clj"}] 63 | +- 64 | +- "Renaming does not report a line number" 65 | +- "test-resources/diffs/5.diff" 66 | +- [])) 67 | +- 68 | +-(deftest unified-diff 69 | +- (are [description filename revised-filename expected] (testing description 70 | +- (is (= (slurp expected) 71 | +- (sut/unified-diff filename 72 | +- (slurp filename) 73 | +- (slurp revised-filename)))) 74 | +- true) 75 | +- 76 | +- "Adding to an empty file" 77 | +- "test-resources/diffs/files/1.txt" 78 | +- "test-resources/diffs/files/1_revised.txt" 79 | +- "test-resources/diffs/files/1.diff" 80 | +- 81 | +- "Removing all contents" 82 | +- "test-resources/diffs/files/1_revised.txt" 83 | +- "test-resources/diffs/files/1.txt" 84 | +- "test-resources/diffs/files/1_reversed.diff" 85 | +- 86 | +- "Removing newlines" 87 | +- "test-resources/diffs/files/2.txt" 88 | +- "test-resources/diffs/files/2_revised.txt" 89 | +- "test-resources/diffs/files/2.diff" 90 | +- 91 | +- "Adding newlines" 92 | +- "test-resources/diffs/files/2_revised.txt" 93 | +- "test-resources/diffs/files/2.txt" 94 | +- "test-resources/diffs/files/2_reversed.diff")) 95 | -------------------------------------------------------------------------------- /test-resources/diffs/8.diff: -------------------------------------------------------------------------------- 1 | diff --git a/8.patch b/8.patch 2 | new file mode 100644 3 | index 0000000..2e3ff59 4 | --- /dev/null 5 | +++ b/8.patch 6 | @@ -0,0 +1 @@ 7 | +no-newline 8 | \ No newline at end of file 9 | -------------------------------------------------------------------------------- /test-resources/diffs/files/1.diff: -------------------------------------------------------------------------------- 1 | --- a/mocked/absolute/path/test-resources/diffs/files/1.txt 2 | +++ b/mocked/absolute/path/test-resources/diffs/files/1.txt 3 | @@ -1,1 +1,1 @@ 4 | - 5 | +Hello World! -------------------------------------------------------------------------------- /test-resources/diffs/files/1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nedap/formatting-stack/6fc23a538aea3fd256b39647b412b70973091c9d/test-resources/diffs/files/1.txt -------------------------------------------------------------------------------- /test-resources/diffs/files/1_reversed.diff: -------------------------------------------------------------------------------- 1 | --- a/mocked/absolute/path/test-resources/diffs/files/1_revised.txt 2 | +++ b/mocked/absolute/path/test-resources/diffs/files/1_revised.txt 3 | @@ -1,1 +1,1 @@ 4 | -Hello World! 5 | + -------------------------------------------------------------------------------- /test-resources/diffs/files/1_revised.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test-resources/diffs/files/2.diff: -------------------------------------------------------------------------------- 1 | --- a/mocked/absolute/path/test-resources/diffs/files/2.txt 2 | +++ b/mocked/absolute/path/test-resources/diffs/files/2.txt 3 | @@ -1,7 +1,5 @@ 4 | Hello World! 5 | 6 | - 7 | Hello World again! 8 | 9 | - 10 | Hello Earthlings! -------------------------------------------------------------------------------- /test-resources/diffs/files/2.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | 3 | 4 | Hello World again! 5 | 6 | 7 | Hello Earthlings! 8 | -------------------------------------------------------------------------------- /test-resources/diffs/files/2_reversed.diff: -------------------------------------------------------------------------------- 1 | --- a/mocked/absolute/path/test-resources/diffs/files/2_revised.txt 2 | +++ b/mocked/absolute/path/test-resources/diffs/files/2_revised.txt 3 | @@ -1,5 +1,7 @@ 4 | Hello World! 5 | 6 | + 7 | Hello World again! 8 | 9 | + 10 | Hello Earthlings! -------------------------------------------------------------------------------- /test-resources/diffs/files/2_revised.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | 3 | Hello World again! 4 | 5 | Hello Earthlings! 6 | -------------------------------------------------------------------------------- /test-resources/diffs/incorrect.diff: -------------------------------------------------------------------------------- 1 | --- a/mocked/absolute/path/test-resources/diffs/files/1_revised.txt 2 | --- a/mocked/absolute/path/test-resources/diffs/files/1_revised.txt 3 | -------------------------------------------------------------------------------- /test-resources/eastwood_warning.clj: -------------------------------------------------------------------------------- 1 | (ns eastwood-warning 2 | (:require 3 | [clojure.spec.alpha :as spec])) 4 | 5 | (def x (def y ::z)) 6 | 7 | (def reflection-warning 8 | (letfn [(get-path [x] (.getPath x))] 9 | (get-path (java.io.File. "a.clj")))) 10 | 11 | (def ^:dynamic *dynamic*) 12 | 13 | (defn no-pre-post-warning--1 14 | "This function should not return a prepost warning (because the precondition is dynamic)" 15 | [x] 16 | {:pre [*dynamic*]} 17 | x) 18 | 19 | (def logical-false false) 20 | 21 | (defn no-pre-post-warning--2 22 | "This function should return 1 prepost warning about `#'logical-false` (because the postcondition is dynamic)" 23 | [] 24 | {:pre [logical-false *dynamic*]} ;; variation: *dynamic* is not the first member 25 | logical-false) 26 | 27 | (defn no-pre-post-warning--3 28 | "This function should not return a prepost warning (because there's no precondition at all: the {:pre} is the return value)" 29 | [x] 30 | {:pre [x *dynamic*]}) 31 | 32 | (defn no-pre-post-warning--4 33 | "This function should return no prepost warnings." 34 | [x] 35 | {:pre [x *dynamic*]} ;; variation: *dynamic* is not the first member 36 | logical-false) 37 | 38 | ;; This shouldn't raise any warnings. See https://github.com/jonase/eastwood/issues/337 39 | (spec/coll-of any?) 40 | 41 | ;; This shouldn't raise any warnings. See https://github.com/jonase/eastwood/issues/336 42 | (defmulti example-mm identity) 43 | -------------------------------------------------------------------------------- /test-resources/extra_newlines_warning.clj: -------------------------------------------------------------------------------- 1 | (ns extra-newlines-warning) 2 | 3 | (def x ::x) 4 | 5 | 6 | 7 | (def y ::y) 8 | 9 | -------------------------------------------------------------------------------- /test-resources/integrant_repl.clj: -------------------------------------------------------------------------------- 1 | (ns integrant-repl 2 | "Documents a working setup for Integrant, as a runnable example. 3 | 4 | Not a part of the test suite. 5 | 6 | See also: 7 | 8 | * https://github.com/nedap/formatting-stack/blob/master/README.md 9 | * https://github.com/nedap/formatting-stack/wiki/FAQ 10 | * The `customization-example` sibling namespace." 11 | (:require 12 | [clojure.spec.alpha :as spec] 13 | [formatting-stack.defaults] 14 | [formatting-stack.integrant] 15 | [formatting-stack.protocols.spec :as protocols.spec] 16 | [integrant.repl])) 17 | 18 | (def sample-linters 19 | (conj formatting-stack.defaults/default-linters 20 | (reify formatting-stack.protocols.linter/Linter 21 | (--lint! [this filenames] 22 | [{:source ::my-linter 23 | :level :warning 24 | :column 40 25 | :line 6 26 | :msg "Hello, I am a sample linter!" 27 | :filename "path.clj"}])))) 28 | 29 | (integrant.repl/set-prep! (constantly {:formatting-stack.integrant/component {:linters sample-linters}})) 30 | 31 | (comment 32 | (integrant.repl/reset)) 33 | -------------------------------------------------------------------------------- /test-resources/invalid_syntax.clj: -------------------------------------------------------------------------------- 1 | #{] 2 | -------------------------------------------------------------------------------- /test-resources/kondo_warning.clj: -------------------------------------------------------------------------------- 1 | (ns kondo-warning) 2 | 3 | (let [unused ::unused] 4 | ::return) 5 | -------------------------------------------------------------------------------- /test-resources/linelength_sample.clj: -------------------------------------------------------------------------------- 1 | (ns linelength-sample) 2 | 3 | [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] ;; should be grouped 4 | ; intentional newline 5 | ; intentional newline 6 | ; intentional newline 7 | ; intentional newline 8 | ; intentional newline 9 | ; intentional newline 10 | ; intentional newline 11 | ; intentional newline 12 | ; intentional newline 13 | [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] ;; should be grouped 14 | [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] 15 | ; intentional newline 16 | ; intentional newline 17 | ; intentional newline 18 | ; intentional newline 19 | ; intentional newline 20 | ; intentional newline 21 | ; intentional newline 22 | [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] ;; should be grouped 23 | [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] 24 | [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] 25 | -------------------------------------------------------------------------------- /test-resources/linting_formatters_example.clj: -------------------------------------------------------------------------------- 1 | (ns linting-formatters-example 2 | "Documents how formatters can be used as linters for a non-mutative experience. 3 | 4 | Not a part of the test suite. 5 | 6 | NOTE: Highly subject to change! see https://github.com/nedap/formatting-stack/pull/152 7 | 8 | See also: 9 | 10 | * https://github.com/nedap/formatting-stack/blob/master/README.md 11 | * https://github.com/nedap/formatting-stack/wiki/FAQ" 12 | (:require 13 | [formatting-stack.core] 14 | [formatting-stack.defaults :refer [default-linters]] 15 | [formatting-stack.formatters.clean-ns :as formatters.clean-ns] 16 | [formatting-stack.formatters.cljfmt :as formatters.cljfmt] 17 | [formatting-stack.formatters.how-to-ns :as formatters.how-to-ns] 18 | [formatting-stack.formatters.newlines :as formatters.newlines] 19 | [formatting-stack.formatters.no-extra-blank-lines :as formatters.no-extra-blank-lines] 20 | [formatting-stack.formatters.trivial-ns-duplicates :as formatters.trivial-ns-duplicates] 21 | [formatting-stack.indent-specs :refer [default-third-party-indent-specs]] 22 | [formatting-stack.strategies :as strategies])) 23 | 24 | (def formatters 25 | (->> [(-> (formatters.cljfmt/new {:third-party-indent-specs default-third-party-indent-specs}) 26 | (assoc :strategies [strategies/all-files])) 27 | (-> (formatters.how-to-ns/new {}) 28 | (assoc :strategies [strategies/all-files])) 29 | (formatters.no-extra-blank-lines/new) 30 | (formatters.newlines/new {}) 31 | (-> (formatters.trivial-ns-duplicates/new {}) 32 | (assoc :strategies [strategies/all-files 33 | strategies/files-with-a-namespace 34 | strategies/exclude-edn])) 35 | (when (strategies/refactor-nrepl-available?) 36 | (-> (formatters.clean-ns/new {}) 37 | (assoc :strategies [strategies/all-files 38 | strategies/files-with-a-namespace 39 | strategies/exclude-cljc 40 | strategies/exclude-cljs 41 | strategies/exclude-edn 42 | strategies/namespaces-within-refresh-dirs-only 43 | strategies/do-not-use-cached-results!])))] 44 | (filterv some?))) 45 | 46 | (comment 47 | (formatting-stack.core/format! :linters (into default-linters formatters) 48 | :formatters [] ;; disable all formatters 49 | :in-background? false)) 50 | -------------------------------------------------------------------------------- /test-resources/magic.clj: -------------------------------------------------------------------------------- 1 | (ns magic 2 | (:require 3 | [fulcro.server])) 4 | -------------------------------------------------------------------------------- /test-resources/ns_aliases_warning.clj: -------------------------------------------------------------------------------- 1 | (ns ns-aliases-warning 2 | (:require 3 | [clojure.string :as foo])) 4 | -------------------------------------------------------------------------------- /test-resources/ns_unordered.clj: -------------------------------------------------------------------------------- 1 | (ns ns-unordered 2 | (:require 3 | [clojure.string :as foo] 4 | [clojure.stacktrace :as bar])) 5 | -------------------------------------------------------------------------------- /test-resources/orpn.clj: -------------------------------------------------------------------------------- 1 | (ns orpn 2 | "This namespace aides testing `formatting-stack.linters.one-resource-per-ns`") 3 | -------------------------------------------------------------------------------- /test-resources/orpn.cljc: -------------------------------------------------------------------------------- 1 | (ns orpn 2 | "This namespace aides testing `formatting-stack.linters.one-resource-per-ns`") 3 | -------------------------------------------------------------------------------- /test-resources/orpn.cljs: -------------------------------------------------------------------------------- 1 | (ns orpn 2 | "This namespace aides testing `formatting-stack.linters.one-resource-per-ns`") 3 | -------------------------------------------------------------------------------- /test-resources/sample_clj_ns.clj: -------------------------------------------------------------------------------- 1 | (ns sample-clj-ns 2 | (:require 3 | [foo.bar.baz :as baz]) 4 | (:import 5 | (java.util UUID))) 6 | -------------------------------------------------------------------------------- /test-resources/sample_cljc_ns.cljc: -------------------------------------------------------------------------------- 1 | (ns sample-cljc-ns 2 | (:require 3 | #?(:clj [foo.bar.baz :as baz-clj] 4 | :cljs [foo.bar.baz :as baz-cljs])) 5 | #?(:clj 6 | (:import 7 | (java.util UUID))) 8 | #?(:cljs 9 | (:require-macros 10 | [sample-cljc-ns :refer [the-macro]]))) 11 | -------------------------------------------------------------------------------- /test-resources/sample_cljs_ns.cljs: -------------------------------------------------------------------------------- 1 | (ns sample-cljs-ns 2 | (:require 3 | [foo.bar.baz :as baz]) 4 | (:require-macros 5 | [sample-cljs-ns :refer [the-macro]])) 6 | -------------------------------------------------------------------------------- /test-resources/valid_syntax.clj: -------------------------------------------------------------------------------- 1 | (ns valid-syntax) 2 | -------------------------------------------------------------------------------- /test-resources/wrong_indent.clj: -------------------------------------------------------------------------------- 1 | (ns wrong-indent) 2 | 3 | (def x 4 | [ 5 | 1 6 | 3 4 7 | 3]) 8 | -------------------------------------------------------------------------------- /test/formatting_stack/global_test_setup.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.global-test-setup 2 | (:require 3 | [clojure.tools.namespace.repl :refer [refresh-dirs set-refresh-dirs]])) 4 | 5 | (when (System/getenv "CI") 6 | (-> (reify Thread$UncaughtExceptionHandler 7 | (uncaughtException [_ thread e] 8 | (-> e pr-str println) 9 | (System/exit 1))) 10 | (Thread/setDefaultUncaughtExceptionHandler))) 11 | 12 | ;; * the "worker" source-path must be excluded (just like in `dev/dev.clj`) 13 | ;; * the "dev" source-path must be excluded (since it's not a concern of the test suite) 14 | ;; if updating this, please check if `dev/dev.clj` also needs updating. 15 | (when-not (seq refresh-dirs) ;; Make `lein test` pass (regardless of CI env) while not interfering with dev.clj 16 | (set-refresh-dirs "src" "test")) 17 | -------------------------------------------------------------------------------- /test/formatting_stack/test_helpers.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.test-helpers 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.java.shell :refer [sh with-sh-dir]] 5 | [formatting-stack.util.diff :as util.diff] 6 | [nedap.speced.def :as speced]) 7 | (:import 8 | (java.io File))) 9 | 10 | (speced/defn filename-as-resource [^string? filename] 11 | (str "file:" (-> filename 12 | File. 13 | .getAbsolutePath))) 14 | 15 | (defn with-mocked-diff-path 16 | "Fixture to stub the absolute path in #'util.diff/unified-diff" 17 | [t] 18 | (binding [util.diff/*to-absolute-path-fn* (fn [filename] 19 | (str "/mocked/absolute/path/" filename))] 20 | (t))) 21 | 22 | ;; https://gist.github.com/edw/5128978#gistcomment-2956766 23 | (defn recursive-delete 24 | [& fs] 25 | (when-let [f (first fs)] 26 | (if-let [cs (seq (.listFiles (io/file f)))] 27 | (recur (concat cs fs)) 28 | (do (io/delete-file f :silently) 29 | (recur (rest fs)))))) 30 | 31 | (def git-integration-dir 32 | "./git-integration-testing") 33 | 34 | (defn with-git-repo 35 | "Clones current repo into #'git-integration-dir for use in testing. 36 | Folder is removed afterwards." 37 | [t] 38 | (with-sh-dir git-integration-dir 39 | (try 40 | (recursive-delete git-integration-dir) 41 | (.mkdirs (io/as-file git-integration-dir)) 42 | (sh "git" "clone" (System/getProperty "user.dir") ".") 43 | (t) 44 | (finally 45 | (recursive-delete git-integration-dir))))) 46 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/component.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.component 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [com.stuartsierra.component :as component] 5 | [formatting-stack.component :as sut] 6 | [formatting-stack.reporters.passthrough :as reporters.passthrough] 7 | [nedap.utils.spec.api :refer [check!]])) 8 | 9 | (def sample-report 10 | {:source ::some-linter 11 | :level :warning 12 | :column 40 13 | :line 1 14 | :msg "omg" 15 | :filename "foo.clj"}) 16 | 17 | (defn proof [a] 18 | (reify formatting-stack.protocols.linter/Linter 19 | (--lint! [this filenames] 20 | (check! empty? filenames) 21 | (reset! a [sample-report])))) 22 | 23 | (deftest works 24 | (testing "It can be started/stopped without errors" 25 | (let [p (atom nil) 26 | ;; pass an empty stack (except for the `proof`), so that no side-effects will be triggered (would muddy the test suite): 27 | opts {:strategies [] 28 | :third-party-indent-specs {} 29 | :formatters [] 30 | :linters [(proof p)] 31 | :processors [] 32 | :in-background? false 33 | :reporter (reporters.passthrough/new)} 34 | instance (sut/new opts)] 35 | (is (= instance 36 | (component/start instance))) 37 | 38 | (testing "It actually runs its members, such as linters" 39 | (is (= [sample-report] 40 | @p))) 41 | 42 | (is (= instance 43 | (component/stop instance)))))) 44 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/core.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.core 2 | (:require 3 | [clojure.test :refer :all] 4 | [formatting-stack.core :as sut] 5 | [formatting-stack.protocols.formatter :as formatter] 6 | [formatting-stack.protocols.reporter :as protocols.reporter] 7 | [matcher-combinators.test :refer [match?]] 8 | [nedap.utils.modular.api :refer [implement]]) 9 | (:import 10 | (clojure.lang ExceptionInfo))) 11 | 12 | (defn throw-exception [& args] 13 | (throw (ex-info "Kaboom!" {}))) 14 | 15 | (def failing-formatter 16 | (implement {:id ::failing-formatter} 17 | formatter/--format! throw-exception)) 18 | 19 | (defn store-in-latest [{::keys [latest]} args] 20 | (reset! latest args)) 21 | 22 | (defn recording-reporter [latest] 23 | (implement {::latest latest} 24 | protocols.reporter/--report store-in-latest)) 25 | 26 | (deftest format! 27 | (testing "exceptions are passed to the reporter" 28 | (let [latest (atom nil)] 29 | (sut/format! :formatters [failing-formatter] 30 | :linters [] 31 | :processors [] 32 | :reporter (recording-reporter latest) 33 | :in-background? false) 34 | (is (match? [{:source :formatting-stack/process! 35 | :msg "Exception during {:id :functional.formatting-stack.core/failing-formatter}" 36 | :level :exception 37 | :exception #(instance? ExceptionInfo %)}] 38 | @latest))))) 39 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns 2 | (:require 3 | [clojure.test :refer [are deftest is]] 4 | [formatting-stack.formatters.clean-ns :as sut] 5 | [formatting-stack.formatters.clean-ns.impl :as impl] 6 | [formatting-stack.formatters.how-to-ns] 7 | [formatting-stack.strategies :as strategies] 8 | [formatting-stack.util :refer [read-ns-decl]] 9 | [formatting-stack.util.ns :as util.ns] 10 | [functional.formatting-stack.formatters.clean-ns.should-be-cleaned] 11 | [functional.formatting-stack.formatters.clean-ns.should-not-be-cleaned] 12 | [functional.formatting-stack.formatters.clean-ns.should-not-be-cleaned-2] 13 | [functional.formatting-stack.formatters.clean-ns.should-not-be-partially-cleaned])) 14 | 15 | (def should-be-cleaned-f "test/functional/formatting_stack/formatters/clean_ns/should_be_cleaned.clj") 16 | (def should-be-cleaned (read-ns-decl should-be-cleaned-f)) 17 | 18 | (def should-not-be-partially-cleaned-f "test/functional/formatting_stack/formatters/clean_ns/should_not_be_partially_cleaned.clj") 19 | (def should-not-be-partially-cleaned (read-ns-decl should-not-be-partially-cleaned-f)) 20 | 21 | (def should-not-be-cleaned-f "test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned.clj") 22 | (def should-not-be-cleaned (read-ns-decl should-not-be-cleaned-f)) 23 | 24 | (def should-not-be-cleaned-2-f "test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned_2.clj") 25 | (def should-not-be-cleaned-2 (read-ns-decl should-not-be-cleaned-2-f)) 26 | 27 | (def should-not-be-cleaned-3-f "test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned_3.clj") 28 | (def should-not-be-cleaned-3 (read-ns-decl should-not-be-cleaned-3-f)) 29 | 30 | (def should-not-be-cleaned-4-f "test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned_4.clj") 31 | (def should-not-be-cleaned-4 (read-ns-decl should-not-be-cleaned-4-f)) 32 | 33 | (def should-not-be-cleaned-5-f "test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned_5.clj") 34 | (def should-not-be-cleaned-5 (read-ns-decl should-not-be-cleaned-5-f)) 35 | 36 | (assert should-be-cleaned) 37 | 38 | (assert should-not-be-partially-cleaned) 39 | 40 | (assert should-not-be-cleaned) 41 | 42 | (assert should-not-be-cleaned-2) 43 | 44 | (assert should-not-be-cleaned-3) 45 | 46 | (assert should-not-be-cleaned-4) 47 | 48 | (assert should-not-be-cleaned-5) 49 | 50 | (when (strategies/refactor-nrepl-available?) 51 | (deftest used-namespace-names 52 | (is (not (seq (impl/used-namespace-names should-be-cleaned-f #{})))) 53 | (is (seq (impl/used-namespace-names should-not-be-partially-cleaned-f #{}))) 54 | (is (seq (impl/used-namespace-names should-not-be-cleaned-f #{}))) 55 | (is (seq (impl/used-namespace-names should-not-be-cleaned-2-f #{}))))) 56 | 57 | (when (strategies/refactor-nrepl-available?) 58 | (deftest clean-ns-form 59 | (are [op filename libspec-whitelist namespaces-that-should-never-cleaned] 60 | (let [cleaner (sut/make-cleaner formatting-stack.formatters.how-to-ns/default-how-to-ns-opts 61 | @sut/default-nrepl-opts 62 | namespaces-that-should-never-cleaned 63 | libspec-whitelist 64 | filename) 65 | v (util.ns/replaceable-ns-form filename cleaner formatting-stack.formatters.how-to-ns/default-how-to-ns-opts)] 66 | (op v)) 67 | some? should-be-cleaned-f sut/default-libspec-whitelist #{} 68 | nil? should-be-cleaned-f sut/default-libspec-whitelist #{'functional.formatting-stack.formatters.clean-ns.should-be-cleaned} 69 | some? "dev/user.clj" sut/default-libspec-whitelist #{} 70 | nil? "dev/user.clj" sut/default-libspec-whitelist sut/default-namespaces-that-should-never-cleaned 71 | nil? should-not-be-partially-cleaned-f sut/default-libspec-whitelist #{} 72 | nil? should-not-be-cleaned-f sut/default-libspec-whitelist #{} 73 | nil? should-not-be-cleaned-2-f sut/default-libspec-whitelist #{} 74 | nil? should-not-be-cleaned-3-f sut/default-libspec-whitelist #{} 75 | nil? should-not-be-cleaned-4-f sut/default-libspec-whitelist #{} 76 | nil? should-not-be-cleaned-5-f sut/default-libspec-whitelist #{} 77 | some? should-not-be-cleaned-3-f #{} #{} 78 | some? should-not-be-cleaned-4-f #{} #{} 79 | some? should-not-be-cleaned-5-f #{} #{}))) 80 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/does_not_have_duplicates.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.does-not-have-duplicates 2 | (:require 3 | [clojure.string] 4 | [clojure.set])) 5 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/has_duplicates.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.has-duplicates 2 | (:require 3 | clojure.string 4 | [clojure.string])) 5 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/has_duplicates_2.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.has-duplicates-2 2 | (:require 3 | [clojure.string :as a] 4 | [clojure.string :as b])) 5 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/has_duplicates_3.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.has-duplicates-3 2 | (:require 3 | [clojure.string :as string] 4 | [clojure.string])) 5 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/impl.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.impl 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.test :refer [are deftest]] 5 | [formatting-stack.formatters.clean-ns.impl :as sut])) 6 | 7 | (deftest has-duplicate-requires? 8 | (are [input assertion] (let [file (io/file "test" "functional" "formatting_stack" "formatters" "clean_ns" input) 9 | _ (assert (-> file .exists)) 10 | filename (-> file .getAbsolutePath) 11 | result (sut/has-duplicate-requires? filename)] 12 | (case assertion 13 | :has-duplicates result 14 | :does-not-have-duplicates (not result))) 15 | "has_duplicates.clj" :has-duplicates 16 | "has_duplicates_2.clj" :has-duplicates 17 | "has_duplicates_3.clj" :has-duplicates 18 | "impl.clj" :does-not-have-duplicates 19 | "does_not_have_duplicates.clj" :does-not-have-duplicates)) 20 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/should_be_cleaned.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.should-be-cleaned 2 | (:require 3 | [clojure.java.io :as io])) 4 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.should-not-be-cleaned 2 | (:require 3 | [clojure.java.io :as io])) 4 | 5 | ^{:foo ^{::io/foo 42} []} [] 6 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned_2.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.should-not-be-cleaned-2 2 | (:require 3 | [clojure.java.io :as io])) 4 | 5 | ^{:foo ^{:foo io/as-url} []} [] 6 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned_3.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.should-not-be-cleaned-3 2 | "Should not cleaned because of `#'formatting-stack.formatters.clean-ns`." 3 | (:require 4 | [functional.formatting-stack.formatters.clean-ns.specs])) 5 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned_4.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.should-not-be-cleaned-4 2 | (:require 3 | [functional.formatting-stack.formatters.clean-ns.specs.foo])) 4 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/should_not_be_cleaned_5.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.should-not-be-cleaned-5 2 | (:require 3 | [specs.foo])) 4 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/should_not_be_partially_cleaned.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.should-not-be-partially-cleaned 2 | (:require 3 | ;; ideally [as-file] would get removed, but with the current approach that's impossible. 4 | [clojure.java.io :as io :refer [as-file]])) 5 | 6 | ^{:foo ^{:foo io/copy} []} [] 7 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/specs.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.specs) 2 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/clean_ns/specs/foo.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.clean-ns.specs.foo) 2 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/cljfmt.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.cljfmt 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.formatters.cljfmt :as sut] 5 | [formatting-stack.protocols.linter :as linter] 6 | [matcher-combinators.test :refer [match?]])) 7 | 8 | (deftest lint! 9 | (let [linter (sut/new {})] 10 | (are [filename expected] (match? expected 11 | (linter/lint! linter [filename])) 12 | "test-resources/valid_syntax.clj" 13 | [] 14 | 15 | "test-resources/invalid_syntax.clj" 16 | [{:source :formatting-stack/report-processing-error 17 | :level :exception 18 | :filename "test-resources/invalid_syntax.clj"}] 19 | 20 | "test-resources/wrong_indent.clj" 21 | [{:source :cljfmt/indent 22 | :msg "Indentation or whitespace is off on line 4-5" 23 | :line 4 24 | :column 0 25 | :filename "test-resources/wrong_indent.clj"} 26 | {:source :cljfmt/indent 27 | :msg "Indentation or whitespace is off on line 7" 28 | :line 7 29 | :column 0 30 | :filename "test-resources/wrong_indent.clj"}]))) 31 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/how_to_ns.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.how-to-ns 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.formatters.how-to-ns :as sut] 5 | [formatting-stack.protocols.linter :as linter] 6 | [matcher-combinators.test :refer [match?]])) 7 | 8 | (deftest lint! 9 | (let [linter (sut/new {})] 10 | (are [filename expected] (match? expected 11 | (linter/lint! linter [filename])) 12 | "test-resources/valid_syntax.clj" 13 | [] 14 | 15 | "test-resources/invalid_syntax.clj" 16 | [{:source :formatting-stack/report-processing-error 17 | :level :exception 18 | :filename "test-resources/invalid_syntax.clj"}] 19 | 20 | "test-resources/ns_unordered.clj" 21 | [{:source :how-to-ns/ns 22 | :msg "Detected unsorted, renamed or extra clauses in the ns format" 23 | :diff string? 24 | :filename "test-resources/ns_unordered.clj"}]))) 25 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/newlines.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.newlines 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.formatters.newlines :as sut] 5 | [formatting-stack.protocols.linter :as linter] 6 | [matcher-combinators.test :refer [match?]])) 7 | 8 | (deftest lint! 9 | (let [linter (sut/new {})] 10 | (are [filename expected] (match? expected 11 | (linter/lint! linter [filename])) 12 | "test-resources/valid_syntax.clj" 13 | [] 14 | 15 | "test-resources/invalid_syntax.clj" 16 | [] 17 | 18 | "test-resources/extra_newlines_warning.clj" 19 | [{:source :formatting-stack/newlines 20 | :msg "File should end in 1 newlines" 21 | :line 7 22 | :column 1 23 | :filename "test-resources/extra_newlines_warning.clj"}]))) 24 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/formatters/no_extra_blank_lines.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.formatters.no-extra-blank-lines 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.formatters.no-extra-blank-lines :as sut] 5 | [formatting-stack.protocols.linter :as linter] 6 | [matcher-combinators.test :refer [match?]])) 7 | 8 | (deftest lint! 9 | (let [linter (sut/new)] 10 | (are [filename expected] (match? expected 11 | (linter/lint! linter [filename])) 12 | "test-resources/valid_syntax.clj" 13 | [] 14 | 15 | "test-resources/invalid_syntax.clj" 16 | [] 17 | 18 | "test-resources/extra_newlines_warning.clj" 19 | [{:source :formatting-stack/no-extra-blank-lines 20 | :msg "File has two or more consecutive blank lines" 21 | :line 4 22 | :column 1 23 | :filename "test-resources/extra_newlines_warning.clj"}]))) 24 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/integrant.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.integrant 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [formatting-stack.integrant :as sut] 5 | [formatting-stack.reporters.passthrough :as reporters.passthrough] 6 | [integrant.core :as integrant] 7 | [nedap.utils.spec.api :refer [check!]])) 8 | 9 | (def sample-report 10 | {:source ::some-linter 11 | :level :warning 12 | :column 40 13 | :line 1 14 | :msg "omg" 15 | :filename "foo.clj"}) 16 | 17 | (defn proof [a] 18 | (reify formatting-stack.protocols.linter/Linter 19 | (--lint! [this filenames] 20 | (check! empty? filenames) 21 | (reset! a [sample-report])))) 22 | 23 | (deftest works 24 | (testing "It can be started/stopped without errors" 25 | (let [p (atom nil) 26 | ;; pass an empty stack (except for the `proof`), so that no side-effects will be triggered (would muddy the test suite): 27 | opts {:strategies [] 28 | :third-party-indent-specs {} 29 | :formatters [] 30 | :linters [(proof p)] 31 | :processors [] 32 | :in-background? false 33 | :reporter (reporters.passthrough/new)} 34 | system {:formatting-stack.integrant/component opts} 35 | started-system (integrant/init system)] 36 | 37 | (testing "It actually runs its members, such as linters" 38 | (is (= [sample-report] 39 | @p))) 40 | 41 | (integrant/halt! started-system)))) 42 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/kondo_classpath_cache.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.kondo-classpath-cache 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.kondo-classpath-cache :as sut] 5 | [formatting-stack.util :refer [rcomp]])) 6 | 7 | (deftest runner 8 | (testing "All choices have identical arglists (and therefore can be swapped transparently)" 9 | (let [arglists (->> sut/runner-mapping vals (map (rcomp find-var meta :arglists)))] 10 | (assert (seq arglists)) 11 | (assert (every? some? arglists)) 12 | (is (apply = arglists)))) 13 | 14 | (let [proof (atom nil)] 15 | (are [input expected] (testing "All arguments passed to the `body` are evaluated" 16 | (reset! proof []) 17 | (is (= expected 18 | input)) 19 | true) 20 | @(sut/runner (swap! proof conj 1)) [1] 21 | @(sut/runner (swap! proof conj 1) (swap! proof conj 2)) [1 2]))) 22 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/linters/eastwood.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.linters.eastwood 2 | (:require 3 | [clojure.test :refer [are deftest use-fixtures]] 4 | [formatting-stack.linters.eastwood :as sut] 5 | [formatting-stack.protocols.linter :as linter] 6 | [matcher-combinators.matchers :as matchers] 7 | [matcher-combinators.test :refer [match?]])) 8 | 9 | (use-fixtures :once (fn [tests] 10 | ;; prevent humongous AST representations from being printed: 11 | (binding [*print-level* 5] 12 | (tests)))) 13 | 14 | (deftest lint! 15 | (let [linter (sut/new {})] 16 | (are [filename expected] (match? expected 17 | (linter/lint! linter [filename])) 18 | "test-resources/valid_syntax.clj" 19 | [] 20 | 21 | "test-resources/invalid_syntax.clj" 22 | (matchers/equals 23 | [{:source :formatting-stack/report-processing-error 24 | :level :exception 25 | :msg "Encountered an exception while running Eastwood" 26 | :exception #(= "Unmatched delimiter ]." (ex-message %))}]) 27 | 28 | "test-resources/eastwood_warning.clj" 29 | (matchers/in-any-order 30 | [{:source :eastwood/reflection 31 | :msg "reference to field getPath can't be resolved." 32 | :line pos-int? 33 | :column pos-int? 34 | :warning-details-url "https://github.com/jonase/eastwood#reflection" 35 | :filename "test-resources/eastwood_warning.clj"} 36 | {:source :eastwood/def-in-def 37 | :line pos-int? 38 | :column pos-int? 39 | :warning-details-url "https://github.com/jonase/eastwood#def-in-def" 40 | :filename "test-resources/eastwood_warning.clj"} 41 | {:source :eastwood/wrong-pre-post 42 | :line pos-int? 43 | :column pos-int? 44 | :warning-details-url "https://github.com/jonase/eastwood#wrong-pre-post" 45 | :filename "test-resources/eastwood_warning.clj"}])))) 46 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/linters/kondo.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.linters.kondo 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.linters.kondo :as sut] 5 | [formatting-stack.protocols.linter :as linter] 6 | [matcher-combinators.matchers :as matchers] 7 | [matcher-combinators.test :refer [match?]])) 8 | 9 | (deftest lint! 10 | (let [linter (sut/new {:kondo-clj-options {:output {:exclude-files []} 11 | :linters {:unused-binding {:level :warning}}}})] 12 | (are [filename expected] (match? expected 13 | (linter/lint! linter [filename])) 14 | "test-resources/valid_syntax.clj" 15 | [] 16 | 17 | "test-resources/invalid_syntax.clj" 18 | (matchers/embeds 19 | [{:level :error, 20 | :filename "test-resources/invalid_syntax.clj", 21 | :line 1, 22 | :column 2, 23 | :source :kondo/syntax}]) 24 | 25 | "test-resources/kondo_warning.clj" 26 | (matchers/embeds 27 | [{:source :kondo/unused-binding 28 | :level :warning 29 | :line 3 30 | :column 7 31 | :filename "test-resources/kondo_warning.clj"}])))) 32 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/linters/line_length.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.linters.line-length 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.linters.line-length :as sut] 5 | [formatting-stack.protocols.linter :as linter] 6 | [matcher-combinators.test :refer [match?]])) 7 | 8 | (deftest lint! 9 | (let [linter (sut/new {:max-line-length 24 10 | :merge-threshold 5})] 11 | (are [filename expected] (match? expected 12 | (linter/lint! linter [filename])) 13 | "test-resources/valid_syntax.clj" 14 | [] 15 | 16 | "test-resources/invalid_syntax.clj" 17 | [] 18 | 19 | "test-resources/sample_clj_ns.clj" 20 | [{:source :formatting-stack/line-length 21 | :line 3 22 | :column 25 23 | :msg "Line exceeding 24 columns." 24 | :filename "test-resources/sample_clj_ns.clj"}] 25 | 26 | "test-resources/linelength_sample.clj" 27 | [{:filename "test-resources/linelength_sample.clj", 28 | :source :formatting-stack/line-length, 29 | :level :warning, 30 | :line 3, 31 | :column 54, 32 | :msg "Line exceeding 24 columns."} 33 | {:source :formatting-stack/line-length 34 | :line 13 35 | :column 54 36 | :msg "Line exceeding 24 columns (spanning 2 lines)." 37 | :filename "test-resources/linelength_sample.clj"} 38 | {:filename "test-resources/linelength_sample.clj", 39 | :source :formatting-stack/line-length, 40 | :level :warning, 41 | :line 22, 42 | :column 54, 43 | :msg "Line exceeding 24 columns (spanning 3 lines)."}]))) 44 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/linters/loc_per_ns.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.linters.loc-per-ns 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.linters.loc-per-ns :as sut] 5 | [formatting-stack.protocols.linter :as linter] 6 | [matcher-combinators.test :refer [match?]])) 7 | 8 | (deftest lint! 9 | (let [linter (sut/new {:max-lines-per-ns 4})] 10 | (are [filename expected] (match? expected 11 | (linter/lint! linter [filename])) 12 | "test-resources/valid_syntax.clj" 13 | [] 14 | 15 | "test-resources/invalid_syntax.clj" 16 | [] 17 | 18 | "test-resources/sample_clj_ns.clj" 19 | [{:source :formatting-stack/loc-per-ns 20 | :line 5 21 | :column 0 22 | :msg "Longer than 4 LOC." 23 | :filename "test-resources/sample_clj_ns.clj"}]))) 24 | -------------------------------------------------------------------------------- /test/functional/formatting_stack/linters/ns_aliases.clj: -------------------------------------------------------------------------------- 1 | (ns functional.formatting-stack.linters.ns-aliases 2 | (:require 3 | [clojure.test :refer [are deftest testing]] 4 | [formatting-stack.linters.ns-aliases :as sut] 5 | [formatting-stack.linters.ns-aliases.impl :as sut.impl] 6 | [formatting-stack.protocols.linter :as linter] 7 | [formatting-stack.strategies :as strategies] 8 | [matcher-combinators.test :refer [match?]])) 9 | 10 | (deftest lint! 11 | (let [linter (sut/new {:max-lines-per-ns 4 12 | :augment-acceptable-aliases-whitelist? false})] 13 | (are [filename expected] (match? expected 14 | (linter/lint! linter [filename])) 15 | "test-resources/valid_syntax.clj" 16 | [] 17 | 18 | "test-resources/invalid_syntax.clj" 19 | [] ;; reader exceptions are ignored 20 | 21 | "test-resources/ns_aliases_warning.clj" 22 | [{:source :formatting-stack/ns-aliases 23 | :line 3 24 | :column 4 25 | :warning-details-url "https://stuartsierra.com/2015/05/10/clojure-namespace-aliases" 26 | :msg "[clojure.string :as foo] is not a derived alias." 27 | :filename "test-resources/ns_aliases_warning.clj"}])) 28 | 29 | (when (strategies/refactor-nrepl-3-4-1-available?) 30 | (testing "`:augment-acceptable-aliases-whitelist?`" 31 | (are [input expected] (match? expected 32 | (linter/lint! (sut/new {:augment-acceptable-aliases-whitelist? input}) 33 | ["test-resources/ns_aliases_warning.clj"])) 34 | true [] 35 | false [{:source :formatting-stack/ns-aliases 36 | :line 3 37 | :column 4 38 | :warning-details-url "https://stuartsierra.com/2015/05/10/clojure-namespace-aliases" 39 | :msg "[clojure.string :as foo] is not a derived alias." 40 | :filename "test-resources/ns_aliases_warning.clj"}])))) 41 | -------------------------------------------------------------------------------- /test/integration/formatting_stack/linters/one_resource_per_ns.clj: -------------------------------------------------------------------------------- 1 | (ns integration.formatting-stack.linters.one-resource-per-ns 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.linters.one-resource-per-ns :as sut] 5 | [formatting-stack.test-helpers :as test-helpers] 6 | [formatting-stack.util :refer [rcomp]])) 7 | 8 | (defn first! [coll] 9 | {:pre [(->> coll count #{1})]} 10 | (->> coll first)) 11 | 12 | (deftest analyze 13 | 14 | (testing "This namespace is unambiguous" 15 | (is (= (sut/analyze "test/integration/formatting_stack/linters/one_resource_per_ns.clj") 16 | ()))) 17 | 18 | (testing "Sample files are ambiguous (on a per-extension basis)" 19 | (are [input extension expected] (testing input 20 | (is (= (into #{} 21 | (map test-helpers/filename-as-resource) 22 | expected) 23 | (->> input 24 | sut/analyze 25 | (filter (rcomp :extension #{extension})) 26 | (first!) 27 | :filenames 28 | set))) 29 | true) 30 | "test-resources/orpn.clj" ".clj" #{"test-resources-extra/orpn.clj" 31 | "test-resources/orpn.clj"} 32 | "test-resources-extra/orpn.clj" ".clj" #{"test-resources-extra/orpn.clj" 33 | "test-resources/orpn.clj"} 34 | 35 | "test-resources/orpn.cljs" ".cljs" #{"test-resources-extra/orpn.cljs" 36 | "test-resources/orpn.cljs"} 37 | "test-resources-extra/orpn.cljs" ".cljs" #{"test-resources-extra/orpn.cljs" 38 | "test-resources/orpn.cljs"} 39 | 40 | "test-resources/orpn.cljc" ".cljc" #{"test-resources-extra/orpn.cljc" 41 | "test-resources/orpn.cljc"} 42 | "test-resources-extra/orpn.cljc" ".cljc" #{"test-resources-extra/orpn.cljc" 43 | "test-resources/orpn.cljc"}))) 44 | -------------------------------------------------------------------------------- /test/integration/formatting_stack/processors/test_runner.clj: -------------------------------------------------------------------------------- 1 | (ns integration.formatting-stack.processors.test-runner 2 | (:require 3 | [formatting-stack.processors.test-runner :as sut] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest test! 7 | (testing "asserts *load-tests* is true" 8 | (binding [*load-tests* false] 9 | (is (thrown-with-msg? AssertionError #"clojure.test/\*load-tests\*" 10 | (sut/test! :target-branch "main")))))) 11 | -------------------------------------------------------------------------------- /test/integration/formatting_stack/strategies/default_branch_name.clj: -------------------------------------------------------------------------------- 1 | (ns integration.formatting-stack.strategies.default-branch-name 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [formatting-stack.strategies :as sut])) 5 | 6 | ;; This deftest lives in its own ns, for not having unrelated fixtures interfere 7 | (deftest default-branch-name 8 | (is (= "main" (sut/default-branch-name)))) 9 | -------------------------------------------------------------------------------- /test/integration/formatting_stack/strategies/impl.clj: -------------------------------------------------------------------------------- 1 | (ns integration.formatting-stack.strategies.impl 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.strategies.impl :as sut]) 5 | (:import 6 | (java.io File) 7 | (java.util UUID))) 8 | 9 | (deftest dir-contains? 10 | (are [dirname filename expected] (= expected 11 | (sut/dir-contains? dirname filename)) 12 | 13 | "." (-> "src/formatting_stack/strategies/impl.clj" File.) true 14 | (-> "." File. .getAbsolutePath) (File. "project.clj") true 15 | "." (File. "project.clj") true 16 | "." (File. "dev/user.clj") true 17 | "dev" (File. "dev/user.clj") true 18 | "." (File. "LICENSE") true 19 | (-> "." File. .getAbsolutePath) (File. "LICENSE") true 20 | "." (File. "./LICENSE") true 21 | "dev" (File. "LICENSE") false 22 | "dev" (File. "./LICENSE") false 23 | (-> "." File. .getAbsolutePath) (File. "I_dont_exist") false 24 | "." (File. "I_dont_exist") false 25 | "dev" (File. "I_dont_exist") false 26 | "dev" (File. "user.clj") false)) 27 | 28 | (deftest absolutize 29 | (are [target] (testing target 30 | (is (= [(-> target File. .getCanonicalPath)] 31 | (sut/absolutize "git" [target]))) 32 | true) 33 | "src/formatting_stack/strategies/impl.clj" 34 | "src/../src/formatting_stack/strategies/impl.clj") 35 | 36 | (is (spec-assertion-thrown? ::sut/existing-files (sut/absolutize "git" ["I_dont_exist"])))) 37 | 38 | (deftest git-ref-exists? 39 | (are [input expected] (testing input 40 | (is (= expected 41 | (sut/git-ref-exists? input))) 42 | true) 43 | "f374fcc" true 44 | "main" true 45 | "v4.3.0" true 46 | (-> (UUID/randomUUID) str) false)) 47 | -------------------------------------------------------------------------------- /test/specs/foo.clj: -------------------------------------------------------------------------------- 1 | (ns specs.foo) 2 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/component/impl.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.component.impl 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.component.impl :as sut])) 5 | 6 | (deftest parse-options 7 | (are [input expected] (testing input 8 | (is (= expected 9 | (sut/parse-options input))) 10 | true) 11 | {} () 12 | {:a 1} '(:a 1) 13 | {:a 1 :b nil} '(:a 1))) 14 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/formatters/clean_ns.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.formatters.clean-ns 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [formatting-stack.formatters.clean-ns :as sut] 5 | [formatting-stack.strategies :as strategies])) 6 | 7 | (when (strategies/refactor-nrepl-available?) 8 | (deftest with-memoized-libspec-allowlist 9 | (testing "Binds `*libspec-allowlist*`, which means that memoization will be effectively used" 10 | (let [bound? (fn [] 11 | @(resolve 'refactor-nrepl.ns.libspec-allowlist/*libspec-allowlist*))] 12 | (assert (not (bound?))) 13 | (is (sut/with-memoized-libspec-allowlist 14 | (bound?))))))) 15 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/formatters/cljfmt/impl.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.formatters.cljfmt.impl 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.test :refer [are deftest is testing]] 5 | [formatting-stack.formatters.cljfmt.impl :as sut] 6 | [unit.formatting-stack.formatters.cljfmt.impl.sample-data :refer [foo]]) 7 | (:import 8 | (java.io File))) 9 | 10 | (def ^File this-file 11 | "A file that contains a 'foo refer." 12 | (io/file "test" "unit" "formatting_stack" "formatters" "cljfmt" "impl.clj")) 13 | 14 | (def ^File related-file 15 | "A file that contains a 'foo def." 16 | (io/file "test" "unit" "formatting_stack" "formatters" "cljfmt" "impl" "sample_data.clj")) 17 | 18 | (def ^File unrelated-file 19 | "A file that contains no 'foo def or refer." 20 | (io/file "test" "unit" "formatting_stack" "strategies.clj")) 21 | 22 | (assert (-> this-file .exists)) 23 | 24 | (assert (-> related-file .exists)) 25 | 26 | (assert (-> unrelated-file .exists)) 27 | 28 | (deftest cljfmt-indents-for 29 | 30 | (testing "No nil rules are produced" 31 | (is (every? identity (-> (sut/cljfmt-indents-for nil {}) 32 | vals)))) 33 | 34 | (testing "`:defn` resolves as a known-good rule" 35 | (let [key (gensym) 36 | rule (get (sut/cljfmt-indents-for nil {key {:style/indent :defn}}) 37 | key)] 38 | (is (= [[:inner 0]] 39 | rule)))) 40 | 41 | (testing "Nonsensical rules are omitted" 42 | (let [key (gensym) 43 | rule (get (sut/cljfmt-indents-for nil {key {:style/indent :sdlkfjdslfj}}) 44 | key 45 | ::not-found)] 46 | (is (= ::not-found 47 | rule)))) 48 | 49 | (testing "`def`s and `:refer`s are understood" 50 | (are [file e] (let [{u 'foo 51 | q 'unit.formatting-stack.formatters.cljfmt.impl.sample-data/foo} (-> file 52 | str 53 | (sut/cljfmt-indents-for {}))] 54 | (case e 55 | :unqualified-too 56 | (do 57 | (is (= [[:block 0]] 58 | u)) 59 | (is (= [[:block 0]] 60 | q))) 61 | 62 | :qualified-only 63 | (do 64 | (is (nil? u)) 65 | (is (= q 66 | [[:block 0]])))) 67 | 68 | true) 69 | 70 | this-file :unqualified-too 71 | related-file :unqualified-too 72 | unrelated-file :qualified-only)) 73 | 74 | (testing "Rules can override clojure.core's" 75 | (are [file e] (let [{u 'do 76 | q 'unit.formatting-stack.formatters.cljfmt.impl.sample-data/do} (-> file 77 | str 78 | (sut/cljfmt-indents-for {}))] 79 | 80 | (case e 81 | :unqualified-too 82 | (do 83 | (is (= [[:block 7]] 84 | u)) 85 | (is (= [[:block 7]] 86 | q))) 87 | 88 | :qualified-only 89 | (do 90 | (is (= [[:block 0]] 91 | u)) 92 | (is (= [[:block 7]] 93 | q)))) 94 | 95 | true) 96 | 97 | this-file :qualified-only 98 | related-file :unqualified-too))) 99 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/formatters/cljfmt/impl/magic_symbols.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.formatters.cljfmt.impl.magic-symbols 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.test :refer [are deftest is]] 5 | [formatting-stack.formatters.cljfmt.impl :as sut])) 6 | 7 | (deftest works 8 | (are [filename has-magic-indent?] (let [indents (sut/cljfmt-indents-for filename {}) 9 | action (-> indents (get 'action))] 10 | (is (> (count indents) 10)) 11 | (if has-magic-indent? 12 | (is (= [[:inner 0]] 13 | action)) 14 | (is (nil? action)))) 15 | 16 | nil 17 | false 18 | 19 | (-> (io/file "test-resources" "magic.clj") 20 | (.getAbsolutePath)) 21 | true)) 22 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/formatters/cljfmt/impl/sample_data.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.formatters.cljfmt.impl.sample-data 2 | (:refer-clojure :exclude [do])) 3 | 4 | (defmacro foo 5 | {:style/indent 0} 6 | []) 7 | 8 | (defmacro do 9 | {:style/indent 7} 10 | []) 11 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/formatters/no_extra_blank_lines.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.formatters.no-extra-blank-lines 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.formatters.no-extra-blank-lines :as sut])) 5 | 6 | (deftest without-extra-newlines 7 | (are [i e] (= e 8 | (sut/without-extra-newlines i)) 9 | "\n" 10 | "\n" 11 | 12 | "\n\n" 13 | "\n\n" 14 | 15 | "\n\n\n" 16 | "\n\n" 17 | 18 | "\n\n\n\n\n\n\n\n" 19 | "\n\n" 20 | 21 | "a\n\n\na" 22 | "a\n\na" 23 | 24 | "\n\na\n" 25 | "\n\na\n")) 26 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/indent_specs.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.indent-specs 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [formatting-stack.formatters.cljfmt.impl :as cljfmt.impl] 5 | [formatting-stack.indent-specs :as sut] 6 | [formatting-stack.processors.cider :as processors.cider] 7 | [formatting-stack.protocols.processor :refer [process!]])) 8 | 9 | (deftest default-third-party-indent-specs 10 | (testing "They can be processed without throwing errors, i.e. the quoted values are syntactically correct" 11 | 12 | (is (->> sut/default-third-party-indent-specs 13 | cljfmt.impl/cljfmt-third-party-indent-specs 14 | doall)) 15 | 16 | (is (do (-> (processors.cider/new {:third-party-indent-specs sut/default-third-party-indent-specs}) 17 | (process! [])) 18 | true)))) 19 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/linters/ns_aliases.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.linters.ns-aliases 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.linters.ns-aliases :as sut])) 5 | 6 | (deftest name-and-alias 7 | (are [input expected] (= expected 8 | (sut/name-and-alias input)) 9 | '[foo], ['foo nil] 10 | '[foo :refer :all], ['foo nil] 11 | '[foo :refer :all :as bar], ['foo 'bar] 12 | '[foo :as bar :refer :all], ['foo 'bar] 13 | '[foo :as bar], ['foo 'bar] 14 | '["foo" :as bar], ["foo" 'bar])) 15 | 16 | (deftest derived? 17 | (are [ns-name _as alias expected] (= expected 18 | (sut/derived? alias :from ns-name)) 19 | 'com.enterprise.foo.bar :as 'bar, true 20 | 'com.enterprise.foo.bar :as 'foo.bar, true 21 | 'com.enterprise.foo.bar :as 'enterprise.foo.bar, true 22 | 'com.enterprise.foo.bar :as 'com.enterprise.foo.bar, true 23 | 24 | 'com.enterprise.foo.bar :as 'com.enterprise.foo, false 25 | 'com.enterprise.foo.bar :as 'com.enterprise, false 26 | 'com.enterprise.foo.bar :as 'com, false 27 | 'com.enterprise.foo.bar :as 'enterprise, false 28 | 'com.enterprise.foo.bar :as 'foo, false 29 | 30 | 'com.enterprise.foo.bar :as 'b, false 31 | 32 | 'net.xyz.core :as 'xyz true 33 | 'net.xyz.core :as 'net.xyz true 34 | 35 | 'net.xyz.alpha :as 'xyz true 36 | 'net.xyz.alpha :as 'net.xyz true 37 | 38 | 'net.xyz.alpha.core :as 'xyz true 39 | 'net.xyz.alpha.core :as 'net.xyz true 40 | 41 | 'net.xyz.core.alpha :as 'xyz true 42 | 'net.xyz.core.alpha :as 'net.xyz true 43 | 44 | 'buddy.core.keys :as 'buddy.keys true 45 | 'buddy.core.keys :as 'buddy.core.keys true 46 | 'buddy.core.keys :as 'keys.buddy false 47 | 48 | 'clj-http :as 'http true 49 | 'clj-http :as 'htta false 50 | 'clj-time.core :as 'time true 51 | 'clj-time.core :as 'tima false 52 | 'clj-time.format :as 'time.format true 53 | 'clj-time.format :as 'time.farmat false)) 54 | 55 | (deftest acceptable-require-clause? 56 | (are [whitelist input expected] (= expected 57 | (sut/acceptable-require-clause? whitelist input)) 58 | 59 | sut/default-acceptable-aliases-whitelist 'foo true 60 | sut/default-acceptable-aliases-whitelist '[foo] true 61 | sut/default-acceptable-aliases-whitelist '[foo :as foo] true 62 | sut/default-acceptable-aliases-whitelist '[foo :as bar] false 63 | 64 | sut/default-acceptable-aliases-whitelist '[foo :as sut] true 65 | {} '[foo :as sut] false 66 | 67 | sut/default-acceptable-aliases-whitelist '[datomic.api :as d] true 68 | {} '[datomic.api :as d] false)) 69 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/linters/ns_aliases/impl.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.linters.ns-aliases.impl 2 | (:require 3 | [clojure.test :refer [are deftest]] 4 | [formatting-stack.linters.ns-aliases.impl :as sut] 5 | [matcher-combinators.matchers :as matchers] 6 | [matcher-combinators.test :refer [match?]])) 7 | 8 | (deftest merge-aliases 9 | (are [m1 m2 expected] (match? expected 10 | (sut/merge-aliases m1 m2)) 11 | {} {} {} 12 | {'a ['b]} {} {'a ['b]} 13 | {} {'a ['b]} {'a ['b]} 14 | {'a ['b]} {'a ['c]} {'a (matchers/in-any-order ['b 'c])} 15 | {'a ['b]} {'a ['b]} {'a ['b]} 16 | {'a ['b 'c] 'd ['e 'f]} {'a ['g 'h] 'd ['i 'j]} {'a (matchers/in-any-order ['b 'c 'g 'h]) 17 | 'd (matchers/in-any-order ['e 'f 'i 'j])} 18 | {'a ['b 'c] 'd ['e 'f]} {'a ['b 'c] 'd ['e 'f]} {'a (matchers/in-any-order ['b 'c]) 19 | 'd (matchers/in-any-order ['e 'f])})) 20 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/linters/one_resource_per_ns.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.linters.one-resource-per-ns 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.linters.one-resource-per-ns :as sut] 5 | [formatting-stack.test-helpers :as test-helpers])) 6 | 7 | (deftest ns-decl->resource-path 8 | (are [input expected] (testing input 9 | (is (= expected 10 | (sut/ns-decl->resource-path input ".clj"))) 11 | true) 12 | '(ns unit) "unit.clj" 13 | '(ns unit.formatting-stack.linters.one-resource-per-ns) "unit/formatting_stack/linters/one_resource_per_ns.clj" 14 | '(ns foo!?) "foo_BANG__QMARK_.clj")) 15 | 16 | (deftest resource-path->filenames 17 | (are [input] (testing input 18 | (is (= (-> "test/" (str input) test-helpers/filename-as-resource vector) 19 | (sut/resource-path->filenames input))) 20 | true) 21 | "unit/formatting_stack/linters/one_resource_per_ns.clj" 22 | "unit/formatting_stack/linters/ns_aliases.clj")) 23 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/processors/test_runner/impl.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.processors.test-runner.impl 2 | (:require 3 | [clojure.string :as string] 4 | [clojure.test :refer [are deftest is testing]] 5 | [formatting-stack.processors.test-runner.impl :as sut] 6 | [formatting-stack.project-parsing :refer [project-namespaces]] 7 | [nedap.speced.def :as speced] 8 | [nedap.utils.spec.api :refer [check!]])) 9 | 10 | (speced/defn make-ns [^keyword? k] 11 | (-> k 12 | str 13 | (string/replace "/" ".") 14 | (string/replace ":" "") 15 | symbol 16 | create-ns)) 17 | 18 | (deftest add-t 19 | (are [input expected] (= expected 20 | (-> input make-ns sut/add-t)) 21 | ::foo "unit.formatting-stack.processors.test-runner.impl.t-foo" 22 | ::foo.bar "unit.formatting-stack.processors.test-runner.impl.foo.t-bar")) 23 | 24 | (deftest sut-consumers 25 | (let [corpus (project-namespaces)] 26 | 27 | (check! (partial < 100) (-> corpus count)) 28 | 29 | (are [input expected] (= expected 30 | (sut/sut-consumers corpus input)) 31 | (the-ns 'formatting-stack.strategies) [(the-ns 'integration.formatting-stack.strategies) 32 | (the-ns 'integration.formatting-stack.strategies.default-branch-name) 33 | (the-ns 'unit.formatting-stack.strategies)]))) 34 | 35 | (deftest permutations 36 | (testing "Multi-segment ns" 37 | (is (= '("unit.com.example.thing" 38 | "test.com.example.thing" 39 | 40 | ;; note that this example doesn't make much sense but it is unavoidable: 41 | ;; `unit` could be anywhere in the middle of the ns 42 | "com.unit.example.thing" 43 | "com.test.example.thing" 44 | 45 | "com.example.unit.thing" 46 | "com.example.test.thing" 47 | 48 | "com.example.thing.unit" 49 | "com.example.thing.test") 50 | (sut/permutations (make-ns :com.example/thing) #{"test" "unit"})))) 51 | 52 | (testing "Single-segment ns" 53 | (is (= '("unit.thing" 54 | "test.thing" 55 | 56 | "thing.unit" 57 | "thing.test") 58 | (sut/permutations (make-ns :thing) #{"test" "unit"}))))) 59 | 60 | (deftest testable-namespaces 61 | (are [input expected] (= expected 62 | (sut/testable-namespaces input)) 63 | 64 | [] 65 | [] 66 | 67 | ;; `:test` metadata detection 68 | ["project.clj" 69 | "test/unit/formatting_stack/processors/test_runner/impl.clj" 70 | "/"] 71 | [(the-ns 'unit.formatting-stack.processors.test-runner.impl)] 72 | 73 | ;; `sut` alias detection 74 | ["src/formatting_stack/strategies.clj"] 75 | [(the-ns 'integration.formatting-stack.strategies) 76 | (the-ns 'integration.formatting-stack.strategies.default-branch-name) 77 | (the-ns 'unit.formatting-stack.strategies)])) 78 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/reporters/impl.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.reporters.impl 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.reporters.impl :as sut])) 5 | 6 | (deftest normalize-filenames 7 | (let [cwd (System/getProperty "user.dir")] 8 | (are [input expected] (testing input 9 | (is (= {:filename expected} 10 | (sut/normalize-filenames {:filename input}))) 11 | true) 12 | nil nil 13 | "a" "a" 14 | cwd "" 15 | (str cwd "/a.clj") "a.clj" 16 | (str cwd "/a.clj" cwd), (str "a.clj" cwd)))) 17 | 18 | (deftest truncate-line-wise 19 | (are [input expected] (testing input 20 | (is (= expected 21 | (sut/truncate-line-wise input 5))) 22 | true) 23 | "" "" 24 | "a" "a" 25 | "aaaaa" "aaaaa" 26 | "aaaaaEXCEEDING" "aaaa…" 27 | "\n" "\n" 28 | "a\na" "a\na" 29 | "a\naaaaaEXCEEDING" "a\naaaa…" 30 | "aaaaaEXCEEDING\na" "aaaa…\na")) 31 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/reporters/pretty_line_printer.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.reporters.pretty-line-printer 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.reporters.pretty-line-printer :as sut])) 5 | 6 | (deftest print-warnings 7 | (are [desc input expected] (testing [desc input] 8 | (is (= expected 9 | (with-out-str 10 | (sut/print-warnings {:colorize? false 11 | :max-msg-length 20} 12 | input)))) 13 | true) 14 | "Empty" 15 | [] 16 | "" 17 | 18 | "Basic" 19 | [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0}] 20 | "filename 21 | ⚠️ 0:0 message :unit.formatting-stack.reporters.pretty-line-printer/source\n" 22 | 23 | "Truncating message" 24 | [{:filename "filename", :source ::source, :level :warning, :line 0 :column 0, 25 | :msg "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et"}] 26 | "filename 27 | ⚠️ 0:0 Lorem ipsum dolor si :unit.formatting-stack.reporters.pretty-line-printer/source\n" 28 | 29 | "Sorts by `:line`" 30 | [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0} 31 | {:filename "filename", :msg "message", :source ::source, :level :warning, :line 2 :column 2} 32 | {:filename "filename", :msg "message", :source ::source, :level :warning, :line 1 :column 1}] 33 | "filename 34 | ⚠️ 0:0 message :unit.formatting-stack.reporters.pretty-line-printer/source 35 | ⚠️ 1:1 message :unit.formatting-stack.reporters.pretty-line-printer/source 36 | ⚠️ 2:2 message :unit.formatting-stack.reporters.pretty-line-printer/source\n" 37 | 38 | "Groups by `:source`" 39 | [{:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 0 :column 0} 40 | {:filename "filename", :msg "message", :source ::source-B, :level :warning, :line 2 :column 2} 41 | {:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 1 :column 1}] 42 | "filename 43 | ⚠️ 0:0 message :unit.formatting-stack.reporters.pretty-line-printer/source-A 44 | ⚠️ 1:1 message :unit.formatting-stack.reporters.pretty-line-printer/source-A 45 | ⚠️ 2:2 message :unit.formatting-stack.reporters.pretty-line-printer/source-B\n" 46 | 47 | "Can print `:msg-extra-data` (at the correct indentation level)" 48 | [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0, :msg-extra-data ["Foo" "Bar"]}] 49 | "filename 50 | ⚠️ 0:0 message :unit.formatting-stack.reporters.pretty-line-printer/source 51 | Foo 52 | Bar\n" 53 | 54 | "Can print missing `:column` and `:line`" 55 | [{:filename "filename", :msg "message", :source ::source, :level :warning}] 56 | "filename 57 | ⚠️ message :unit.formatting-stack.reporters.pretty-line-printer/source\n")) 58 | 59 | (deftest print-summary 60 | (are [input expected] (= expected 61 | (with-out-str 62 | (sut/print-summary {:colorize? false 63 | :summary? true} 64 | input))) 65 | (repeat 5 {:level :warning}) 66 | "5 warnings found\n" 67 | 68 | (repeat 4 {:level :error}) 69 | "4 errors found\n" 70 | 71 | (repeat 3 {:level :exception}) 72 | "3 exceptions occurred\n" 73 | 74 | (concat (repeat 5 {:level :warning}) 75 | (repeat 4 {:level :error}) 76 | (repeat 3 {:level :exception})) 77 | "4 errors found 78 | 3 exceptions occurred 79 | 5 warnings found\n")) 80 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/reporters/pretty_printer.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.reporters.pretty-printer 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.reporters.pretty-printer :as sut])) 5 | 6 | (deftest print-warnings 7 | (are [desc input expected] (testing [desc input] 8 | (is (= expected 9 | (with-out-str 10 | (sut/print-warnings {:colorize? false 11 | :print-diff? true 12 | :max-msg-length 20} 13 | input)))) 14 | true) 15 | "Empty" 16 | [] 17 | "" 18 | 19 | "Basic" 20 | [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0}] 21 | "filename 22 | :unit.formatting-stack.reporters.pretty-printer/source 23 | 0:0 message\n\n" 24 | 25 | "Sorts by `:line`" 26 | [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0} 27 | {:filename "filename", :msg "message", :source ::source, :level :warning, :line 2 :column 2} 28 | {:filename "filename", :msg "message", :source ::source, :level :warning, :line 1 :column 1}] 29 | "filename 30 | :unit.formatting-stack.reporters.pretty-printer/source 31 | 0:0 message 32 | 1:1 message 33 | 2:2 message\n\n" 34 | 35 | "Groups by `:source`" 36 | [{:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 0 :column 0} 37 | {:filename "filename", :msg "message", :source ::source-B, :level :warning, :line 2 :column 2} 38 | {:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 1 :column 1}] 39 | "filename 40 | :unit.formatting-stack.reporters.pretty-printer/source-A 41 | 0:0 message 42 | 1:1 message 43 | :unit.formatting-stack.reporters.pretty-printer/source-B 44 | 2:2 message\n\n" 45 | 46 | "Can print a given `:warning-details-url`, once at most per `:source` group" 47 | [{:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 0 :column 0} 48 | {:filename "filename", :msg "message", :source ::source-A, :level :warning, :line 1 :column 1 :warning-details-url "http://example.test/foo"}] 49 | "filename 50 | :unit.formatting-stack.reporters.pretty-printer/source-A 51 | See: http://example.test/foo 52 | 0:0 message 53 | 1:1 message\n\n" 54 | 55 | "Can print `:msg-extra-data` (at the correct indentation level)" 56 | [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0, :msg-extra-data ["Foo" "Bar"]}] 57 | "filename 58 | :unit.formatting-stack.reporters.pretty-printer/source 59 | 0:0 message 60 | Foo 61 | Bar\n\n" 62 | 63 | "Can print missing `:column` and `:line`" 64 | [{:filename "filename", :msg "message", :source ::source, :level :warning}] 65 | "filename 66 | :unit.formatting-stack.reporters.pretty-printer/source 67 | ?:? message\n\n" 68 | 69 | "Can print `:diff`" 70 | [{:filename "filename", :msg "message", :source ::source, :level :warning, :line 0 :column 0 :diff (slurp "test-resources/diffs/files/1.diff")}] 71 | "filename 72 | :unit.formatting-stack.reporters.pretty-printer/source 73 | 0:0 message 74 | --- a/mocked/absolute/path/test-resources/diffs/files/1.txt 75 | +++ b/mocked/absolute/path/test-resources/diffs/files/1.txt 76 | @@ -1,1 +1,1 @@ 77 | - 78 | +Hello World!\n\n")) 79 | 80 | (deftest print-summary 81 | (are [input expected] (= expected 82 | (with-out-str 83 | (sut/print-summary {:colorize? false 84 | :summary? true} 85 | input))) 86 | (repeat 5 {:level :warning}) 87 | "5 warnings found\n" 88 | 89 | (repeat 4 {:level :error}) 90 | "4 errors found\n" 91 | 92 | (repeat 3 {:level :exception}) 93 | "3 exceptions occurred\n" 94 | 95 | (concat (repeat 5 {:level :warning}) 96 | (repeat 4 {:level :error}) 97 | (repeat 3 {:level :exception})) 98 | "4 errors found 99 | 3 exceptions occurred 100 | 5 warnings found\n")) 101 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/strategies.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.strategies 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.test :refer [deftest is use-fixtures]] 5 | [formatting-stack.strategies :as sut] 6 | [formatting-stack.strategies.impl :as sut.impl] 7 | [formatting-stack.strategies.impl.git-status :as git-status])) 8 | 9 | (use-fixtures :once (fn [tests] 10 | (binding [sut.impl/*filter-existing-files?* false 11 | sut.impl/*skip-existing-files-check?* true] 12 | (tests)))) 13 | 14 | (def not-completely-staged-files 15 | ["AM test/unit/formatting_stack/a.clj" 16 | "MM test/unit/formatting_stack/a.clj" 17 | "AD test/unit/formatting_stack/b.clj" 18 | " M test/unit/formatting_stack/c.clj" 19 | " D test/unit/formatting_stack/d.clj" 20 | "?? test/unit/formatting_stack/e.clj"]) 21 | 22 | (def completely-staged-files 23 | ["M test/unit/formatting_stack/f.clj" 24 | "R test/unit/formatting_stack/g.clj -> test/unit/formatting_stack/G.clj" 25 | "A test/unit/formatting_stack/h.clj"]) 26 | 27 | (def all-files (into not-completely-staged-files completely-staged-files)) 28 | 29 | (defn strip-git [files] 30 | (letfn [(strip [s] 31 | (-> s 32 | (str/replace #".* " "") 33 | (str/replace "test/unit/formatting_stack/g.clj -> " "")))] 34 | (->> files 35 | (map strip) 36 | (sut.impl/absolutize "git")))) 37 | 38 | (deftest git-completely-staged 39 | (is (= (strip-git completely-staged-files) 40 | (sut/git-completely-staged :files [] :impl all-files)))) 41 | 42 | (deftest git-not-completely-staged 43 | (let [expected (->> not-completely-staged-files 44 | (remove git-status/deleted-file?) 45 | (strip-git))] 46 | (assert (-> expected count pos?)) 47 | (is (= expected 48 | (sut/git-not-completely-staged :files [] :impl all-files))))) 49 | 50 | (deftest git-diff-against-default-branch 51 | (is (= (sut.impl/absolutize "git" ["a.clj"]) 52 | (sut/git-diff-against-default-branch :target-branch "main" 53 | :files [] 54 | :impl ["a.clj" "b.clj"] 55 | :blacklist (sut.impl/absolutize "git" ["b.clj"]))))) 56 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/strategies/impl/git_diff.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.strategies.impl.git-diff 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.strategies.impl.git-diff :as sut])) 5 | 6 | (deftest deletion? 7 | (are [input expected] (testing input 8 | (is (= expected 9 | (sut/deletion? input))) 10 | true) 11 | "" false 12 | "D\t" true 13 | "D\t a.clj" true)) 14 | 15 | (deftest remove-markers 16 | (are [input expected] (testing input 17 | (is (= expected 18 | (sut/remove-markers input))) 19 | true) 20 | 21 | ;; `A` is replaced: 22 | 23 | "A\t" "" 24 | "A\tfoo.clj" "foo.clj" 25 | "foo.cljA\t" "foo.cljA\t" 26 | 27 | ;; `B` is replaced (and so on for other letters - we're not testing a whole regex range) 28 | 29 | "B\t" "" 30 | "B\tfoo.clj" "foo.clj" 31 | "foo.cljB\t" "foo.cljB\t" 32 | 33 | ;; `Ñ` is not replaced, because `git-diff` does not emit that 34 | 35 | "Ñ\t" "Ñ\t" 36 | "Ñ\tfoo.clj" "Ñ\tfoo.clj" 37 | "foo.cljÑ\t" "foo.cljÑ\t")) 38 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/strategies/impl/git_status.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.strategies.impl.git-status 2 | (:require 3 | [clojure.test :refer [are deftest is testing]] 4 | [formatting-stack.strategies.impl.git-status :as sut])) 5 | 6 | (deftest deleted-file? 7 | (are [input expected] (testing input 8 | (is (= expected 9 | (sut/deleted-file? input))) 10 | true) 11 | "D " true 12 | " D " true 13 | "DD " true 14 | "DM " true 15 | "DU " true 16 | "AD " true 17 | "CD " true 18 | "MD " true 19 | "RD " true 20 | "UD " true 21 | 22 | "" false 23 | "?? " false 24 | " M " false 25 | "M " false 26 | "MM " false 27 | "A " false 28 | "AM " false 29 | "R " false 30 | "RM " false 31 | "C " false 32 | "CM " false 33 | "UU " false 34 | "AA " false 35 | "AU " false 36 | "UA " false 37 | 38 | "../src/foo/bar.clj" false 39 | "src/foo/bar.clj" false)) 40 | 41 | (deftest remove-deletion-markers 42 | (are [input] (testing input 43 | (let [filename "a.clj" 44 | complete (str input filename)] 45 | (is (= filename 46 | (sut/remove-deletion-markers complete))) 47 | true)) 48 | "D " 49 | " D " 50 | "DD " 51 | "DM " 52 | "DU " 53 | "AD " 54 | "CD " 55 | "MD " 56 | "RD " 57 | "UD ")) 58 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/util.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.util 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.test :refer :all] 5 | [formatting-stack.util :as sut] 6 | [formatting-stack.util.diff :as diff] 7 | [matcher-combinators.test :refer [match?]])) 8 | 9 | (deftest read-ns-decl 10 | (are [input expected] (= expected 11 | (sut/read-ns-decl input)) 12 | "test-resources/sample_clj_ns.clj" '(ns sample-clj-ns 13 | (:require [foo.bar.baz :as baz]) 14 | (:import (java.util UUID))) 15 | "test-resources/sample_cljc_ns.cljc" '(ns sample-cljc-ns 16 | (:require [foo.bar.baz :as baz-clj]) 17 | (:import (java.util UUID))) 18 | "test-resources/sample_cljs_ns.cljs" '(ns sample-cljs-ns 19 | (:require [foo.bar.baz :as baz]) 20 | (:require-macros [sample-cljs-ns :refer [the-macro]])))) 21 | 22 | (deftest process-in-parallel! 23 | (testing "error reporting" 24 | (are [f xs expected] (match? expected 25 | (sut/process-in-parallel! f xs)) 26 | (fn [_] (throw (ex-info "expected" {}))) 27 | ["made_up_name.clj"] 28 | [{:exception #(= "expected" (ex-message %)) 29 | :level :exception 30 | :filename "made_up_name.clj" 31 | :msg #(str/starts-with? % "Encountered an exception while running")}] 32 | 33 | (fn [_] (assert false "expected")) 34 | ["made_up_name.clj"] 35 | [{:exception #(= "Assert failed: expected\nfalse" (ex-message %)) 36 | :level :exception 37 | :filename "made_up_name.clj" 38 | :msg #(str/starts-with? % "Encountered an exception while running")}] 39 | 40 | (fn [_] (diff/diff->line-numbers (slurp "test-resources/diffs/incorrect.diff"))) 41 | ["made_up_name.clj"] 42 | [{:exception #(= "A FROM_FILE line ('---') must be directly followed by a TO_FILE line ('+++')!" (ex-message %)) 43 | :level :exception 44 | :filename "made_up_name.clj" 45 | :msg #(str/starts-with? % "Encountered an exception while running")}]))) 46 | 47 | (deftest partition-between 48 | (are [pred input expected] (= expected 49 | (sut/partition-between pred input)) 50 | identity 51 | () 52 | () 53 | 54 | > 55 | '(1 2 3) 56 | '((1 2 3)) 57 | 58 | < 59 | '(1 2 3) 60 | '((1) (2) (3)) 61 | 62 | #(< 2 (- %2 %1)) 63 | '(1 2 3 8 9 10) 64 | '((1 2 3) (8 9 10)))) 65 | 66 | (deftest unlimited-pr-str 67 | (let [input [1 2 [3 4 [5 6]]] 68 | expected "[1 2 [3 4 [5 6]]]"] 69 | ;; override user settings 70 | (binding [*print-length* nil 71 | *print-level* nil] 72 | (is (= expected (sut/unlimited-pr-str input))) 73 | 74 | (testing "not affected by *print-length*" 75 | (binding [*print-length* 1] 76 | (is (= expected (sut/unlimited-pr-str input))))) 77 | 78 | (testing "not affected by *print-level*" 79 | (binding [*print-level* 1] 80 | (is (= expected (sut/unlimited-pr-str input)))))))) 81 | -------------------------------------------------------------------------------- /test/unit/formatting_stack/util/diff.clj: -------------------------------------------------------------------------------- 1 | (ns unit.formatting-stack.util.diff 2 | (:require 3 | [clojure.test :refer [are deftest is testing use-fixtures]] 4 | [formatting-stack.test-helpers :refer [with-mocked-diff-path]] 5 | [formatting-stack.util.diff :as sut])) 6 | 7 | (use-fixtures :once with-mocked-diff-path) 8 | 9 | (deftest diff->line-numbers 10 | (are [description filename expected] (testing description 11 | (is (= expected 12 | (sut/diff->line-numbers (slurp filename)))) 13 | true) 14 | 15 | "Additions do not report a line number" 16 | "test-resources/diffs/1.diff" 17 | [] 18 | 19 | "Multiple sections are reported individually" 20 | "test-resources/diffs/2.diff" 21 | [{:start 14 22 | :end 14 23 | :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 24 | {:start 144 25 | :end 144 26 | :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 27 | {:start 146 28 | :end 146 29 | :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"} 30 | {:start 154 31 | :end 154 32 | :filename "src/formatting_stack/formatters/trivial_ns_duplicates.clj"}] 33 | 34 | "consecutive removed lines are grouped" 35 | "test-resources/diffs/3.diff" 36 | [{:start 12 37 | :end 12 38 | :filename "src/formatting_stack/formatters/no_extra_blank_lines.clj"} 39 | {:start 30 40 | :end 33 41 | :filename "src/formatting_stack/formatters/no_extra_blank_lines.clj"} 42 | {:start 41 43 | :end 41 44 | :filename "src/formatting_stack/protocols/spec.clj"}] 45 | 46 | "Moving code reports on the removed line" 47 | "test-resources/diffs/4.diff" 48 | [{:start 5 49 | :end 5 50 | :filename "test/unit/formatting_stack/strategies.clj"}] 51 | 52 | "Renaming does not report anything" 53 | "test-resources/diffs/5.diff" 54 | [] 55 | 56 | "Deleting reports entire file" 57 | "test-resources/diffs/6.diff" 58 | [{:end 82 59 | :start 1 60 | :filename "/dev/null"}] 61 | 62 | "Adding a new file does not report anything" 63 | "test-resources/diffs/7.diff" 64 | [] 65 | 66 | "Can handle `\\ No newline at end of file`" 67 | "test-resources/diffs/8.diff" 68 | []) 69 | 70 | (testing "exceptions are passed through" 71 | (is (thrown? IllegalStateException 72 | (sut/diff->line-numbers (slurp "test-resources/diffs/incorrect.diff")))))) 73 | 74 | (deftest unified-diff 75 | (are [description filename revised-filename expected] (testing description 76 | (is (= (slurp expected) 77 | (sut/unified-diff filename 78 | (slurp filename) 79 | (slurp revised-filename)))) 80 | true) 81 | 82 | "Adding to an empty file" 83 | "test-resources/diffs/files/1.txt" 84 | "test-resources/diffs/files/1_revised.txt" 85 | "test-resources/diffs/files/1.diff" 86 | 87 | "Removing all contents" 88 | "test-resources/diffs/files/1_revised.txt" 89 | "test-resources/diffs/files/1.txt" 90 | "test-resources/diffs/files/1_reversed.diff" 91 | 92 | "Removing newlines" 93 | "test-resources/diffs/files/2.txt" 94 | "test-resources/diffs/files/2_revised.txt" 95 | "test-resources/diffs/files/2.diff" 96 | 97 | "Adding newlines" 98 | "test-resources/diffs/files/2_revised.txt" 99 | "test-resources/diffs/files/2.txt" 100 | "test-resources/diffs/files/2_reversed.diff")) 101 | -------------------------------------------------------------------------------- /worker/formatting_stack/background.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.background 2 | "This file live in a distinct source-paths so it's not affected by the Reloaded workflow, 3 | while developing formatting-stack itself.") 4 | 5 | (defonce workload (atom nil)) 6 | 7 | (defonce ^Thread 8 | ^{:doc "The runner for 'background' execution. Can be stopped via the thread interruption mechanism."} 9 | runner 10 | (let [f (fn [] 11 | (while (not (-> (Thread/currentThread) .isInterrupted)) 12 | (if-let [job @workload] 13 | (when (compare-and-set! workload job nil) 14 | (try 15 | (job) 16 | (catch Exception e 17 | (-> e .printStackTrace)) 18 | (catch AssertionError e 19 | (-> e .printStackTrace)))) 20 | (Thread/sleep 50))))] 21 | (-> f 22 | Thread. 23 | (doto (.setName (str `runner))) 24 | ;; Important - daemonize this thread, otherwise under certain conditions it can prevent the JVM from exiting. 25 | ;; (We exercise this implicitly via `lein eastwood` in CI) 26 | (doto (.setDaemon true)) 27 | (doto .start)))) 28 | -------------------------------------------------------------------------------- /worker/formatting_stack/kondo_classpath_cache.clj: -------------------------------------------------------------------------------- 1 | (ns formatting-stack.kondo-classpath-cache 2 | "Holds a cache for the entire classpath, to make better use of the project-wide analysis capabilities. 3 | 4 | Only needs to be created once, as the classpath never changes." 5 | (:require 6 | [clj-kondo.core :as kondo] 7 | [clojure.string :as string]) 8 | (:import 9 | (java.io File))) 10 | 11 | (def runner-mapping 12 | {"future" `future 13 | "delay" `delay}) 14 | 15 | (def ^:private runner-choice 16 | (-> runner-mapping 17 | (get (System/getProperty "formatting-stack.kondo-classpath-cache.runner", "future")) 18 | (doto assert))) 19 | 20 | (defmacro runner 21 | {:style/indent 0} 22 | [& body] 23 | {:pre [(seq body)]} 24 | (apply list runner-choice body)) 25 | 26 | ;; Use kondo's official default config dir, so that we don't bloat consumers' project layouts: 27 | (def ^String cache-parent-dir ".clj-kondo") 28 | 29 | ;; Don't use .clj-kondo directly since it can be accessed concurrently (e.g. f-s + a second Kondo from VS Code): 30 | (def ^String cache-subdir "formatting-stack-cache") 31 | 32 | (def cache-dir (str cache-parent-dir File/separator cache-subdir)) 33 | 34 | (def classpath-cache 35 | (runner 36 | (let [files (-> (System/getProperty "java.class.path") 37 | (string/split #"\:"))] 38 | (-> (File. cache-parent-dir cache-subdir) .mkdirs) 39 | (kondo/run! {:lint files 40 | :cache-dir cache-dir})))) 41 | --------------------------------------------------------------------------------