|
| 1 | +(ns etaoin.api |
| 2 | + (:require [clj-kondo.hooks-api :as api] |
| 3 | + [etaoin.hooks-util :as h])) |
| 4 | + |
| 5 | +(defn- nil-node? [n] |
| 6 | + (and (api/token-node? n) (nil? (api/sexpr n)))) |
| 7 | + |
| 8 | +(defn- with-bound-arg [node arg-offset] |
| 9 | + (let [macro-args (rest (:children node)) |
| 10 | + leading-args (take arg-offset macro-args) |
| 11 | + interesting-args (drop arg-offset macro-args) |
| 12 | + [opts binding-sym & body] (if (h/symbol-node? (second interesting-args)) |
| 13 | + interesting-args |
| 14 | + (cons nil interesting-args))] |
| 15 | + ;; if the user has specified nil or {} for options we can suggest that is not necessary |
| 16 | + (when (and opts |
| 17 | + (or (and (api/map-node? opts) (not (seq (:children opts)))) |
| 18 | + (nil-node? opts))) |
| 19 | + (api/reg-finding! (assoc (meta opts) |
| 20 | + :message "Empty or nil driver options can be omitted" |
| 21 | + :type :etaoin/empty-opts))) |
| 22 | + |
| 23 | + (cond |
| 24 | + (not (h/symbol-node? binding-sym)) |
| 25 | + ;; it makes more sense here to report on the incoming node position instead of what we expect to be the binding-sym |
| 26 | + (api/reg-finding! (assoc (meta node) |
| 27 | + :message "Expected binding symbol for driver" |
| 28 | + :type :etaoin/binding-sym)) |
| 29 | + |
| 30 | + ;; we don't want to explicitly expect a map because the map might come from |
| 31 | + ;; an evalution, but we can do some checks |
| 32 | + (and opts ;; we'll assume a list-node is a function call (eval) |
| 33 | + (not (nil-node? opts)) ;; nil is actually old-v1 syntax acceptable |
| 34 | + (not (api/list-node? opts)) ;; some fn call |
| 35 | + (not (h/symbol-node? opts)) ;; from a binding maybe |
| 36 | + ;; there are other eval node types... @(something) for example... maybe we'll add them in if folks ask |
| 37 | + (not (api/map-node? opts))) |
| 38 | + ;; we can report directly on the opts node, because at this point we know we expect |
| 39 | + ;; this arg position to be an opts map |
| 40 | + (api/reg-finding! (assoc (meta opts) |
| 41 | + :message "When specified, opts should be a map" |
| 42 | + :type :etaoin/opts-map-type)) |
| 43 | + |
| 44 | + ;; one last nicety, if the first form in body is a map, the user has accidentally swapped |
| 45 | + ;; binding and opt args |
| 46 | + (api/map-node? (first body)) |
| 47 | + (api/reg-finding! (assoc (meta (first body)) |
| 48 | + :message "When specified, opts must appear before binding symbol" |
| 49 | + :type :etaoin/opts-map-pos)) |
| 50 | + |
| 51 | + :else |
| 52 | + {:node (api/list-node |
| 53 | + (list* |
| 54 | + (api/token-node 'let) |
| 55 | + ;; simulate the effect, macro is creating a new thing (driver for example) |
| 56 | + ;; via binding it. I don't think the bound value matters for the linting process |
| 57 | + (api/vector-node [binding-sym (api/map-node [])]) |
| 58 | + ;; reference the other args so that they are not linted as unused |
| 59 | + (api/vector-node leading-args) |
| 60 | + opts ;; might be a binding, so ref it too |
| 61 | + body))}))) |
| 62 | + |
| 63 | +(defn- with-x-down |
| 64 | + "This is somewhat of a maybe an odd duck. |
| 65 | + I think it is assumed to be used within a threading macro. |
| 66 | + And itself employs a threadfirst macro. |
| 67 | + So each body form need to have an action (dummy or not) threaded into it." |
| 68 | + [node] |
| 69 | + (let [macro-args (rest (:children node)) |
| 70 | + [input x & body] macro-args |
| 71 | + dummy-action (api/map-node [])] |
| 72 | + {:node (api/list-node |
| 73 | + (apply list* |
| 74 | + (api/token-node 'do) |
| 75 | + ;; reference x and input just in case they contain something lint-relevant |
| 76 | + x input |
| 77 | + ;; dump the body, threading a dummy action in as first arg |
| 78 | + (map (fn [body-form] |
| 79 | + (cond |
| 80 | + ;; not certain this is absolutely what we want, but maybe close enough |
| 81 | + (h/symbol-node? body-form) (api/list-node (list* body-form dummy-action)) |
| 82 | + (api/list-node? body-form) (let [children (:children body-form)] |
| 83 | + (assoc body-form :children (apply list* |
| 84 | + (first children) |
| 85 | + dummy-action |
| 86 | + (rest children)))) |
| 87 | + :else |
| 88 | + (api/reg-finding! (assoc (meta body-form) |
| 89 | + :message "expected to be threaded through an action" |
| 90 | + :type :etaoin/with-x-action)))) |
| 91 | + body)))})) |
| 92 | + |
| 93 | +(defn with-browser |
| 94 | + "Covers etaoin.api/with-chrome and all its variants |
| 95 | + [opt? bind & body]" |
| 96 | + [{:keys [node]}] |
| 97 | + (with-bound-arg node 0)) |
| 98 | + |
| 99 | +(defn with-driver |
| 100 | + "Very similar to with-browser but bound arg is 1 deeper |
| 101 | + [type opt? bind & body]" |
| 102 | + [{:keys [node]}] |
| 103 | + (with-bound-arg node 1)) |
| 104 | + |
| 105 | +(defn with-key-down |
| 106 | + "[input key & body]" |
| 107 | + [{:keys [node]}] |
| 108 | + (with-x-down node)) |
| 109 | + |
| 110 | +(defn with-pointer-btn-down |
| 111 | + "[input button & body]" |
| 112 | + [{:keys [node]}] |
| 113 | + (with-x-down node)) |
0 commit comments