diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index fce8f976d..487692376 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -1,6 +1,6 @@ {:linters {:shadowed-var {:level :warning} :single-key-in {:level :warning} - :missing-docstring {:level :warning}} + :missing-docstring {:level :ignore}} :lint-as {;; Herb herb.core/defglobal clojure.core/def diff --git a/.dir-locals.el b/.dir-locals.el index 89b3942e1..07077edd7 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,5 +1,9 @@ ((nil . ((cider-clojure-cli-aliases . ":dev:behave/app") - (cider-default-cljs-repl . figwheel-main)))) + (cider-default-cljs-repl . figwheel-main) + (eval . (progn + (load (expand-file-name "scripts/behave.el" + (locate-dominating-file buffer-file-name ".dir-locals.el"))) + (add-hook 'after-save-hook #'behave-format-on-save nil t)))))) ;; VMS Configuration ;; TODO: Fix to avoid having two separate aliases for projects diff --git a/.github/workflows/clj-kondo.yml b/.github/workflows/clj-kondo.yml new file mode 100644 index 000000000..85fc46f1a --- /dev/null +++ b/.github/workflows/clj-kondo.yml @@ -0,0 +1,29 @@ +name: clj-kondo linter + +on: + pull_request: + branches: + - main + +jobs: + clj-lint: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Get changed Clojure files + id: changed-files + run: | + FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(clj|cljs|cljc)$' | tr '\n' ' ') + echo "files=$FILES" >> $GITHUB_OUTPUT + echo "has_files=$([ -n "$FILES" ] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT + + - uses: DeLaGuardo/clojure-lint-action@master + if: steps.changed-files.outputs.has_files == 'true' + with: + clj-kondo-args: --lint ${{ steps.changed-files.outputs.files }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/align_requires.clj b/scripts/align_requires.clj new file mode 100644 index 000000000..4a71841de --- /dev/null +++ b/scripts/align_requires.clj @@ -0,0 +1,74 @@ +#!/usr/bin/env bb +(ns align-requires + (:require [babashka.fs :as fs] + [clojure.string :as str] + [rewrite-clj.zip :as z] + [rewrite-clj.node :as n])) + +(defn- keyword-entry? + "True if a require vector has :as or :refer as its second element. + Uses z/right (skips whitespace) to find the keyword directly." + [zloc] + (when-let [second-child (-> zloc z/down z/right)] + (try (#{:as :refer} (z/sexpr second-child)) + (catch Exception _ nil)))) + +(defn- require-vectors + "Returns all zipper locations of [ns-sym :as/:refer ...] vectors + within the :require clause of the given file zipper." + [root-zloc] + (->> root-zloc + (iterate z/next) + (take-while (complement z/end?)) + (filter #(and (z/vector? %) (keyword-entry? %))))) + +(defn- ns-sym-len + [entry-zloc] + (count (str (z/sexpr (z/down entry-zloc))))) + +(defn- set-whitespace-after-ns-sym + [entry-zloc spaces] + (let [ws-node (n/whitespace-node (str/join (repeat spaces " ")))] + (-> entry-zloc + z/down ; at ns-sym + z/right* ; at raw whitespace node between ns-sym and keyword + (z/replace ws-node) + z/up))) + +(defn- align-file! + "Aligns all `:imports`/`:requires` dependencies." + [path] + (let [content (slurp path) + zloc (z/of-string content {:track-position? true}) + entries (require-vectors zloc)] + (when (seq entries) + (let [max-len (apply max (map ns-sym-len entries)) + ;; Re-parse to get fresh zipper for mutations + aligned (loop [acc (z/of-string content)] + (let [es (require-vectors acc) + ;; Find first entry whose spacing doesn't match target + target (fn [e] (- (inc max-len) (ns-sym-len e))) + bad-e (first (filter (fn [e] + (let [ws (-> e z/down z/right*)] + (not= (str/join (repeat (target e) " ")) + (z/string ws)))) + es))] + (if bad-e + (recur (set-whitespace-after-ns-sym bad-e (target bad-e))) + acc))) + new-content (z/root-string aligned)] + (when (not= content new-content) + (println "Aligned:" path) + (spit path new-content)))))) + +(let [arg (or (first *command-line-args*) ".") + paths (if (fs/regular-file? arg) + [arg] + (->> (fs/glob arg "**/*.{clj,cljs,cljc}") + (map str) + (remove #(re-find #"/(target|node_modules|resources/public/cljs)/" %))))] + (doseq [path paths] + (align-file! path))) + +;; usage +;; bb align_requires.clj diff --git a/scripts/behave.el b/scripts/behave.el new file mode 100644 index 000000000..a9e374a22 --- /dev/null +++ b/scripts/behave.el @@ -0,0 +1,32 @@ +;;; behave.el --- Project-local Emacs helpers for behave-polylith + +(defun behave-format-on-save () + "Run cljfmt fix + align_requires on save, then silently revert buffer." + (let* ((project-root (locate-dominating-file buffer-file-name ".dir-locals.el")) + (align-script (expand-file-name "scripts/align_requires.clj" project-root)) + (file (buffer-file-name)) + (buf (current-buffer)) + (cmd (format "cd %s && bb %s %s && clojure -M:format fix %s" + (shell-quote-argument project-root) + (shell-quote-argument align-script) + (shell-quote-argument file) + (shell-quote-argument file)))) + (make-process + :name "clj-format-on-save" + :buffer nil + :command (list "bash" "-c" cmd) + :sentinel (lambda (_proc event) + (when (and (string-prefix-p "finished" event) + (buffer-live-p buf)) + (with-current-buffer buf + (unless (buffer-modified-p) + (revert-buffer t t t)))))))) + +(defun behave-jack-in-cms () + "Start the VMS/CMS REPL via `cider-jack-in-clj&cljs` with the :dev:behave/cms alias." + (interactive) + (let ((cider-clojure-cli-aliases ":dev:behave/cms")) + (cider-jack-in-clj&cljs nil))) + +(provide 'behave) +;;; behave.el ends here