diff --git a/deps.edn b/deps.edn index 1071e7d..4133a6c 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,7 @@ {:paths ["src/main" - "src/test"] + "src/test" + "resources"] :deps {com.thheller/shadow-css {:mvn/version "0.2.0"}} diff --git a/resources/clj-kondo.exports/shadow/grove/clj_kondo/shadow/grove.clj b/resources/clj-kondo.exports/shadow/grove/clj_kondo/shadow/grove.clj new file mode 100644 index 0000000..e0fb2a8 --- /dev/null +++ b/resources/clj-kondo.exports/shadow/grove/clj_kondo/shadow/grove.clj @@ -0,0 +1,137 @@ +(ns clj-kondo.shadow.grove + (:require [clj-kondo.hooks-api :as api] + [clojure.string :as str])) + +(def valid-hook-names + "Return if the provided `name` is a valid hook name" + #{'render 'bind 'event 'hook '<<}) + +(defn- -hook-node->name + "Return the provided name of the `node`, or `nil` + if it is not a list node." + [node] + (when (api/list-node? node) + (api/sexpr (first (:children node))))) + +(defmulti rewrite-hooks! + "Multimethod to rewrite a hooks body for linting in clj-kondo. Is called with a sequence of + hooks to be rewritten. + + It dispatches of of the hook name of the first hook in the sequence. Most implementations will + recursively call `rewrite-hooks!` on the rest of their list." + (fn [hooks] + (if-not (seq hooks) + :done + (if-some [hook-name (-> hooks first -hook-node->name valid-hook-names)] + hook-name + :invalid)))) + +(defmethod rewrite-hooks! :done + [_] + nil) + +(defmethod rewrite-hooks! :invalid + [[hook & hooks]] + (api/reg-finding! + (assoc + (meta + (if (api/list-node? hook) + (first (:children hook)) + hook)) + :level :error + :message + (str "Invalid hook: " + (or (-hook-node->name hook) (api/sexpr hook)) + ", should be one of: " (str/join ", " valid-hook-names)) + :type :shadow.grove/invalid-hook)) + (rewrite-hooks! hooks)) + +(defmethod rewrite-hooks! 'bind + [[hook & hooks]] + (list + (let [[_ bindings expr] (:children hook)] + (api/list-node + (list* + (api/token-node 'let) + (api/vector-node [bindings expr]) + (rewrite-hooks! hooks)))))) + +(defmethod rewrite-hooks! 'hook + [[hook & hooks]] + (cons + (api/list-node + (list* + (api/token-node 'do) + (-> hook :children rest))) + (rewrite-hooks! hooks))) + +(defmethod rewrite-hooks! 'render + [[hook & hooks]] + (concat + (-> hook :children rest) + (rewrite-hooks! hooks))) + +(defmethod rewrite-hooks! '<< + [[hook & hooks]] + (concat + (-> hook :children rest) + (rewrite-hooks! hooks))) + +(defmethod rewrite-hooks! 'event + [[hook & hooks]] + (let [[_ event-name params & body] (:children hook)] + (when-not (api/keyword-node? event-name) + (api/reg-finding! + (assoc (meta event-name) + :level :error + :message "Event name must be keyword" + :type :shadow.grove/invalid-event))) + (when-not (<= 1 (count (:children params)) 3) + (api/reg-finding! + (assoc (meta params) + :level :error + :message "Must be arity 1, 2, or 3. Definition called with `env`, `ev`, `e`" + :type :shdow.grove/invalid-event-artity))) + (cons + (api/list-node + (list* + (api/token-node 'fn) + params + body)) + (rewrite-hooks! hooks)))) + +(defn validate-hooks! + "Function to validate all hooks inside a `defc` and make sure they create a valid component." + [parent-node hook-nodes] + ;; Right now all this does is check that a component has a `render`, leaving more sophisticated + ;; per-hook analysis to the `rewrite-hook!` methods. + (let [render-seen? (->> hook-nodes + (keep -hook-node->name) + (set) + (#(contains? % 'render)))] + (when-not render-seen? + (api/reg-finding! + (assoc + (meta parent-node) + :level :error + :message "Missing `render` hook" + :type :shadow.grove/invalid-component))))) + +(defn defc + [{:keys [node]}] + (let [[_ name & args] (:children node) + [comp-bindings + hooks] (->> args + (drop-while #(not (api/vector-node? %))) + ((juxt first rest))) + rewritten-hooks (->> hooks + (rewrite-hooks!))] + + (validate-hooks! node hooks) + {:node + (api/list-node + (list* + (api/token-node 'defn) + name + comp-bindings + rewritten-hooks))})) diff --git a/resources/clj-kondo.exports/shadow/grove/config.edn b/resources/clj-kondo.exports/shadow/grove/config.edn new file mode 100644 index 0000000..6bc1ab8 --- /dev/null +++ b/resources/clj-kondo.exports/shadow/grove/config.edn @@ -0,0 +1,3 @@ +{:hooks + {:analyze-call + {shadow.grove/defc clj-kondo.shadow.grove/defc}}}