Skip to content

Instantly share code, notes, and snippets.

@riotrah
Created June 20, 2022 18:13
Show Gist options
  • Save riotrah/be60b7ccefe99436b28c9bb11bbe08d9 to your computer and use it in GitHub Desktop.
Save riotrah/be60b7ccefe99436b28c9bb11bbe08d9 to your computer and use it in GitHub Desktop.
clj-kondo hook for clj-commons.secretary.core/defroute
(ns clj-kondo.clj-commons.secretary
(:require
[clj-kondo.hooks-api :as api]
[clojure.string :as str]))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn defroute
"Macro analysis for `secretary.core/defroute`.
Does some extra work to make `defroute` more user-friendly, for those whose IDEs make use of clj-kondo.
0. For unnamed routes, it does nothing, and returns the original `defroute` form.
1. For named routes (`(defroute some-route \"/some/route\" ...)`):
It turns:
```
(defroute
some-route \"/some/:route/:param1\"
[route]
...)
```
into:
```
(defn some-route
\"Route handler for url: `/some/:route/:param1`.
Route props: `:route`, `:param1`\"
[route param1]
...)
```
- It makes said named routes have better go-to-definition etc by transforming them into defn calls.
- It makes the implicitly destructed path params (`(defroute some-route \")) into explicit destructed params,
instead of an inner `let` binding as done in the actual macroexpansion for IDE arg hover purposes.
- It adds a docstring to the \"generated\" `defn`, which contains the route's path string and the route's props.
- It makes clj-kondo ensure that routes with no path-params will lint-error if any args are passed to them.
- Conversely, routes with path-params will lint-error if the arg (a map, though it doesn't check if it is a map)
are _not_ passed to them."
[{:keys [node]}]
(let [[_defroute-sym & body] (-> node :children)
[named-route-fn-sym body] (if (api/token-node? (first body))
[(first body) (next body)]
[nil body])
is-named-route? (some? named-route-fn-sym)
[route-string body] (if (api/string-node? (first body))
[(first body) (next body)]
[nil body])
[let-binding-vector body] (if (api/vector-node? (first body))
[(-> body first) (next body)]
[nil body])
has-url-params? (boolean (seq (:children let-binding-vector)))
expanded (if is-named-route?
(api/list-node
(list*
;; Transform the `defroute` sym into a `defn` one.
(api/token-node 'defn)
;; The original route name.
named-route-fn-sym
;; The following automatically generates (in-IDE hover) docstring like so:
;;
;; "Route handler for url: `/databases/:database-id/draft/table_versions/:table-name/:version-id`.
;; <newline>
;; Route props: `:database-id` `:table-name` `:version-id`."
(api/string-node
(list* "Route handler for url: `",
(api/sexpr route-string),
"`.",
(when has-url-params?
(list
"\nRoute props: "
(str/join ", "
(map #(str "`:" % "`")
(:children let-binding-vector)))
"."))))
;; The path param arg vector.
;; If there are none, it's an empty vector, raising an error if any args are
;; passed when called. Otherwise, it's a vector of the destructed single map arg..
(api/vector-node
(if has-url-params?
;; The destructed map arg.
[(api/map-node [(api/keyword-node :keys)
let-binding-vector])]
[]))
body))
node)]
(comment (when expanded
(-> node api/sexpr prn)
(-> expanded prn)
(prn)))
{:node expanded}))
(comment
;; Example 1: usage without named route - the hook does nothing
#_(sec/defroute
"route/:string1"
[string1]
(prn string1))
;; Example 2: usage with named route - this is the _output_ of the hook, after it's been tranformed into a `defn` etc.
(defn named-route-fn-sym
"route/:string1" ; maybe use as optional docstring
[url-params]
(let
[{:keys [string1]} url-params]
(prn string1))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment