This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-04-24
Channels
- # announcements (3)
- # babashka (23)
- # beginners (35)
- # cider (3)
- # clara (3)
- # clj-kondo (14)
- # cljdoc (1)
- # cljs-dev (1)
- # clojure (82)
- # clojure-austin (9)
- # clojure-europe (5)
- # clojurescript (23)
- # conjure (62)
- # cursive (73)
- # defnpodcast (1)
- # emacs (3)
- # ethereum (1)
- # gratitude (1)
- # hyperfiddle (12)
- # introduce-yourself (1)
- # leiningen (2)
- # lsp (44)
- # malli (7)
- # polylith (2)
- # portal (17)
- # re-frame (5)
- # reitit (3)
- # sci (8)
- # shadow-cljs (5)
- # tools-build (11)
I want to try to write a custom hook for the helix/defnc
macro. Specifically, I want it to behave almost completely like a regular defn
, except for a single thing:
• In a specific place in the macro, I want to teach clj-kondo
to treat a particular form as a thread-first chain.
• It's not a macro or fn call that I can ":lint-as ->", it's more like a map like {:to-be-threaded [ (foo "2nd arg") (bar "2nd arg" "3rd arg") ]}
Deets:
• There's a special key (`:wrap`) in a kinda-metadata map basically, whose value is a vector of function calls, each of which, when defnc
is expanded, have their first arg passed in via an emitted thread-first chain.
• Kondo does not know statically that these function calls will eventually make sense when they're threaded, so it yells invalid-arity
at me.
• While we could ignore kondo in these cases (somehow? I don't even know how to do that either), it would be sick to preserve arity checking, just accounting for/ignoring just the first arg.
;; usage example
(defnc function-component-name
[props another-param]
{:wrap [(hoc-fn "baz")]} ;; this guy here, a special "arg" after the "actual" arg vector
<...body...>)
;; where the fn named hoc-fn above could be:
(defn hoc-fn
[component baz]
<...body...>)
;; macroexpanded output of "this guy here" bit somewhere within `defnc`'s expansion
(-> some-inner-component
(hoc-fn "baz"))
If I wrote a custom hook, would I be able to just mirror default vanilla defn
linting, except at that specific map key?
If so, how do I ensure I've got the right node at which the :wrap
key is?
• I don't want to accidentally apply this special linting to the actual arg vector, in case there's a key in an map there with the same name
• etc
Thanks!Maybe it helps to look at a couple of examples? https://github.com/clj-kondo/clj-kondo/discussions/1528
> If so, how do I ensure I've got the right node at which the :wrap
key is?
You basically have to do that parsing yourself: first find the child that is after the vector, if it's a map then do your special thing, etc

Is function-component-name
supposed to be the same as some-inner-component
?
@UEENNMX0T technically no? some-inner-component
is more or less an inline (fn [args] body)
, which after being threaded through the higher order functions (in my example just hoc-fn)
, is returned by defnc
, and bound to the name function-component-name
I was thinking of posting real ish code, but didn't want to noise it up for those unfamiliar with helix. I'll post in a sec.
Thanks for helping out guys!!!!
Real-life-ish code examples
(defn hoc-fn [component-fn] ($ component-fn))
In the following snippet, there are 2 lines with comments saying "example"
Those are both cases of calls to functions that expect at least one argument, but when used here, we leave that out, as the macro defnc
will take care of that for us.
;; usage of defnc
(defnc textbox
[{:keys [class value title] :as props} ref]
{:wrap [(react/forwardRef) ;; example
(hoc-fn)]} ;; example
(d/input
{:ref ref
:type "text"
:auto-complete "chrome-off"
:class (cx textbox-style class)
:on-change (when on-change #(on-change (.. % -target -value) %))
& (dissoc props :class :on-change)}
children))
Helix's impl of defnc
- https://github.com/lilactown/helix/blob/63a79430b2600201fdf16dc3d687b8bf16b7bb2a/src/helix/core.clj#L251-L260
(defmacro defnc
"docstrings here"
;; ... a bunch of stuff mostly unrelated...
;; then, near the end of the implementation of `defnc`,
;; (removed nesting whitespace here)
(def ~(vary-meta
component-var-name
merge
{:helix/component? true})
~@(when-not (nil? docstring)
(list docstring))
(-> ~(fnc* component-fn-name props-bindings
(cons (when flag-fast-refresh?
`(if ^boolean goog/DEBUG
(when ~sig-sym
(~sig-sym))))
body))
(cond->
(true? ^boolean goog/DEBUG)
(doto (goog.object/set "displayName" ~fully-qualified-name)))
~@(-> opts :wrap))) ;; the threading place
;; ... rest of `defnc` implementation ...
hey @U04V15CAJ, I'm trying to use :macroexpand
hooks for the above now.
IE.,
:hooks {: macroexpand { helix.core/defnc hooks.helix.core/defnc }}
.
I've got (more or less) a copy-pasta of the source code of the helix.core/defnc
macro in /.clj-kondo/hooks/helix/core.clj
1. From reading the hooks.md, I'm under the impression that what this type of hook does, is tell kondo/sci
what to expand the macro to when it sees in the wild.
a. Therefore, if I literally just copy the original (defmacro defnc ...)
verbatim from helix
's source code,
b. Except for inlining imports or removing bits of code that don't affect my desired thing (the :wrap
threading) etc,
c. Should it "just work"?
i. (Minus any typos or lil mistakes on my end of course)
d. Or have I completely misunderstood what :macroexpand
hooks do?
2. How do I debug or ascertain that kondo is even "picking up" or "registering" that I've set a hook for defnc
?
a. I placed a couple of prn
statements and whatnot, and then ran clj-kondo --lint
on a few files
b. But I see neither the prn statements nor has anything changed from before I defined this hook
3. Does it matter that I'm in cljs?
Thanks again for your time and help!
okay, i read through the code and i don't understand it at all lol. i was hoping i could help out but this macro is... way too complex for me
At 2: if you don't see the prn output then it's probably not configured correctly. As with debugging anything, dumb down the example to something more trivial and try again
clj-kondo --debug --lint path/to/file