(ns hooks.defclass
"clj-kondo hook for shadow.cljs.modern/defclass.
defclass is (defclass Name (extends Base)? (field f v)*
(constructor [this args] body*) Protocol (method [this args] body*)* …).
clj-kondo has no idea Name is being defined, so every reference to it —
including the internals-style re-exports from other namespaces — reports
Unresolved symbol, and so does every constructor/method parameter.
Rewrite it into a form clj-kondo does understand: a def for the name, and
an fn per constructor/method so the parameters bind their bodies. Bare
protocol symbols (Object, IDeref) are dropped — they are not call sites.
Three details are load-bearing, each measured against this tree:
- the def needs a VALUE. A bare (def x) is an Uninitialized var warning,
31 of them across chat, board and rooms-kit.
- (extends Base) must survive as a reference to Base, or the require that
brought Base in looks unused — node:events/EventEmitter in rooms-kit's
chat.cljs and board.cljs, ws/WebSocket in node/env.cljs.
- `super` may be bound ONLY for a class that extends. Bind it always and
every non-extending class reports an unused binding; bind it never and
every extending constructor reports Unresolved symbol: super."
(:require [clj-kondo.hooks-api :as api]))
(defn- form-named?
[n nm]
(and (= :list (api/tag n))
(= nm (str (first (:children n))))))
(defn- method-like?
"A (sym [params] body*) list — the shape both constructor and methods take."
[n]
(let [[head params] (:children n)]
(and (= :list (api/tag n))
(some? head) (api/token-node? head)
(some? params) (= :vector (api/tag params)))))
(defn defclass [{:keys [node]}]
(let [[_ name-node & body] (:children node)
extends-node (first (filter #(form-named? % "extends") body))
;; keep the extended class as a real reference so its require counts
extends (when extends-node
(api/list-node (list* (api/token-node 'do)
(rest (:children extends-node)))))
params-of (fn [params]
(if extends-node
(api/vector-node (cons (api/token-node 'super)
(:children params)))
params))
fns (for [n body :when (and (not (form-named? n "extends"))
(not (form-named? n "field"))
(method-like? n))]
(let [[_ params & fbody] (:children n)]
(api/list-node (list* (api/token-node 'fn) (params-of params) fbody))))
;; (field f v): v is real code and may call things
fields (for [n body :when (form-named? n "field")]
(api/list-node (list* (api/token-node 'do) (drop 2 (:children n)))))]
{:node (api/list-node
(list* (api/token-node 'do)
(api/list-node [(api/token-node 'def) name-node (api/token-node 'nil)])
(concat (when extends [extends]) fields fns)))}))