Fork me on GitHub

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

;; where the fn named hoc-fn above could be:
(defn hoc-fn 
  [component baz] 

;; 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?

👀 1

> 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

😮 1
thanks3 1
Noah Bogart13:04:26

Is function-component-name supposed to be the same as some-inner-component?

Noah Bogart13:04:49

Maybe share a small real code example


@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
    {: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)}
Helix's impl of defnc -
(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
                {: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
                 (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, 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!

Noah Bogart18:04:43

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

😅 1

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

Noah Bogart18:04:07

make sure you're running clj-kondo --debug in addition to whatever else you do

Noah Bogart18:04:23

clj-kondo --debug --lint path/to/file


At 1: macroexpand is similar to analyze-call but works directly on s-expressions, that's the only difference. for debugging, inspect the return value from the macro using prn and see if looks correct


At 3: probably not