diff --git a/.clj-kondo/hooks/ornament.clj b/.clj-kondo/hooks/ornament.clj index 5cf30b0..d43953d 100644 --- a/.clj-kondo/hooks/ornament.clj +++ b/.clj-kondo/hooks/ornament.clj @@ -1,35 +1,46 @@ (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 (api/token-node? class-name) + _ (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 symbol" + :message "Style name must be a simple symbol" :type :lambdaisland.ornament/invalid-syntax})) - ; _ (prn :class-name class-name) - _ (when-not (api/keyword-node? html-tag) + _ (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})) - ; _ (prn :html-tag html-tag) - ; _ (prn :more more) 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/vector-node? x) + (api/token-node? x))) more)) - ; _ (prn :fn-tag fn-tag) _ (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}))] + :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 @@ -38,15 +49,23 @@ binding-vec body)) new-def-node (api/list-node - (list (api/token-node 'def) - class-name - fn-node))] - (prn :new-def-node (api/sexpr new-def-node)) - {:node new-def-node}) - ;; nil node - (let [def-class-form (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 - (api/token-node 'nil)))] - (prn :def-class-form (api/sexpr def-class-form)) + 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})))) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5d070f5..be0f215 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,7 @@ jobs: uses: DeLaGuardo/setup-clojure@master with: cli: '1.10.3.943' + clj-kondo: 'latest' - name: 🗝 maven cache uses: actions/cache@v4 diff --git a/test/lambdaisland/ornament/clj_kondo_test.clj b/test/lambdaisland/ornament/clj_kondo_test.clj new file mode 100644 index 0000000..310c25c --- /dev/null +++ b/test/lambdaisland/ornament/clj_kondo_test.clj @@ -0,0 +1,99 @@ +(ns lambdaisland.ornament.clj-kondo-test + (:require [clojure.test :refer [deftest testing is]] + [clojure.java.shell :as shell] + [clojure.edn :as edn])) + +(defn- lint-string + "Run clj-kondo --lint on a code string, return findings as EDN." + [code] + (let [{:keys [out]} (shell/sh "clj-kondo" + "--lint" "-" + "--config-dir" ".clj-kondo" + "--config" "{:output {:format :edn}}" + :in code)] + (:findings (edn/read-string out)))) + +(defn- ornament-warnings + "Return only the ornament/invalid-syntax findings." + [findings] + (filter #(= :lambdaisland.ornament/invalid-syntax (:type %)) findings)) + +(defn- warning-messages + "Return the set of warning message strings from findings." + [findings] + (into #{} (map :message) findings)) + +;; Valid patterns — should produce no ornament warnings + +(deftest keyword-tag-test + (testing "keyword tag produces no warnings" + (let [warnings (-> "(require '[lambdaisland.ornament :as o]) + (o/defstyled foo :div)" + lint-string + ornament-warnings)] + (is (empty? warnings))))) + +(deftest styled-component-as-tag-test + (testing "styled component used as tag produces no warnings" + (let [warnings (-> "(require '[lambdaisland.ornament :as o]) + (o/defstyled base :span {:color \"blue\"}) + (o/defstyled inherited base)" + lint-string + ornament-warnings)] + (is (empty? warnings))))) + +(deftest symbol-in-rules-test + (testing "symbol references in rules position produce no warnings" + (let [warnings (-> "(require '[lambdaisland.ornament :as o]) + (o/defstyled bold :span :font-bold) + (o/defstyled heading :h1 bold :text-3xl)" + lint-string + ornament-warnings)] + (is (empty? warnings))))) + +(deftest fn-body-test + (testing "defstyled with fn body produces no warnings" + (let [warnings (-> "(require '[lambdaisland.ornament :as o]) + (o/defstyled foo :div ([props] [:div props]))" + lint-string + ornament-warnings)] + (is (empty? warnings))))) + +;; Invalid patterns — should produce ornament warnings + +(deftest non-symbol-name-test + (testing "non-symbol name produces warning" + (let [msgs (-> "(require '[lambdaisland.ornament :as o]) + (o/defstyled 123 :div)" + lint-string + ornament-warnings + warning-messages)] + (is (contains? msgs "Style name must be a simple symbol"))))) + +(deftest non-keyword-non-styled-tag-test + (testing "numeric literal as tag produces warning" + (let [msgs (-> "(require '[lambdaisland.ornament :as o]) + (o/defstyled foo 123)" + lint-string + ornament-warnings + warning-messages)] + (is (contains? msgs "Tag must be a keyword or an ornament-styled-component"))))) + +(deftest regular-def-as-tag-test + (testing "regular def used as tag produces warning" + (let [msgs (-> "(require '[lambdaisland.ornament :as o]) + (def not-styled 42) + (o/defstyled foo not-styled)" + lint-string + ornament-warnings + warning-messages)] + (is (contains? msgs "Tag must be a keyword or an ornament-styled-component"))))) + +(deftest invalid-fn-part-test + (testing "non-list in fn position produces warning" + (let [msgs (-> "(require '[lambdaisland.ornament :as o]) + (o/defstyled foo :div #{:a :b})" + lint-string + ornament-warnings + warning-messages)] + (is (contains? msgs "Function part (if present) must be a list")))))