diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cbba9..4c6d52a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Added +- Export clj-kondo hooks and config via `resources/clj-kondo.exports/` so library + consumers can activate them by running + `clj-kondo --copy-configs --dependencies --lint classpath` + ## Fixed ## Changed diff --git a/README.md b/README.md index e0efc9f..148c6dc 100644 --- a/README.md +++ b/README.md @@ -945,11 +945,26 @@ meta-merge). e.g. `{:colors ^:replace {...}}`) ## clj-kondo Support -This library includes a built-in clj-kondo configuration to improve linting for Ornament components. +Ornament ships clj-kondo hooks and configuration that validate `defstyled` forms — catching bad tag names, invalid syntax, and providing correct `def`-like analysis. -To use the configuration, simply copy the .clj-kondo/ directory from this repository into the root of your project. +The hooks are bundled in the JAR under `clj-kondo.exports/`. To activate them in your project: -After copying the directory, clj-kondo will automatically use this configuration to provide more accurate linting and reduce false positives in your Ornament projects. +1. Ensure a `.clj-kondo` directory exists at your project root: + ``` + mkdir -p .clj-kondo + ``` + +2. Copy the configs from your dependencies: + ``` + clj-kondo --lint "$(clojure -Spath)" --copy-configs --skip-lint + ``` + +3. Warm the linting cache: + ``` + clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel + ``` + +After that, clj-kondo will automatically validate `defstyled` forms in your project. Consider checking the copied configs into version control. ## Babashka compatibility diff --git a/resources/clj-kondo.exports/com.lambdaisland/ornament/config.edn b/resources/clj-kondo.exports/com.lambdaisland/ornament/config.edn new file mode 100644 index 0000000..548c6e6 --- /dev/null +++ b/resources/clj-kondo.exports/com.lambdaisland/ornament/config.edn @@ -0,0 +1,5 @@ +{:lint-as {lambdaisland.ornament/defprop clojure.core/def + lambdaisland.ornament/defrules clojure.core/def} + :hooks {:analyze-call {lambdaisland.ornament/defstyled hooks.ornament/defstyled}} + :linters {:lambdaisland.ornament/invalid-syntax + {:level :warning}}} diff --git a/resources/clj-kondo.exports/com.lambdaisland/ornament/hooks/ornament.clj b/resources/clj-kondo.exports/com.lambdaisland/ornament/hooks/ornament.clj new file mode 100644 index 0000000..d43953d --- /dev/null +++ b/resources/clj-kondo.exports/com.lambdaisland/ornament/hooks/ornament.clj @@ -0,0 +1,71 @@ +(ns hooks.ornament + (:require [clj-kondo.hooks-api :as api])) + +(defonce styled-registry (atom #{})) + +(defn- symbol-node? [node] + (and (api/token-node? node) + (symbol? (api/sexpr node)))) + +(defn- styled-component? [node] + (and (symbol-node? node) + (contains? @styled-registry (api/sexpr node)))) + +(defn defstyled [{:keys [node]}] + (let [[class-name html-tag & more] (rest (:children node)) + _ (when-not (and (symbol-node? class-name) + (simple-symbol? (api/sexpr class-name))) + (api/reg-finding! {:row (:row (meta class-name)) + :col (:col (meta class-name)) + :message "Style name must be a simple symbol" + :type :lambdaisland.ornament/invalid-syntax})) + _ (when-not (or (api/keyword-node? html-tag) + (styled-component? html-tag)) + (api/reg-finding! {:row (:row (meta html-tag)) + :col (:col (meta html-tag)) + :message "Tag must be a keyword or an ornament-styled-component" + :type :lambdaisland.ornament/invalid-syntax})) + fn-tag (first (drop-while (fn [x] + (or (api/string-node? x) + (api/keyword-node? x) + (api/map-node? x) + (api/vector-node? x) + (api/token-node? x))) + more)) + _ (when (and fn-tag + (not (api/list-node? fn-tag))) + (api/reg-finding! {:row (:row (meta fn-tag)) + :col (:col (meta fn-tag)) + :message "Function part (if present) must be a list" + :type :lambdaisland.ornament/invalid-syntax})) + symbol-tag? (symbol-node? html-tag)] + ;; Register this component in the styled-registry + (swap! styled-registry conj (api/sexpr class-name)) + (if (api/list-node? fn-tag) + (let [[binding-vec & body] (:children fn-tag) + fn-node (api/list-node + (list* + (api/token-node 'fn) + binding-vec + body)) + new-def-node (api/list-node + (if symbol-tag? + (list (api/token-node 'def) + class-name + (api/list-node + (list (api/token-node 'do) + html-tag + fn-node))) + (list (api/token-node 'def) + class-name + fn-node)))] + {:node new-def-node}) + (let [def-class-form (api/list-node + (if symbol-tag? + (list (api/token-node 'def) + class-name + html-tag) + (list (api/token-node 'def) + class-name + (api/token-node 'nil))))] + {:node def-class-form}))))