0

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?

Peter
  • 3,619
  • 3
  • 31
  • 37

2 Answers2

1

There is already a linter for this:

https://github.com/clj-kondo/clj-kondo/blob/master/doc/linters.md#redundant-call

You just have to enable it with :level :warning.

The reason you're getting non-locations is because metadata is missing on generated nodes. This should fixed in a newer version of clj-kondo. What version were you using?

If you return nil from your hook, clj-kondo continues running with the un-expanded call.

Michiel Borkent
  • 34,228
  • 15
  • 86
  • 149
  • Great! I'll use that linter for now, thanks. I am using clj-kondo v2022.11.02, FWIW. And my concern wasn't with the missing _location_ so much as the `(clojure.core/->> G__4 (map map-fn))` etc. being syntax that wasn't actually in my original file -- it's part of the `cond->>` macro-expansion. I didn't expect my hooks to be analyzing macro-expanded code. – Peter Dec 01 '22 at 23:23
  • Actually, I looked at the `:redundant-call` linter, and it's not quite what I'm after: it'll catch `(-> foo)` which is good, but not `(-> foo :bar)`, which is what I want to discourage (in favour of `(:bar foo)`, slightly shorter) – Peter Dec 02 '22 at 00:03
  • Right, I don't think this is supported yet, but feel free to reach out on Github for further discussion – Michiel Borkent Dec 05 '22 at 19:48
0

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))))))
Peter
  • 3,619
  • 3
  • 31
  • 37