Skip to content

Commit

Permalink
Merge pull request #86 from pitch-io/roman/expose-linter
Browse files Browse the repository at this point in the history
expose component linting api
  • Loading branch information
roman01la authored Jan 28, 2023
2 parents 79eccb3 + 977a7d3 commit c92e593
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 28 deletions.
2 changes: 1 addition & 1 deletion core/deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
enlive/enlive {:mvn/version "1.1.6"}
hiccup/hiccup {:mvn/version "1.0.5"}
rum/rum {:mvn/version "0.11.2"}}}
:test {:extra-paths ["test"]
:test {:extra-paths ["test" "dev"]
:extra-deps {org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/clojurescript {:mvn/version "1.10.879"}
thheller/shadow-cljs {:mvn/version "2.15.5"}
Expand Down
2 changes: 1 addition & 1 deletion core/dev/uix/examples.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,4 @@
(defonce -init
(let [root (uix.dom/create-root (js/document.getElementById "root"))]
(uix.dom/render-root ($ app) root)
nil))
nil))
35 changes: 35 additions & 0 deletions core/dev/uix/linters.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
(ns uix.linters
(:require
[cljs.analyzer :as ana]
[clojure.pprint :as pprint]
[uix.linter :as linter]))

;; per element linting
(defmethod linter/lint-element :element/no-inline-styles [_ form env]
(let [[_ tag props & children] form]
(when (and (keyword? tag)
(map? props)
(contains? props :style))
(linter/add-error! form :element/no-inline-styles (linter/form->loc (:style props))))))

(defmethod ana/error-message :element/no-inline-styles [_ _]
"Inline styles are not allowed, put them into a CSS file instead")

;; Hooks linting
(defmethod linter/lint-hook-with-deps :hooks/too-many-lines [_ form env]
(when (> (count (str form)) 180)
(linter/add-error! form :hooks/too-many-lines (linter/form->loc form))))

(defmethod ana/error-message :hooks/too-many-lines [_ {:keys [source]}]
(str "React hook is too large to be declared directly in component's body, consider extracting it into a custom hook:\n\n"
(with-out-str (pprint/pprint `(~'defn ~'use-my-hook []
~source)))))

;; Components linting
(defmethod linter/lint-component :component/kebab-case-name [_ form env]
(let [[_ sym] form]
(when-not (re-matches #"[a-z-]+" (str sym))
(linter/add-error! form :component/kebab-case-name (linter/form->loc sym)))))

(defmethod ana/error-message :component/kebab-case-name [_ _]
"Component name should be in kebab case")
6 changes: 4 additions & 2 deletions core/src/uix/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
A component should have a single argument of props."
[sym & fdecl]
(let [[fname args fdecl] (parse-sig `defui sym fdecl)]
(uix.linter/lint! sym fdecl &env)
(uix.linter/lint! sym fdecl &form &env)
(if (uix.lib/cljs-env? &env)
(let [var-sym (-> (str (-> &env :ns :name) "/" sym) symbol (with-meta {:tag 'js}))
body (uix.dev/with-fast-refresh var-sym fdecl)]
Expand All @@ -108,7 +108,7 @@
[(first fdecl) (rest fdecl)]
[(gensym "uix-fn") fdecl])
[fname args body] (parse-sig `fn sym fdecl)]
(uix.linter/lint! sym body &env)
(uix.linter/lint! sym body &form &env)
(if (uix.lib/cljs-env? &env)
(let [var-sym (with-meta sym {:tag 'js})]
`(let [~var-sym ~(if (empty? args)
Expand All @@ -131,8 +131,10 @@
DOM element: ($ :button#id.class {:on-click handle-click} \"click me\")
React component: ($ title-bar {:title \"Title\"})"
([tag]
(uix.linter/lint-element* &form &env)
(uix.compiler.aot/compile-element [tag] {:env &env}))
([tag props & children]
(uix.linter/lint-element* &form &env)
(uix.compiler.aot/compile-element (into [tag props] children) {:env &env})))

;; === Hooks ===
Expand Down
68 changes: 45 additions & 23 deletions core/src/uix/linter.clj
Original file line number Diff line number Diff line change
Expand Up @@ -42,30 +42,33 @@
(contains? effect-hooks (name (first form))))

(defn form->loc [form]
(select-keys form [:line :column]))
(select-keys (meta form) [:line :column]))

(defn find-env-for-form [type form]
(case type
(::hook-in-branch ::hook-in-loop
::deps-coll-literal ::literal-value-in-deps
::unsafe-set-state ::missing-key)
(form->loc (meta form))
(form->loc form)

::inline-function
(form->loc (meta (second form)))
(form->loc (second form))

::deps-array-literal
(form->loc (meta (.-val form)))
(form->loc (.-val form))

nil))

(defn add-error! [form type]
(swap! *component-context* update :errors conj {:source form
:source-context *source-context*
:type type
:env (find-env-for-form type form)}))
(defn add-error!
([form type]
(add-error! form type (find-env-for-form type form)))
([form type env]
(swap! *component-context* update :errors conj {:source form
:source-context *source-context*
:type type
:env env})))

(defn- uix-element? [form]
(defn uix-element? [form]
(and (list? form) (= '$ (first form))))

(defn- missing-key? [[_ _ attrs :as form]]
Expand Down Expand Up @@ -269,19 +272,31 @@
(:name v) ", use `use-subscribe` hook instead.\n"
"Read https://github.com/pitch-io/uix/blob/master/docs/interop-with-reagent.md#syncing-with-ratoms-and-re-frame for more context"))

(defn lint! [sym form env]
(defmulti lint-component (fn [type form env]))
(defmulti lint-element (fn [type form env]))
(defmulti lint-hook-with-deps (fn [type form env]))

(defn- run-linters! [mf & args]
(doseq [[key f] (.getMethodTable ^clojure.lang.MultiFn mf)]
(apply f key args)))

(defn- report-errors!
([env]
(report-errors! env nil))
([env m]
(let [{:keys [errors]} @*component-context*
{:keys [column line]} env]
(run! #(ana/warning (:type %)
(or (:env %) env)
(merge {:column column :line line} m %))
errors))))

(defn lint! [sym body form env]
(binding [*component-context* (atom {:errors []})]
(lint-body! form)
(lint-re-frame! form env)
(let [{:keys [errors]} @*component-context*
{:keys [column line]} env]
(doseq [err errors]
(ana/warning (:type err)
(or (:env err) env)
(into {:name (str (-> env :ns :name) "/" sym)
:column column
:line line}
err))))))
(lint-body! body)
(lint-re-frame! body env)
(run-linters! lint-component form env)
(report-errors! env {:name (str (-> env :ns :name) "/" sym)})))

;; === Exhaustive Deps ===

Expand Down Expand Up @@ -490,5 +505,12 @@

(defn lint-exhaustive-deps! [env form f deps]
(doseq [[error-type opts] (lint-exhaustive-deps env form f deps)]
(ana/warning error-type (or (:env opts) env) opts)))
(ana/warning error-type (or (:env opts) env) opts))
(binding [*component-context* (atom {:errors []})]
(run-linters! lint-hook-with-deps form env)
(report-errors! env)))

(defn lint-element* [form env]
(binding [*component-context* (atom {:errors []})]
(uix.linter/run-linters! uix.linter/lint-element form env)
(report-errors! env)))
11 changes: 10 additions & 1 deletion core/test/uix/linter_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:require [clojure.test :refer [deftest is testing]]
[shadow.cljs.devtools.cli]
[uix.linter]
[uix.linters]
[clojure.string :as str]))

;; === Rules of Hooks ===
Expand Down Expand Up @@ -265,4 +266,12 @@
(is (str/includes? out-str "UIx element is missing :key attribute, which is required"))
(is (str/includes? out-str "($ :div.test-missing-key {} x)"))
(is (str/includes? out-str "($ :div.test-missing-key ($ x))"))
(is (str/includes? out-str "($ :div.test-missing-key-nested ($ x))")))))
(is (str/includes? out-str "($ :div.test-missing-key-nested ($ x))")))

(testing "plugin linters should work"
(is (str/includes? out-str (str :component/kebab-case-name)))
(is (str/includes? out-str (str :hooks/too-many-lines)))
(is (str/includes? out-str (str :element/no-inline-styles)))
(is (str/includes? out-str "Component name should be in kebab case"))
(is (str/includes? out-str "React hook is too large to be declared directly in component's body"))
(is (str/includes? out-str "Inline styles are not allowed, put them into a CSS file instead")))))
11 changes: 11 additions & 0 deletions core/test/uix/linter_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,14 @@
(do
($ :div.test-missing-key ($ x))
($ :div.test-missing-key-nested ($ x))))))

(defui Button [{:keys [on-click children]}]
(uix.core/use-effect
(fn []
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
[])
($ :button {:on-click on-click
:style {:color "red"}}
children))

0 comments on commit c92e593

Please sign in to comment.