Fork me on GitHub
#clj-kondo
<
2024-02-17
>
pez13:02:25

I want to lint #C06JZ4X334N defcomponent, which is very similar to defn, with the addition that there can be component lifecycle declarations of the form :some-keyword (fn …) between the name of the component and the argument list. Example from the README:

(defcomponent heading
  :on-render (fn [dom-node val old-val])
  [data]
  [:h2 {:style {:background "#000"}} (:text data)])
I have no idea where to start. 😃

borkdude13:02:21

try :lint-as {dumdom/whatever clj-kondo.lint-as/def-catch-all}

👀 1
borkdude13:02:39

it would seem more natural to me if dumdom had written the macro like this:

(defcomponent heading
  {:on-render (fn ...)}
  [data]
  )
using metadata notation

borkdude13:02:47

which is already supported by defn

pez13:02:43

I’m currently using a macro that someone wrote that “fixes” this similar to what you suggest:

(ns dumbom.dumdom.macros
  (:require [dumdom.core :as dumdom]))

(defn- extract-docstr
  [[docstr? & forms]]
  (if (string? docstr?)
    [docstr? forms]
    ["" (cons docstr? forms)]))

(defn- extract-opts
  ([[argvec ?opts & forms]]
   (assert (vector? argvec))
   (if (map? ?opts)
     [?opts (cons argvec forms)]
     [{} (list* argvec ?opts forms)])))

(defmacro defcomponent
  "A workaround to make default linting resolve `defcomponent` without complaints.

  Instead of this:
  (defcomponent Widget
      \"A Widget\"
      :on-mount #(...)
      :on-render #(...)
      [value constant-value]
      (some-child-components))

  We can write this:
  (defcomponent Widget
      [value constant-value]
      {:name \"A Widget\"
       :on-mount #(...)
       :on-render #(...)}
      (some-child-components))"
  [name & forms]
  (let [[docstr forms] (extract-docstr forms)
        [options forms] (extract-opts forms)
        [argvec & body] forms
        options (merge {:name (str (:name (:ns &env)) "/" name)} options)]
    `(def ~name ~docstr (dumdom.core/component (fn ~argvec ~@body) ~options))))
And using that instead. I’d like to not have to do this.

borkdude13:02:43

Well, it's not up to me how people write their macros but the closer you stay to existing syntax, the easier it is to get working with clj-kondo. Maybe you could bring that advice to the library author. Above I gave one solution (def-catch-all). Another is to write a hook.

pez13:02:13

The catch-all gets rid of the warnings, but it doesn’t “recognize” local bindings, afaict.

borkdude13:02:43

that's right, it just tries to do the best it can: see that the name defines a var and ignore all the rest in the body

pez13:02:48

The library author is writing a replacement of Dumdom that doesn’t have components. 😃

borkdude13:02:50

if you want to improve on that, write a hook

pez13:02:09

I’ll read up on hooks.

borkdude13:02:48

I gave a workshop on ClojureD 2022 on hooks ;)

pez13:02:47

I visited @U07FP7QJ0’ Minecraft workshop instead! 😃

minecraft 1
cjohansen13:02:23

I now regret my macro design decisions 😅

cjohansen13:02:58

Maybe dumdom could support the alternative variant as well, I think that should be doable

catjam 1
borkdude13:02:31

if you support it so that people can write: {:lint-as {dumdom/defcomponent clojure.core/defn}} that would be nice

pez13:02:33

Yeah, that’s what the macro I posted above achieves.

pez13:02:39

If you are attending the Conj this year, I will attend any workshop you host, @U04V15CAJ.

😄 2
pez13:02:23

Though having my kids glued to when I slayed the Ender Dragon with Clojure was totally awesome, so no regrets here!

borkdude13:02:20

yeah, minecraft is certainly less boring than clj-kondo hooks :)

borkdude13:02:50

although I've never tried it myself

pez13:02:55

I’ll return to judging that once I have this hook written. 😃

😂 4
pez16:02:01

So, for this form:

(defcomponent heading
  :on-render (fn [dom-node val old-val])
  [data]
  [:h2 {:style {:background "#000"}} (:text data)])
If I print it at the start of my hook:
My hook returns:
{:node }
I was thinking that this would make kondo lint the defn form. But maybe I am wrong about that? Interestingly, if I add something between the dumdom options and the argument vector:
(defcomponent heading
  :on-render (fn [dom-node val old-val])
  'foo
  [data]
  [:h2 {:style {:background "#000"}} (:text data)])
Then data is treated as a local binding, but the args in the lambda are not.

pez16:02:04

The hook:

(ns hooks.dumdom
  (:require [clj-kondo.hooks-api :as api]))

(defn- extract-docstr
  [[docstr? & forms :as maybe-forms]]
  (if (api/string-node? docstr?)
    [(api/sexpr docstr?) forms]
    ["no docs" maybe-forms]))

(defn- extract-opts
  ([forms]
   (extract-opts forms {}))
  ([[k v & forms :as maybe-forms] opts]
   (if (api/keyword-node? k)
     (extract-opts forms (assoc opts (api/sexpr k) (api/sexpr v)))
     [opts maybe-forms])))

(defn defcomponent [{:keys [node]}]
  (println "defcomponent\n node:" node)
  (let [[name & forms] (rest (:children node))
        [docstr forms] (extract-docstr forms)
        [options forms] (extract-opts forms)
        [argvec & body] forms
        new-node (api/list-node
                  (list*
                   (api/token-node 'defn)
                   name
                   docstr
                   options
                   (api/sexpr argvec)
                   body))]
    {:node new-node}))

borkdude17:02:24

Try running clj-kondo with the --debug flag on the command line, it might output some helpful info

👀 1
borkdude17:02:42

also print the new-node

pez17:02:22

clj-kondo --lint - --debug << 'EOF'
(ns dumbom.home
  (:require [dumdom.core :refer [defcomponent]]))

(defn heading "no docs" {:on-render (fn [dom-node val old-val])} [data] [:h2 {:style {:background "#000 "}} (:text data)])

(defcomponent heading
  :on-render (fn [dom-node val old-val] foo)
  [data]
  [:h2 {:style {:background "#000"}} (:text data)])
EOF
[clj-kondo] Auto-loading config path: rewrite-clj/rewrite-clj
[clj-kondo] Auto-loading config path: http-kit/http-kit
[clj-kondo] Auto-loading config path: babashka/fs
[clj-kondo] Auto-loading config path: funcool/promesa
[clj-kondo] Linting file: <stdin>
defcomponent
 node: <list: (defcomponent heading :on-render (fn [dom-node val old-val] foo) [data] [:h2 {:style {:background "#000"}} (:text data)])>
<stdin>:4:42: warning: unused binding dom-node
<stdin>:4:51: warning: unused binding val
<stdin>:4:55: warning: unused binding old-val
<stdin>:6:1: error: Not a node: no docs
<stdin>:7:19: warning: unused binding dom-node
<stdin>:7:28: warning: unused binding val
<stdin>:7:32: warning: unused binding old-val
<stdin>:7:41: error: Unresolved symbol: foo
<stdin>:8:4: error: Unresolved symbol: data
linting took 61ms, errors: 3, warnings: 6
Not a node: no docs looks like a clue… new-node is not being printed for some reason. It’s printed when I run the hook in the repl…

borkdude17:02:12

this is probably because Not a node: no docs is something you should first solve. And in the REPL you don't run with --debug

borkdude17:02:09

In extract-docstr you return either a vector of nodes, but the second thing is a vector of a string and a node

🙏 1
pez17:02:49

Yeah, should solve that. The missing print is just a (println ...)

pez08:02:06

Got it working. Please let me know if you see anything stupid being done in the hook code. https://gist.github.com/PEZ/1ac56f00b8e9e622dca2e78c66d4922a

pez08:02:33

@U9MKYDN4Q, Dumdom could provide this config automatically. Want a PR?

cjohansen08:02:45

Very much! 🙏

metal 1
pez11:02:08

@U04V15CAJ, the auto-config gets imported to the library project (when using Calva+clojure-lsp). Do you think I should add the auto-config to .gitignore? It risks getting a bit confusing with the duplicates…

borkdude11:02:56

duplicates?

pez11:02:04

I end up with the same files in resources and in .clj-kondo/cjohansen/dumdom.

borkdude11:02:00

you can unduplicate this by removing the files in .clj-kondo/cjohansen and then adding this to .clj-kondo/config.edn:

:config-paths ["../resources/...."]

👀 1
borkdude11:02:59

To activate the exported configuration in your local project, you can add the following to .clj-kondo/config.edn:

{:config-paths ["../resources/clj-kondo.exports/<your-org>/<your-libname>"]}

pez11:02:25

Yeah, I read that before, but thought it was if I needed the config for the library (which doesn’t seem necessary). But it makes sense that it will dedup things too. 😃

cjohansen11:02:49

Is this on the consumer side?

borkdude11:02:12

this is only for linting inside your library, not for consumers

👍 1
cjohansen11:02:27

I guess my real question was: will this cause changes in the PR @U0ETXRFEWor is it ready to merge?

borkdude11:02:50

it will change the PR

cjohansen11:02:09

As suspected. Thanks 😊

pez11:02:05

Not getting it to stop downloading the auto-config, though… I copied the path using VS Code to make sure I get it right. :thinking_face:

borkdude11:02:06

yes, it will always import the config. that doesn't make sense for this lib so in this lib you can add .clj-kondo/cjohansen/dumdom to .gitignore I guess

👍 1
pez11:02:56

Fantastic documentation for the hooks, @U04V15CAJ.

❤️ 1
cjohansen11:02:38

Not to mention the exquisite service ❤️

❤️ 1
pez11:02:07

Not to mention that!

pez11:02:13

A thing I missed and kept not realizing was that api/map-node does not work on maps. The docs clearly say it expects a sequable, so this is entirely on me. But wondering if it would hurt to make it work on maps too?

borkdude11:02:11

this stuff is directly linked to how rewrite-clj works, not sure if it makes sense to change that API

👍 1