clojurestatic-analysisclj-kondo

Can I execute a clj-kondo hook before any macro expansions occur?


I've got a clj-kondo hook which tells me when I'm threading a value through only one form:

;; .clj-kondo/config.edn
{
...
  :hooks {:analyze-call {clojure.core/-> peter.analyzers/superfluous-arrow
                         clojure.core/->> peter.analyzers/superfluous-arrow}}}
}

;; ./.clj-kondo/peter/analyzers.clj

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

(defn superfluous-arrow
  [{:keys [node]}]
  (let [[arrow _data & forms] (:children node)]
    (when (= 1 (count forms))
      (api/reg-finding!
       (assoc (meta node)
              :message (format "%s: no need to thread a single form - %s (meta %s)" arrow node (meta node))
              :type :peter.analyzers/superfluous-arrow)))))

When I run clj-kondo I get some false positives. e.g. if I run the above on this file:

;; bogus.clj

(ns bogus)

;; from 
(defn do-stuff
  [coll {:keys [map-fn max-num-things batch-size]}]
  (cond->> coll
    map-fn         (map map-fn)
    max-num-things (take max-num-things)
    batch-size     (partition batch-size))) 

I get the following warnings:

bogus.clj::: warn: clojure.core/->>: no need to thread a single form - (clojure.core/->> G__4 (map map-fn))
bogus.clj::: warn: clojure.core/->>: no need to thread a single form - (clojure.core/->> G__4 (take max-num-things))
bogus.clj::: warn: clojure.core/->>: no need to thread a single form - (clojure.core/->> G__4 (partition batch-size))
linting took 37ms, errors: 0, warnings: 0

It looks like this is because the cond->> macro is getting expanded then the hook is running on the expanded code.

Is there a way to ensure that my hooks run on the verbatim nodes in the source files, rather than after macro expansion, to avoid this problem?


Solution

  • Since version v2022.12.08, clj-kondo has a generated-node? function in the hooks API that checks if a node was generated by a macro.

    So if you want your hook to only execute on verbatim code from your source files, guard your hook code with (when-not (generated-node? node) ...). So in the case of the hook in the question, you could do this:

    (ns peter.analyzers
      (:require
       [clj-kondo.hooks-api :as api]))
    
    
    (defn superfluous-arrow
      [{:keys [node]}]
      (when-not (api/generated-node? node)
        (let [[arrow _data & forms] (:children node)]
          (when (= 1 (count forms))
            (api/reg-finding!
             (assoc (meta node)
                    :message (format "%s: no need to thread a single form - %s" arrow node)
                    :type :peter.analyzers/superfluous-arrow))))))