Skip to content

Latest commit

 

History

History
149 lines (99 loc) · 4.48 KB

File metadata and controls

149 lines (99 loc) · 4.48 KB

Part 3: :analyze-call hooks

In this stage of the workshop, still have src/hooks_workshop/macro_usage.clj open in your editor!

Now we arrive at the most powerful part of clj-kondo hooks, :analyze-call.

The analyze-call hook offers a more precise way of handling custom macros with accurate preservation of location information.

Clj-kondo uses rewrite-clj nodes to represent code. Analyze-call hooks receive the node exactly in the form how clj-kondo uses it.

Exercise 3.0

Read the part in the documentation to familiarize yourself with the node representation here.

Exercise 3.1

Open a REPL with clj-kondo as a dependency. Run the following code:

(require '[clj-kondo.hooks-api :as api])
(def node (api/parse-string "(denfk foo [:x :y])"))

The node object is a rewrite-clj node which gets printed as <list: (denfk foo [:x :y])>.

To access the :children of the node, try:

(:children node)

Each node also has a :tag key:

(:tag node)

What is the tag of the node and each of its children?

API

The clj-kondo.hooks-api namespace exposes functions to deal with nodes: predicates to check what kind of node you are dealing with and functions to create new nodes. View the API documentation here.

Exercise 3.2

Check out the API here and play around with it in the REPL.

Exercise 3.3

Transforming a node

This is a preparation for exercise 3.4.

How can we transform a vector of keys to a vector of symbols?

(def argvec-node (api/parse-string "[:x :y]"))

The keyword nodes are the :children of the argvec-node.

Let's start with just a keyword node:

(def k-node (api/parse-string ":x"))

Inspect the location metadata:

(meta k-node) {:row 1, :col 1, :end-row 1, :end-col 3}

To create a symbol node, we can use:

(api/token-node 'x)

But given the k-node that represents :x, how do we turn it into a symbol node while also preserving the location information?

We can use with-meta and re-use the metadata of the old node. Also we can use api/sexpr to get the keyword as s-expression of the old node.

Now you should have sufficient information to turn argvec-node into a new node that contains symbols instead of keywords, that still have the location information of the keyword nodes.

Hook function

Now let's write our first analyze-hook function. The expected signature of such a function is map -> map. The function is called by clj-kondo with a map that contains:

  • :node: the input (rewrite-clj) node
  • :config: the clj-kondo configuration
  • :lang: either :clj or :cljs, indicating the language of this node
  • :cljc: either true or false, indicating if the node is from a .cljc file
  • :filename: the name of the file currently being analyzed
  • :ns: the name of the namespace currently being analyzed

A hook function can return a map that contains:

  • :node: the transformed node. Clj-kondo will use this node instead of the input node to continue linting. When absent, the input node is used.

Exercise 3.4

In macros.clj we have kdefn3 which is the same as kdefn from earlier in this workshop. Instead of a :macroexpand hook, add an :analyze-call hook configuration in .clj-kondo/config.edn. And put a function in .clj-kondo/hooks_workshop/macros.clj_kondo like this:

(defn kdefn3 [{:keys [node]}]
  (prn node))

Then lint src/hooks_workshop/macro_usage.clj from the command line:

$ clj-kondo --lint src/hooks_workshop/macro_usage.clj

or with the clojure CLI:

clojure -M -m clj-kondo.main --lint src/hooks_workshop/macro_usage.clj

Because there is a call (kdefn3 my-fn3 [:foo :bar] (+ foo baz)), you should see the node being printed:

<list: (kdefn3 my-fn3 [:foo :bar] (+ foo baz))>

Return a new node by returning a map {:node ...}. Use the API to construct the new node. This new node will be analyzed by clj-kondo as the replacement for the old node and as such should improve linting.

You can use the REPL for interactively developing the hook function.