Compare commits
1 Commits
python-run
...
remove-dea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754dd7725f |
@@ -1 +0,0 @@
|
||||
Fix flakey test $ARGUMENTS
|
||||
@@ -1 +0,0 @@
|
||||
Fix issue $ARGUMENTS
|
||||
@@ -1 +0,0 @@
|
||||
Fetch unresolved comments for this branch's PR, then fix them
|
||||
@@ -1 +0,0 @@
|
||||
Repro issue $ARGUMENTS in a failing test
|
||||
1
.clj-kondo/babashka/fs/config.edn
Normal file
1
.clj-kondo/babashka/fs/config.edn
Normal file
@@ -0,0 +1 @@
|
||||
{:lint-as {babashka.fs/with-temp-dir clojure.core/let}}
|
||||
1
.clj-kondo/babashka/sci/config.edn
Normal file
1
.clj-kondo/babashka/sci/config.edn
Normal file
@@ -0,0 +1 @@
|
||||
{:hooks {:macroexpand {sci.core/copy-ns sci.core/copy-ns}}}
|
||||
9
.clj-kondo/babashka/sci/sci/core.clj
Normal file
9
.clj-kondo/babashka/sci/sci/core.clj
Normal file
@@ -0,0 +1,9 @@
|
||||
(ns sci.core)
|
||||
|
||||
(defmacro copy-ns
|
||||
([ns-sym sci-ns]
|
||||
`(copy-ns ~ns-sym ~sci-ns nil))
|
||||
([ns-sym sci-ns opts]
|
||||
`[(quote ~ns-sym)
|
||||
~sci-ns
|
||||
(quote ~opts)]))
|
||||
145
.clj-kondo/better-cond/better-cond/better_cond/core.clj
Normal file
145
.clj-kondo/better-cond/better-cond/better_cond/core.clj
Normal file
@@ -0,0 +1,145 @@
|
||||
(ns better-cond.core
|
||||
"A clj-kondo hook to allow linting of better-cond `cond` macro.
|
||||
This supports better-cond version 2.0.0+.
|
||||
|
||||
To use in a project, change the namespace to hooks.better-cond,
|
||||
put this code in file .clj-kondo/hooks/better_cond.clj, and include
|
||||
a config.edn entry in the :hooks key like:
|
||||
|
||||
:hooks {:analyze-call {better-cond.core/cond hooks.better-cond/cond}}
|
||||
|
||||
Note that the expansion of :when-let and :when-some forms currently
|
||||
takes a shortcut that *would* lead to incorrect values in some
|
||||
cases of restructuring during actual expansion/evaluation, e.g.,
|
||||
:when-let [[x y] nil] would not abort the cond, though :when-let
|
||||
[[x y] [nil nil]] would. However, for linting purposes, the current
|
||||
expansion approach works just fine. It seems unnecessary to do the
|
||||
full expansion in these cases for linting purposes, though that could
|
||||
be done if needed."
|
||||
(:refer-clojure :exclude [cond])
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(def better-cond-simple-keys
|
||||
"Special constructs in better cond, either as keywords or symbols.
|
||||
This includes those keys that are simply transformed. Note that
|
||||
:when-let and :when-some are *not* included as better-cond allows
|
||||
multiple bindings but clojure does not. These two must be handled
|
||||
separately."
|
||||
#{:let :when :do
|
||||
'let 'when 'do})
|
||||
|
||||
(def better-cond-complex-keys
|
||||
"Special constructs in better cond, either as keywords or symbols.
|
||||
This includes those keys that require a multi-step transformation.
|
||||
For example, :when-let and :when-some get converted to a let
|
||||
wrapping a when, wrapping the continuing cond. Note that clojure
|
||||
does not support multiple bindings in the standard when-let and
|
||||
when-some macros."
|
||||
{:when-let 'identity
|
||||
'when-let 'identity
|
||||
:when-some 'some?
|
||||
'when-some 'some?})
|
||||
|
||||
(defn extract-binding-forms
|
||||
[bindings]
|
||||
(keep-indexed #(when (even? %1) %2) bindings))
|
||||
|
||||
(defn process-pairs
|
||||
"Transforms a `cond` with the clauses given as a collection of explicit pairs.
|
||||
Handles all the special better-cond constructs as keywords or symbols.
|
||||
Returns a rewrite-clj list-node representing the transformed code."
|
||||
[node-pairs]
|
||||
(loop [[[lhs rhs :as pair] & pairs] node-pairs
|
||||
new-body [(api/token-node 'clojure.core/cond)]] ; Avoid reprocessing the cond with ns here
|
||||
(if pair
|
||||
(let [lhs-sexpr (api/sexpr lhs)]
|
||||
(clojure.core/cond
|
||||
(= 1 (count pair)) ;; better-cond allows single clause for default
|
||||
, (api/list-node (conj new-body (api/keyword-node :else) lhs))
|
||||
(better-cond-simple-keys lhs-sexpr) ;; Handle special better-cond constructs
|
||||
, (api/list-node
|
||||
(conj new-body
|
||||
(api/keyword-node :else)
|
||||
(api/list-node [(api/token-node (symbol #_"clojure.core" (name lhs-sexpr)))
|
||||
rhs
|
||||
(process-pairs pairs)])))
|
||||
(better-cond-complex-keys lhs-sexpr) ;; Multi stage constructs
|
||||
, (api/list-node
|
||||
(conj new-body
|
||||
(api/keyword-node :else)
|
||||
(api/list-node [(api/token-node 'let)
|
||||
rhs
|
||||
(api/list-node [(api/token-node 'when)
|
||||
(api/list-node ;; ATTN: shortcut here; fine for linting
|
||||
[(api/token-node 'every?)
|
||||
(api/token-node (better-cond-complex-keys lhs-sexpr))
|
||||
(api/vector-node (->> rhs
|
||||
api/sexpr
|
||||
extract-binding-forms
|
||||
(map api/token-node)))])
|
||||
(process-pairs pairs)])])))
|
||||
:else
|
||||
, (recur pairs (conj new-body lhs rhs))))
|
||||
(api/list-node new-body))))
|
||||
|
||||
(defn cond-hook
|
||||
[{:keys [node]}]
|
||||
(let [expr (let [args (rest (:children node))
|
||||
pairs (partition-all 2 args)]
|
||||
(process-pairs pairs))]
|
||||
{:node (with-meta expr
|
||||
(meta node))}))
|
||||
|
||||
(defn process-if-let-pairs [pairs then else]
|
||||
(if (seq pairs)
|
||||
(let [[lhs rhs] (first pairs)]
|
||||
(if (and (api/keyword-node? lhs)
|
||||
(= :let (api/sexpr lhs)))
|
||||
(api/list-node (conj [(api/token-node 'clojure.core/let) rhs
|
||||
(process-if-let-pairs (next pairs) then else)]))
|
||||
(let [test (api/token-node (gensym "test"))]
|
||||
(api/list-node
|
||||
(conj [(api/token-node 'clojure.core/let) (api/vector-node [test rhs])
|
||||
(api/list-node [(api/token-node 'if) test
|
||||
(api/list-node
|
||||
[(api/token-node 'clojure.core/let)
|
||||
(api/vector-node [lhs test])
|
||||
(process-if-let-pairs (next pairs) then else)])
|
||||
else])])))))
|
||||
then))
|
||||
|
||||
(defn if-let-hook
|
||||
[{:keys [node]}]
|
||||
(let [expr (let [[binding-vec then else] (rest (:children node))
|
||||
pairs (partition-all 2 (:children binding-vec))
|
||||
node (process-if-let-pairs pairs then else)]
|
||||
node)]
|
||||
{:node (with-meta expr
|
||||
(meta node))}))
|
||||
|
||||
(defn when-let-hook
|
||||
[{:keys [node]}]
|
||||
(let [expr (let [[binding-vec & body] (rest (:children node))]
|
||||
(api/list-node
|
||||
[(api/token-node 'better-cond.core/if-let)
|
||||
binding-vec
|
||||
(api/list-node (list* (api/token-node 'do)
|
||||
body))]))]
|
||||
{:node (with-meta expr
|
||||
(meta node))}))
|
||||
|
||||
(defn defnc-hook [{:keys [node]}]
|
||||
(let [[defnc-node name-node arg-node & body]
|
||||
(:children node)
|
||||
new-node (api/list-node [(api/token-node (if (str/ends-with? (str defnc-node)
|
||||
"defnc-")
|
||||
'clojure.core/defn-
|
||||
'clojure.core/defn))
|
||||
name-node
|
||||
arg-node
|
||||
(api/list-node
|
||||
(list* (api/token-node 'better-cond.core/cond)
|
||||
body))])]
|
||||
{:node new-node}))
|
||||
9
.clj-kondo/better-cond/better-cond/config.edn
Normal file
9
.clj-kondo/better-cond/better-cond/config.edn
Normal file
@@ -0,0 +1,9 @@
|
||||
{:hooks
|
||||
{:analyze-call
|
||||
{better-cond.core/cond better-cond.core/cond-hook
|
||||
better-cond.core/defnc better-cond.core/defnc-hook
|
||||
better-cond.core/defnc- better-cond.core/defnc-hook
|
||||
better-cond.core/if-let better-cond.core/if-let-hook
|
||||
better-cond.core/if-some better-cond.core/if-let-hook
|
||||
better-cond.core/when-let better-cond.core/when-let-hook
|
||||
better-cond.core/when-some better-cond.core/when-let-hook}}}
|
||||
@@ -0,0 +1,19 @@
|
||||
(ns clj-kondo.com.gfredericks.test.chuck.checking
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]))
|
||||
|
||||
(defn checking
|
||||
[{{:keys [children]} :node}]
|
||||
(let [[_checking desc & opt+bindings+body] children
|
||||
[opts binding-vec & body] (if (api/vector-node? (first opt+bindings+body))
|
||||
(into [(api/map-node {})] opt+bindings+body)
|
||||
opt+bindings+body)]
|
||||
(when-not (even? (count (:children binding-vec)))
|
||||
(throw (ex-info "checking requires an even number of bindings" {})))
|
||||
{:node (api/list-node
|
||||
(list*
|
||||
(api/token-node 'let)
|
||||
(api/vector-node (into [(api/token-node (symbol (gensym "_checking-desc"))) desc]
|
||||
(:children binding-vec)))
|
||||
opts
|
||||
body))}))
|
||||
4
.clj-kondo/com.gfredericks/test.chuck/config.edn
Normal file
4
.clj-kondo/com.gfredericks/test.chuck/config.edn
Normal file
@@ -0,0 +1,4 @@
|
||||
{:hooks
|
||||
{:analyze-call
|
||||
{com.gfredericks.test.chuck.clojure-test/checking
|
||||
clj-kondo.com.gfredericks.test.chuck.checking/checking}}}
|
||||
2
.clj-kondo/com.github.camsaul/humane-are/config.edn
Normal file
2
.clj-kondo/com.github.camsaul/humane-are/config.edn
Normal file
@@ -0,0 +1,2 @@
|
||||
{:lint-as
|
||||
{humane-are.core/are+ clojure.test/are}}
|
||||
39
.clj-kondo/com.github.camsaul/toucan2/config.edn
Normal file
39
.clj-kondo/com.github.camsaul/toucan2/config.edn
Normal file
@@ -0,0 +1,39 @@
|
||||
{:config-paths
|
||||
["macros"]
|
||||
|
||||
:lint-as
|
||||
{toucan2.core/build clojure.core/identity
|
||||
toucan2.core/compile clojure.core/identity
|
||||
toucan2.query/with-built-query clojure.core/let
|
||||
toucan2.tools.compile/build clojure.core/identity
|
||||
toucan2.tools.compile/compile clojure.core/identity}
|
||||
|
||||
:hooks
|
||||
{:analyze-call
|
||||
{toucan2.connection/with-connection hooks.toucan2.connection/with-connection
|
||||
toucan2.connection/with-transaction hooks.toucan2.connection/with-transaction
|
||||
toucan2.core/with-connection hooks.toucan2.connection/with-connection
|
||||
toucan2.core/with-transaction hooks.toucan2.connection/with-transaction
|
||||
toucan2.tools.simple-out-transform/define-out-transform hooks.toucan2.tools.simple-out-transform/define-out-transform
|
||||
toucan2.tools.with-temp/with-temp hooks.toucan2.tools.with-temp/with-temp}
|
||||
|
||||
:macroexpand
|
||||
{toucan.db/with-call-counting macros.toucan2.execute/with-call-count
|
||||
toucan.models/defmodel macros.toucan.models/defmodel
|
||||
toucan2.core/define-after-insert macros.toucan2.tools.after-insert/define-after-insert
|
||||
toucan2.core/define-after-select macros.toucan2.tools.helpers/define-after-select
|
||||
toucan2.core/define-after-update macros.toucan2.tools.after-update/define-after-update
|
||||
toucan2.core/define-before-delete macros.toucan2.tools.helpers/define-before-delete
|
||||
toucan2.core/define-before-insert macros.toucan2.tools.before-insert/define-before-insert
|
||||
toucan2.core/define-before-select macros.toucan2.tools.helpers/define-before-select
|
||||
toucan2.core/define-before-update macros.toucan2.tools.before-update/define-before-update
|
||||
toucan2.execute/with-call-count macros.toucan2.execute/with-call-count
|
||||
toucan2.tools.after-insert/define-after-insert macros.toucan2.tools.after-insert/define-after-insert
|
||||
toucan2.tools.after-select/define-after-select macros.toucan2.tools.helpers/define-after-select
|
||||
toucan2.tools.after-update/define-after-update macros.toucan2.tools.after-update/define-after-update
|
||||
toucan2.tools.before-delete/define-before-delete macros.toucan2.tools.helpers/define-before-delete
|
||||
toucan2.tools.before-insert/define-before-insert macros.toucan2.tools.before-insert/define-before-insert
|
||||
toucan2.tools.before-select/define-before-select macros.toucan2.tools.helpers/define-before-select
|
||||
toucan2.tools.before-update/define-before-update macros.toucan2.tools.before-update/define-before-update
|
||||
toucan2.tools.default-fields/define-default-fields macros.toucan2.tools.default-fields/define-default-fields
|
||||
toucan2.tools.named-query/define-named-query macros.toucan2.tools.named-query/define-named-query}}}
|
||||
@@ -0,0 +1,14 @@
|
||||
(ns hooks.toucan2.connection
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn with-connection [{{[_ bindings & body] :children} :node}]
|
||||
(let [[conn-binding connectable] (:children bindings)]
|
||||
{:node (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node [conn-binding (or connectable
|
||||
(hooks/token-node 'nil))])
|
||||
body))}))
|
||||
|
||||
(defn with-transaction [node]
|
||||
(with-connection node))
|
||||
@@ -0,0 +1,37 @@
|
||||
(ns hooks.toucan2.tools.simple-out-transform
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn- ignore-unused-binding [x]
|
||||
(vary-meta x assoc :clj-kondo/ignore [:unused-binding]))
|
||||
|
||||
(defn define-out-transform
|
||||
[{{[_define-out-transform dispatch-value {[instance-binding] :children, :as bindings} & body] :children, :as node} :node}]
|
||||
{:node (-> (hooks/list-node
|
||||
[(hooks/token-node 'do)
|
||||
dispatch-value
|
||||
(hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'fn)
|
||||
(-> (hooks/vector-node
|
||||
[(ignore-unused-binding (with-meta (hooks/token-node '&query-type) (meta bindings)))
|
||||
(ignore-unused-binding (with-meta (hooks/token-node '&model) (meta bindings)))
|
||||
instance-binding])
|
||||
(with-meta (meta bindings)))
|
||||
body))])
|
||||
(with-meta (meta node)))})
|
||||
|
||||
(comment
|
||||
(defn test-define-out-transform []
|
||||
(as-> '(tools.simple-out-transform/define-out-transform [:toucan.query-type/select.instances ::after-select]
|
||||
[instance]
|
||||
(let [wow 1000]
|
||||
;; don't do after-select if this select is a result of doing something like insert-returning instances
|
||||
(if (isa? &query-type :toucan2.pipeline/select.instances-from-pks)
|
||||
instance
|
||||
(after-select instance)))) <>
|
||||
(hooks/parse-string (pr-str <>))
|
||||
(define-out-transform {:node <>})
|
||||
(:node <>)
|
||||
(hooks/sexpr <>)
|
||||
(binding [*print-meta* false #_true]
|
||||
(clojure.pprint/pprint <>)))))
|
||||
@@ -0,0 +1,22 @@
|
||||
(ns hooks.toucan2.tools.with-temp
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn with-temp [{{[_with-temp {bindings :children} & body] :children} :node}]
|
||||
(let [bindings* (into
|
||||
[]
|
||||
(comp (partition-all 3)
|
||||
(mapcat (fn [[model binding attributes]]
|
||||
(let [binding (or binding (hooks/token-node '_))
|
||||
attributes (or attributes (hooks/token-node 'nil))]
|
||||
[binding (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'do)
|
||||
model
|
||||
attributes))]))))
|
||||
bindings)
|
||||
node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node bindings*)
|
||||
body))]
|
||||
{:node node*}))
|
||||
@@ -0,0 +1,5 @@
|
||||
(ns macros.toucan.models)
|
||||
|
||||
(defmacro defmodel
|
||||
[model _table-name]
|
||||
`(def ~model "Docstring." ~(keyword (name model))))
|
||||
@@ -0,0 +1,4 @@
|
||||
(ns macros.toucan2.common)
|
||||
|
||||
(defn ignore-unused [symb]
|
||||
(vary-meta symb assoc :clj-kondo/ignore [:unused-binding]))
|
||||
@@ -0,0 +1,6 @@
|
||||
(ns macros.toucan2.execute)
|
||||
|
||||
(defmacro with-call-count
|
||||
[[call-count-fn-binding] & body]
|
||||
`(let [~call-count-fn-binding (fn [])]
|
||||
~@body))
|
||||
@@ -0,0 +1,10 @@
|
||||
(ns macros.toucan2.tools.after-insert
|
||||
(:require [macros.toucan2.common :as common]))
|
||||
|
||||
(defmacro define-after-insert
|
||||
[model [instance-binding] & body]
|
||||
`(do
|
||||
~model
|
||||
(fn [~(common/ignore-unused '&model)
|
||||
~instance-binding]
|
||||
~@body)))
|
||||
@@ -0,0 +1,10 @@
|
||||
(ns macros.toucan2.tools.after-update
|
||||
(:require [macros.toucan2.common :as common]))
|
||||
|
||||
(defmacro define-after-update
|
||||
[model [instance-binding] & body]
|
||||
`(do
|
||||
~model
|
||||
(fn [~(common/ignore-unused '&model)
|
||||
~instance-binding]
|
||||
~@body)))
|
||||
@@ -0,0 +1,10 @@
|
||||
(ns macros.toucan2.tools.before-insert
|
||||
(:require [macros.toucan2.common :as common]))
|
||||
|
||||
(defmacro define-before-insert
|
||||
[model [instance-binding] & body]
|
||||
`(do
|
||||
~model
|
||||
(fn [~(common/ignore-unused '&model)
|
||||
~instance-binding]
|
||||
~@body)))
|
||||
@@ -0,0 +1,10 @@
|
||||
(ns macros.toucan2.tools.before-update
|
||||
(:require [macros.toucan2.common :as common]))
|
||||
|
||||
(defmacro define-before-update
|
||||
[model [instance-binding] & body]
|
||||
`(do
|
||||
~model
|
||||
(fn [~(common/ignore-unused '&model)
|
||||
~instance-binding]
|
||||
~@body)))
|
||||
@@ -0,0 +1,6 @@
|
||||
(ns macros.toucan2.tools.default-fields
|
||||
(:require [macros.toucan2.common :as common]))
|
||||
|
||||
(defmacro define-default-fields [model & body]
|
||||
`(let [~(common/ignore-unused '&model) ~model]
|
||||
~@body))
|
||||
@@ -0,0 +1,27 @@
|
||||
(ns macros.toucan2.tools.helpers
|
||||
(:require [macros.toucan2.common :as common]))
|
||||
|
||||
(defmacro define-before-select [dispatch-value [args-binding] & body]
|
||||
`(do
|
||||
~dispatch-value
|
||||
(fn [~(common/ignore-unused '&query-type)
|
||||
~(common/ignore-unused '&model)
|
||||
~args-binding]
|
||||
~@body)))
|
||||
|
||||
(defmacro define-after-select [model [instance-binding] & body]
|
||||
`(do
|
||||
~model
|
||||
(fn [~(common/ignore-unused '&query-type)
|
||||
~(common/ignore-unused '&model)
|
||||
~(common/ignore-unused '&parsed-args)
|
||||
~instance-binding]
|
||||
~@body)))
|
||||
|
||||
(defmacro define-before-delete
|
||||
[model [instance-binding] & body]
|
||||
`(do
|
||||
~model
|
||||
(fn [~(common/ignore-unused '&model)
|
||||
~instance-binding]
|
||||
~@body)))
|
||||
@@ -0,0 +1,22 @@
|
||||
(ns macros.toucan2.tools.named-query
|
||||
(:require [macros.toucan2.common :as common]))
|
||||
|
||||
;;; separate function because recursive macroexpansion doesn't seem to work.
|
||||
(defn- define-named-query*
|
||||
[query-name query-type model resolved-query]
|
||||
`(do
|
||||
~query-name
|
||||
~query-type
|
||||
~model
|
||||
(fn [~(common/ignore-unused '&query-type)
|
||||
~(common/ignore-unused '&model)
|
||||
~(common/ignore-unused '&parsed-args)
|
||||
~(common/ignore-unused '&unresolved-query)]
|
||||
~resolved-query)))
|
||||
|
||||
(defmacro define-named-query
|
||||
([query-name resolved-query]
|
||||
(define-named-query* query-name :default :default resolved-query))
|
||||
|
||||
([query-name query-type model resolved-query]
|
||||
(define-named-query* query-name query-type model resolved-query)))
|
||||
4
.clj-kondo/com.github.metabase/hawk/config.edn
Normal file
4
.clj-kondo/com.github.metabase/hawk/config.edn
Normal file
@@ -0,0 +1,4 @@
|
||||
{:linters
|
||||
{:unresolved-symbol
|
||||
{:exclude
|
||||
[(clojure.test/is [partial= re= schema= =?])]}}}
|
||||
5
.clj-kondo/com.github.seancorfield/next.jdbc/config.edn
Normal file
5
.clj-kondo/com.github.seancorfield/next.jdbc/config.edn
Normal file
@@ -0,0 +1,5 @@
|
||||
{:hooks
|
||||
{:analyze-call
|
||||
{next.jdbc/with-transaction
|
||||
hooks.com.github.seancorfield.next-jdbc/with-transaction}}
|
||||
:lint-as {next.jdbc/on-connection clojure.core/with-open}}
|
||||
@@ -0,0 +1,18 @@
|
||||
(ns hooks.com.github.seancorfield.next-jdbc
|
||||
(:require [clj-kondo.hooks-api :as api]))
|
||||
|
||||
(defn with-transaction
|
||||
"Expands (with-transaction [tx expr opts] body)
|
||||
to (let [tx expr] opts body) pre clj-kondo examples."
|
||||
[{:keys [:node]}]
|
||||
(let [[binding-vec & body] (rest (:children node))
|
||||
[sym val opts] (:children binding-vec)]
|
||||
(when-not (and sym val)
|
||||
(throw (ex-info "No sym and val provided" {})))
|
||||
(let [new-node (api/list-node
|
||||
(list*
|
||||
(api/token-node 'let)
|
||||
(api/vector-node [sym val])
|
||||
opts
|
||||
body))]
|
||||
{:node new-node})))
|
||||
@@ -0,0 +1,18 @@
|
||||
(ns hooks.com.github.seancorfield.next-jdbc
|
||||
(:require [clj-kondo.hooks-api :as api]))
|
||||
|
||||
(defn with-transaction
|
||||
"Expands (with-transaction [tx expr opts] body)
|
||||
to (let [tx expr] opts body) per clj-kondo examples."
|
||||
[{:keys [:node]}]
|
||||
(let [[binding-vec & body] (rest (:children node))
|
||||
[sym val opts] (:children binding-vec)]
|
||||
(when-not (and sym val)
|
||||
(throw (ex-info "No sym and val provided" {})))
|
||||
(let [new-node (api/list-node
|
||||
(list*
|
||||
(api/token-node 'let)
|
||||
(api/vector-node [sym val])
|
||||
opts
|
||||
body))]
|
||||
{:node new-node})))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,147 +0,0 @@
|
||||
{:linters
|
||||
{:metabase/test-helpers-use-non-thread-safe-functions
|
||||
;; TODO -- seems silly to maintain different blacklists and whitelists here than we use for the deftest `^:parallel`
|
||||
;; checker...
|
||||
{:explicitly-safe
|
||||
#{ ;; these toucan methods might actually set global values if it's used outside of a transaction,
|
||||
;; but since mt/with-temp runs in a transaction, so we'll ignore them in this case.
|
||||
toucan2.core/delete!
|
||||
toucan2.core/update!
|
||||
toucan2.core/insert!
|
||||
toucan2.core/insert-returning-instance!
|
||||
toucan2.core/insert-returning-instances!
|
||||
toucan2.core/insert-returning-pk!
|
||||
toucan2.core/insert-returning-pks!
|
||||
clojure.core.async/<!!
|
||||
clojure.core.async/<!
|
||||
clojure.core.async/>!!
|
||||
clojure.core.async/>!
|
||||
clojure.core.async/alts!
|
||||
clojure.core.async/alts!!
|
||||
clojure.core.async/close!
|
||||
clojure.core.async/poll!
|
||||
clojure.core.memoize/memo-clear!
|
||||
clojure.core/conj!
|
||||
clojure.core/persistent!
|
||||
clojure.core/reset!
|
||||
clojure.core/swap!
|
||||
clojure.core/volatile!
|
||||
clojure.core/vreset!
|
||||
clojure.core/vswap!
|
||||
clojure.java.jdbc/execute!
|
||||
methodical.core/add-aux-method-with-unique-key!
|
||||
methodical.core/remove-aux-method-with-unique-key!
|
||||
next.jdbc/execute!
|
||||
|
||||
;; Definitely thread safe
|
||||
metabase.test.util.dynamic-redefs/patch-vars!
|
||||
|
||||
;; TODO: most of these symbols shouldn't be here, we should go through them and
|
||||
;; find the functions/macros that use them and make sure their names end with !
|
||||
;; best way to do this is try remove each of these and rely on kondo output to find places where it's used
|
||||
clojure.test/grant-collection-perms!
|
||||
clojure.test/grant-collection-perms-fn!
|
||||
clojure.test/grant-perms-fn!
|
||||
clojure.test/purge-old-entries!
|
||||
clojure.test/revoke-collection-perms!
|
||||
clojure.test/save-results!
|
||||
metabase-enterprise.advanced-permissions.models.permissions/update-db-download-permissions!
|
||||
metabase-enterprise.internal-user/install-internal-user!
|
||||
metabase-enterprise.sso.integrations.saml-test/call-with-login-attributes-cleared!
|
||||
metabase.actions.actions/perform-action!
|
||||
metabase.actions.models/insert!
|
||||
metabase.analytics.snowplow-test/fake-track-event-impl!
|
||||
metabase.analytics.snowplow/track-event-impl!
|
||||
metabase.app-db.core/finish-db-setup!
|
||||
metabase.app-db.core/migrate!
|
||||
metabase.app-db.core/setup-db!
|
||||
metabase.app-db.schema-migrations-test.impl/run-migrations-in-range!
|
||||
metabase.app-db.setup/migrate!
|
||||
metabase.app-db.setup/setup-db!
|
||||
metabase.channel.email-test/reset-inbox!
|
||||
metabase.channel.email/send-email!
|
||||
metabase.cmd.dump-to-h2/dump-to-h2!
|
||||
metabase.cmd.load-from-h2/load-from-h2!
|
||||
metabase.content-verification.models.moderation-review/create-review!
|
||||
metabase.core.core/ensure-audit-db-installed!
|
||||
metabase.driver.mongo-test/create-database-from-row-maps!
|
||||
metabase.driver.postgres-test/create-enums-db!
|
||||
metabase.driver.postgres-test/drop-if-exists-and-create-db!
|
||||
metabase.driver.sql-jdbc.execute/execute-statement!
|
||||
metabase.indexed-entities.models.model-index/add-values!
|
||||
metabase.driver.sql-jdbc.connection.ssh-tunnel-test/start-mock-servers!
|
||||
metabase.driver.sql-jdbc.connection.ssh-tunnel-test/stop-mock-servers!
|
||||
metabase.indexed-entities.task.index-values/job-init!
|
||||
metabase.model-persistence.models.persisted-info/ready-database!
|
||||
metabase.model-persistence.task.persist-refresh/job-init!
|
||||
metabase.model-persistence.task.persist-refresh/refresh-tables!
|
||||
metabase.model-persistence.task.persist-refresh/schedule-persistence-for-database!
|
||||
metabase.models.on-demand-test/add-dashcard-with-parameter-mapping!
|
||||
metabase.permissions.core/grant-application-permissions!
|
||||
metabase.permissions.core/grant-collection-read-permissions!
|
||||
metabase.permissions.core/grant-collection-readwrite-permissions!
|
||||
metabase.permissions.core/grant-full-data-permissions!
|
||||
metabase.permissions.core/grant-native-readwrite-permissions!
|
||||
metabase.permissions.core/grant-permissions!
|
||||
metabase.permissions.core/revoke-application-permissions!
|
||||
metabase.permissions.core/revoke-data-perms!
|
||||
metabase.permissions.core/update-data-perms-graph!
|
||||
metabase.permissions.core/update-group-permissions!
|
||||
metabase.permissions.models.collection.graph-test/clear-graph-revisions!
|
||||
metabase.permissions.models.collection.graph-test/do-with-n-temp-users-with-personal-collections!
|
||||
metabase.permissions.models.permissions/grant-application-permissions!
|
||||
metabase.permissions.models.permissions/grant-collection-read-permissions!
|
||||
metabase.permissions.models.permissions/grant-collection-readwrite-permissions!
|
||||
metabase.permissions.models.permissions/grant-full-data-permissions!
|
||||
metabase.permissions.models.permissions/grant-native-readwrite-permissions!
|
||||
metabase.permissions.models.permissions/grant-permissions!
|
||||
metabase.permissions.models.permissions/revoke-application-permissions!
|
||||
metabase.permissions.models.permissions/revoke-data-perms!
|
||||
metabase.permissions.models.permissions/update-data-perms-graph!
|
||||
metabase.permissions.models.permissions/update-group-permissions!
|
||||
metabase.permissions.test-util/with-restored-perms!
|
||||
metabase.public-sharing.api-test/add-card-to-dashboard!
|
||||
metabase.pulse.send/send-notifications!
|
||||
metabase.pulse.send/send-pulse!
|
||||
metabase.query-processor.streaming.interface/begin!
|
||||
metabase.query-processor.streaming.interface/finish!
|
||||
metabase.query-processor.streaming.interface/write-row!
|
||||
metabase.sample-data.impl/try-to-extract-sample-database!
|
||||
metabase.settings.models.setting-test/test-user-local-allowed-setting!
|
||||
metabase.settings.models.setting-test/test-user-local-only-setting!
|
||||
metabase.settings.models.setting.cache/restore-cache!
|
||||
metabase.settings.models.setting/set!
|
||||
metabase.settings.models.setting/validate-settings-formatting!
|
||||
metabase.setup.core/create-token!
|
||||
metabase.sso.ldap-test-util/start-ldap-server!
|
||||
metabase.sync.core/sync-database!
|
||||
metabase.sync.sync-metadata.fields.sync-metadata/update-field-metadata-if-needed!
|
||||
metabase.sync.sync-metadata/sync-db-metadata!
|
||||
metabase.sync.util-test/sync-database!
|
||||
metabase.sync.util/store-sync-summary!
|
||||
metabase.task.core/delete-task!
|
||||
metabase.task.impl/delete-task!
|
||||
metabase.test.data.bigquery-cloud-sdk/execute!
|
||||
metabase.test.data.impl.get-or-create/set-test-db-permissions!
|
||||
metabase.test.data.impl/copy-db-tables-and-fields!
|
||||
metabase.test.data.impl/get-or-create-database!
|
||||
metabase.test.data.impl/get-or-create-default-dataset!
|
||||
metabase.test.data.interface/create-db!
|
||||
metabase.test.data.interface/destroy-db!
|
||||
metabase.test.data.oracle/create-user!
|
||||
metabase.test.data.oracle/drop-user!
|
||||
metabase.test.data.sql-jdbc.load-data/make-insert!
|
||||
metabase.test.data.users/clear-cached-session-tokens!
|
||||
metabase.test.initialize/do-initialization!
|
||||
metabase.test.initialize/initialize-if-needed!
|
||||
metabase.test.util.log/ensure-unique-logger!
|
||||
metabase.test.util.log/set-ns-log-level!
|
||||
metabase.test.util/do-with-temp-env-var-value!
|
||||
metabase.test.util/restore-raw-setting!
|
||||
metabase.test.util/upsert-raw-setting!
|
||||
metabase.test/initialize-if-needed!
|
||||
metabase.test/test-helpers-set-global-values!
|
||||
metabase.test/with-temp-env-var-value!
|
||||
metabase.upload.impl-test/set-local-infile!
|
||||
metabase.util.files/create-dir-if-not-exists!
|
||||
metabase.warehouse-schema.models.field-values/create-or-update-full-field-values!}}}}
|
||||
52
.clj-kondo/hooks/clojure/test.clj
Normal file
52
.clj-kondo/hooks/clojure/test.clj
Normal file
@@ -0,0 +1,52 @@
|
||||
(ns hooks.clojure.test
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(def disallowed-parallel-forms
|
||||
'#{with-redefs
|
||||
clojure.core/with-redefs
|
||||
metabase.test/with-temporary-setting-values
|
||||
mt/with-temporary-setting-values})
|
||||
|
||||
(defn warn-about-disallowed-parallel-forms [form]
|
||||
(letfn [(f [form]
|
||||
(when-let [sexpr (and (hooks/token-node? form)
|
||||
(hooks/sexpr form))]
|
||||
(when (disallowed-parallel-forms sexpr)
|
||||
(hooks/reg-finding! (assoc (meta form)
|
||||
:message (format "%s is not allowed inside a ^:parallel test" sexpr)
|
||||
:type :metabase/validate-deftest)))))
|
||||
(walk [form]
|
||||
(f form)
|
||||
(doseq [child (:children form)]
|
||||
(walk child)))]
|
||||
(walk form)))
|
||||
|
||||
(defn deftest [{{[_ test-name & body] :children, :as node} :node}]
|
||||
(let [test-metadata (:meta test-name)
|
||||
metadata-sexprs (map hooks/sexpr test-metadata)
|
||||
combined-metadata (transduce
|
||||
(map (fn [x]
|
||||
(if (map? x)
|
||||
x
|
||||
{x true})))
|
||||
(completing merge)
|
||||
{}
|
||||
metadata-sexprs)
|
||||
parallel? (:parallel combined-metadata)
|
||||
synchronized? (:synchronized combined-metadata)]
|
||||
(when (and parallel? synchronized?)
|
||||
(hooks/reg-finding! (assoc (meta test-name)
|
||||
:message "Test should not be marked both ^:parallel and ^:synchronized"
|
||||
:type :metabase/validate-deftest)))
|
||||
;; only when the custom `:metabase/deftest-not-marked-parallel` is enabled: complain if tests are not explicitly
|
||||
;; marked `^:parallel` or `^:synchronized`. This is mostly to encourage people to mark everything `^:parallel` in
|
||||
;; places like `metabase.lib` tests unless there is a really good reason not to.
|
||||
(when-not (or parallel? synchronized?)
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta test-name)
|
||||
:message "Test should be marked either ^:parallel or ^:synchronized"
|
||||
:type :metabase/deftest-not-marked-parallel-or-synchronized)))
|
||||
(when parallel?
|
||||
(doseq [form body]
|
||||
(warn-about-disallowed-parallel-forms form))))
|
||||
{:node node})
|
||||
293
.clj-kondo/hooks/common.clj
Normal file
293
.clj-kondo/hooks/common.clj
Normal file
@@ -0,0 +1,293 @@
|
||||
(ns hooks.common
|
||||
(:require [clj-kondo.hooks-api :as hooks]
|
||||
[clojure.pprint]))
|
||||
|
||||
(defn with-macro-meta
|
||||
"When introducing internal nodes (let, defn, etc) it is important to provide a meta of an existing token
|
||||
as the current version of kondo will use the whole form by default."
|
||||
[new-node hook-node]
|
||||
(with-meta new-node (meta (first (:children hook-node)))))
|
||||
|
||||
;;; This stuff is to help debug hooks. Trace a function and it will pretty-print the before & after sexprs.
|
||||
;;;
|
||||
;;; (hooks.common/trace #'calculate-bird-scarcity)
|
||||
|
||||
(defn- trace* [f]
|
||||
{:pre [(fn? f)]}
|
||||
(fn traced-fn [node]
|
||||
(println \newline)
|
||||
(clojure.pprint/pprint (hooks/sexpr (:node node)))
|
||||
(let [node* (f node)]
|
||||
(println '=>)
|
||||
(clojure.pprint/pprint (hooks/sexpr (:node node*)))
|
||||
node*)))
|
||||
|
||||
;;; this doesn't seem to work with SCI. Use [[defn-traced]] instead.
|
||||
|
||||
;; (defn trace [varr]
|
||||
;; {:pre [(var? varr)]}
|
||||
;; (when-not (::traced? (meta varr))
|
||||
;; (alter-var-root varr trace*)
|
||||
;; (alter-meta! varr assoc ::traced? true)
|
||||
;; (println "Traced" varr)))
|
||||
|
||||
(defmacro defn-traced
|
||||
"Replace a hook [[defn]] with this to trace the before and after sexprs."
|
||||
[fn-name & fn-tail]
|
||||
`(do
|
||||
(defn fn# ~@fn-tail)
|
||||
(def ~fn-name (trace* fn#))))
|
||||
|
||||
;;;; Common hook definitions
|
||||
|
||||
(defn do*
|
||||
"This is the same idea as [[clojure.core/do]] but doesn't cause Kondo to complain about redundant dos or unused values."
|
||||
[{{[_ & args] :children, :as node} :node}]
|
||||
(let [node* (-> (hooks/list-node
|
||||
(list*
|
||||
(with-meta (hooks/token-node 'do) {:clj-kondo/ignore [:redundant-do]})
|
||||
(for [arg args]
|
||||
(vary-meta arg update :clj-kondo/ignore #(conj (vec %) :unused-value)))))
|
||||
(with-meta (meta node)))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-one-binding
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [x]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil]
|
||||
...)
|
||||
|
||||
Binding is optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[x] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-two-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [x y]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil, y nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[x y] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or y (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-three-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [x y z]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil, y nil, z nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[x y z] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or y (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or z (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-four-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [a b c d]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [a nil, b nil, c nil, d nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[a b c d] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or d (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-one-top-level-binding
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro x
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil]
|
||||
...)"
|
||||
[{{[_ x & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[x (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-two-top-level-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro x y
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil, y nil]
|
||||
...)"
|
||||
[{{[_ x y & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[x (hooks/token-node 'nil)
|
||||
y (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn let-one-with-optional-value
|
||||
"This is exactly like [[clojure.core/let]] but the right-hand side of the binding, `value`, is optional, and only one
|
||||
binding can be established.
|
||||
|
||||
(some-macro [x] ...)
|
||||
=>
|
||||
(let [x nil] ...)
|
||||
|
||||
(some-macro [x 100] ...)
|
||||
=>
|
||||
(let [x 100] ...)"
|
||||
[{{[_ {[binding value] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[binding (or value (hooks/token-node 'nil))])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn- let-second-inner [body bindings]
|
||||
(let [binding-infos (for [[model {[binding value] :children}] (partition 2 bindings)]
|
||||
{:model model
|
||||
:binding binding
|
||||
:value (or value
|
||||
(hooks/token-node 'nil))})]
|
||||
(-> (hooks/vector-node
|
||||
[(hooks/vector-node (map :model binding-infos))
|
||||
(-> (hooks/list-node (list* (hooks/token-node `let)
|
||||
(hooks/vector-node (mapcat (juxt :binding :value) binding-infos))
|
||||
body))
|
||||
(with-meta (meta body)))])
|
||||
(with-meta (meta body)))))
|
||||
|
||||
(defn let-second
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro x [y]
|
||||
...)
|
||||
|
||||
where the let is for second arg.
|
||||
|
||||
=>
|
||||
|
||||
(let [y nil]
|
||||
...)"
|
||||
[{:keys [node]}]
|
||||
(let [[_ first-arg-ref binding+opts & body] (:children node)]
|
||||
{:node (let-second-inner body [first-arg-ref binding+opts])}))
|
||||
|
||||
(defn let-with-optional-value-for-last-binding
|
||||
"This is exactly like [[clojure.core/let]] but the right-hand side of the *last* binding, `value`, is optional.
|
||||
|
||||
(some-macro [x] ...)
|
||||
=>
|
||||
(let [x nil] ...)
|
||||
|
||||
(some-macro [x 100] ...)
|
||||
=>
|
||||
(let [x 100] ...)"
|
||||
[{{[_ {bindings :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[binding value]]
|
||||
[binding (or value (hooks/token-node 'nil))])))
|
||||
bindings))
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-ignored-first-arg
|
||||
"For macros like
|
||||
|
||||
(discard-setting-changes [setting-1 setting-2]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(do ...)
|
||||
|
||||
where the first arg ought to be ignored for linting purposes."
|
||||
[{{[_ _x & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'do)
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-used-first-arg
|
||||
"For macros like
|
||||
|
||||
(with-drivers (filter pred? some-drivers)
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [_1234 (filter pred? some-drivers)]
|
||||
...)
|
||||
|
||||
where the first arg should be linted and appear to be used."
|
||||
[{{[_ arg & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node [(hooks/token-node (gensym "_"))
|
||||
arg])
|
||||
body))]
|
||||
{:node node*}))
|
||||
29
.clj-kondo/hooks/metabase/api/common.clj
Normal file
29
.clj-kondo/hooks/metabase/api/common.clj
Normal file
@@ -0,0 +1,29 @@
|
||||
(ns hooks.metabase.api.common
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common :as common]))
|
||||
|
||||
(defn route-fn-name
|
||||
"route fn hook"
|
||||
[method route]
|
||||
(let [route (if (vector? route) (first route) route)]
|
||||
(-> (str (name method) route)
|
||||
(str/replace #"/" "_")
|
||||
symbol)))
|
||||
|
||||
(defn defendpoint
|
||||
"defendpoint hook"
|
||||
[{:keys [node]}]
|
||||
(let [[method route & body] (rest (:children node))]
|
||||
{:node
|
||||
(hooks/vector-node [;; register usage of compojure core var
|
||||
(common/with-macro-meta (hooks/token-node (symbol (str "compojure.core")
|
||||
(str method)))
|
||||
node)
|
||||
;; define function with route-fn-name
|
||||
(hooks/list-node
|
||||
(list* (common/with-macro-meta (hooks/token-node 'clojure.core/defn)
|
||||
node)
|
||||
(route-fn-name (hooks/sexpr method) (hooks/sexpr route))
|
||||
body))])}))
|
||||
17
.clj-kondo/hooks/metabase/api/search_test.clj
Normal file
17
.clj-kondo/hooks/metabase/api/search_test.clj
Normal file
@@ -0,0 +1,17 @@
|
||||
(ns hooks.metabase.api.search-test
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn with-search-items-in-collection [{{[_ & args] :children, :as node} :node}]
|
||||
;; (println \newline)
|
||||
;; (clojure.pprint/pprint (hooks/sexpr node))
|
||||
(let [[created-items-sym search-string & body] args
|
||||
node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[created-items-sym (hooks/token-node 'nil)])
|
||||
search-string
|
||||
body))]
|
||||
;; (println '=>)
|
||||
;; (clojure.pprint/pprint (hooks/sexpr node*))
|
||||
{:node node*}))
|
||||
14
.clj-kondo/hooks/metabase/db/schema_migrations_test/impl.clj
Normal file
14
.clj-kondo/hooks/metabase/db/schema_migrations_test/impl.clj
Normal file
@@ -0,0 +1,14 @@
|
||||
(ns hooks.metabase.db.schema-migrations-test.impl
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn test-migrations
|
||||
[{{[_ migration-range {[binding] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node `let)
|
||||
(hooks/vector-node
|
||||
[binding (hooks/list-node (list (hooks/token-node `fn)
|
||||
(hooks/vector-node [])))])
|
||||
migration-range
|
||||
body))]
|
||||
{:node node*}))
|
||||
68
.clj-kondo/hooks/metabase/mbql/schemas/macros.clj
Normal file
68
.clj-kondo/hooks/metabase/mbql/schemas/macros.clj
Normal file
@@ -0,0 +1,68 @@
|
||||
(ns hooks.metabase.mbql.schemas.macros
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn- unwrap-defclause-clause-name
|
||||
"The `clause-name-form` can be either a plain symbol, or a vector like `[var-name mbql-clause-name]`. For the vector
|
||||
form, we are only interested in the `var-name`. For a plain symbol, the two names are the same. Return a token node."
|
||||
[clause-name-form]
|
||||
(-> (if (hooks/vector-node? clause-name-form)
|
||||
(first (:children clause-name-form))
|
||||
clause-name-form)
|
||||
(with-meta (meta clause-name-form))))
|
||||
|
||||
(defn- collect-defclause-body-schemas
|
||||
"Collect the argument schemas in the body of a `defclause` form into a vector node.
|
||||
|
||||
The body is a sequence of <argument-name> <argument-schema> pairs. We can ignore the argument names.
|
||||
|
||||
<argument-schema> can optionally be wrapped be `(optional ...)` or `(rest ...)`... we can ignore these."
|
||||
[arg-specs]
|
||||
(hooks/vector-node
|
||||
(for [[_arg-name arg-schema] (partition-all 2 arg-specs)
|
||||
:let [arg-schema (if (and (hooks/list-node? arg-schema)
|
||||
('#{optional rest} (hooks/sexpr (first (:children arg-schema)))))
|
||||
(-> (second (:children arg-schema))
|
||||
(with-meta (meta arg-schema)))
|
||||
arg-schema)]]
|
||||
arg-schema)))
|
||||
|
||||
(defn defclause
|
||||
"e.g.
|
||||
|
||||
(defclause [ag:var var] field-or-expression FieldOrExpressionDef)
|
||||
=>
|
||||
(def ag:var [FieldOrExpressionDef])"
|
||||
[{:keys [node]}]
|
||||
(let [[_defclause clause-name-form & arg-specs] (:children node)]
|
||||
{:node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'def)
|
||||
(unwrap-defclause-clause-name clause-name-form)
|
||||
(hooks/string-node "Docstring.")
|
||||
(collect-defclause-body-schemas arg-specs)))
|
||||
(with-meta (meta node)))}))
|
||||
|
||||
(comment
|
||||
(defn- defclause* [form]
|
||||
(hooks/sexpr
|
||||
(:node
|
||||
(defclause
|
||||
{:node
|
||||
(hooks/parse-string
|
||||
(with-out-str
|
||||
(clojure.pprint/pprint
|
||||
form)))}))))
|
||||
|
||||
(defn- x []
|
||||
(defclause* '(defclause [ag:var var]
|
||||
field-or-expression FieldOrExpressionDef)))
|
||||
;; =>
|
||||
(def ag:var "Docstring." [FieldOrExpressionDef])
|
||||
|
||||
(defn- y []
|
||||
(defclause* '(defclause var
|
||||
x (some-list-schema)
|
||||
y (optional FieldOrExpressionDef)
|
||||
z (rest (some-other-schema)))))
|
||||
;; =>
|
||||
(def var "Docstring." [(some-list-schema) FieldOrExpressionDef (some-other-schema)]))
|
||||
18
.clj-kondo/hooks/metabase/models/interface.clj
Normal file
18
.clj-kondo/hooks/metabase/models/interface.clj
Normal file
@@ -0,0 +1,18 @@
|
||||
(ns hooks.metabase.models.interface
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn define-hydration-method
|
||||
"This is used for both [[metabase.models.interface/define-simple-hydration-method]] and
|
||||
for [[metabase.models.interface/define-batched-hydration-method]]."
|
||||
[{{[_define-simple-hydration-method fn-name hydration-key & fn-tail] :children, :as node} :node}]
|
||||
(let [node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'do)
|
||||
hydration-key
|
||||
(hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'defn)
|
||||
fn-name
|
||||
fn-tail))))
|
||||
(with-meta (update (meta node) :clj-kondo/ignore #(hooks/vector-node (cons :clojure-lsp/unused-public-var (:children %))))))]
|
||||
{:node node}))
|
||||
71
.clj-kondo/hooks/metabase/models/setting.clj
Normal file
71
.clj-kondo/hooks/metabase/models/setting.clj
Normal file
@@ -0,0 +1,71 @@
|
||||
(ns hooks.metabase.models.setting
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common :as common]))
|
||||
|
||||
(defn defsetting
|
||||
"Rewrite a [[metabase.models.defsetting]] form like
|
||||
|
||||
(defsetting my-setting \"Description\" :type :boolean)
|
||||
|
||||
as
|
||||
|
||||
(let [_ \"Description\"
|
||||
_ :type
|
||||
_ :boolean]
|
||||
(defn my-setting \"Docstring.\" [])
|
||||
(defn my-setting! \"Docstring.\" [_value-or-nil]))
|
||||
|
||||
for linting purposes."
|
||||
[{:keys [node]}]
|
||||
(let [[setting-name & args] (rest (:children node))
|
||||
;; (defn my-setting [] ...)
|
||||
getter-node (-> (list
|
||||
(hooks/token-node 'defn)
|
||||
setting-name
|
||||
(hooks/string-node "Docstring.")
|
||||
(hooks/vector-node []))
|
||||
hooks/list-node
|
||||
(with-meta (meta node)))
|
||||
;; (defn my-setting! [_x] ...)
|
||||
setter-node (-> (list
|
||||
(hooks/token-node 'defn)
|
||||
(with-meta
|
||||
(hooks/token-node (symbol (str (hooks/sexpr setting-name) \!)))
|
||||
(meta setting-name))
|
||||
(hooks/string-node "Docstring.")
|
||||
(hooks/vector-node [(hooks/token-node '_value-or-nil)]))
|
||||
hooks/list-node
|
||||
(with-meta (update (meta node) :clj-kondo/ignore #(hooks/vector-node (cons :clojure-lsp/unused-public-var (:children %))))))]
|
||||
{:node (-> (list
|
||||
(hooks/token-node 'let)
|
||||
;; include description and the options map so they can get validated as well.
|
||||
(hooks/vector-node (vec (interleave (repeat (common/with-macro-meta (hooks/token-node '_) node))
|
||||
args)))
|
||||
getter-node
|
||||
setter-node)
|
||||
hooks/list-node
|
||||
(with-meta (meta node)))}))
|
||||
|
||||
(comment
|
||||
(defn- defsetting* [form]
|
||||
(hooks/sexpr
|
||||
(:node
|
||||
(defsetting
|
||||
{:node
|
||||
(hooks/parse-string
|
||||
(with-out-str
|
||||
(clojure.pprint/pprint
|
||||
form)))}))))
|
||||
|
||||
(defn- x []
|
||||
(defsetting*
|
||||
'(defsetting active-users-count
|
||||
(deferred-tru "Cached number of active users. Refresh every 5 minutes.")
|
||||
:visibility :admin
|
||||
:type :integer
|
||||
:default 0
|
||||
:getter (fn []
|
||||
(if-not ((requiring-resolve 'metabase.db/db-is-set-up?))
|
||||
0
|
||||
(cached-active-users-count)))))))
|
||||
@@ -0,0 +1,63 @@
|
||||
(ns hooks.metabase.public-settings.premium-features
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn defenterprise [{node :node}]
|
||||
(let [[_defenterprise fn-name & args] (:children node)
|
||||
[docstring & args] (if (hooks/string-node? (first args))
|
||||
args
|
||||
(cons nil args))
|
||||
[_enterprise-namespace & args] (if (hooks/token-node? (first args))
|
||||
args
|
||||
(cons nil args))
|
||||
[options fn-tail] (loop [options (hooks/map-node []), [x y & more :as args] args]
|
||||
(if (hooks/keyword-node? x)
|
||||
(recur (update options :children concat [x y]) more)
|
||||
[options args]))]
|
||||
{:node (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(hooks/token-node '_options) options])
|
||||
(-> (hooks/list-node
|
||||
(concat
|
||||
(filter some?
|
||||
(list (hooks/token-node 'defn)
|
||||
fn-name
|
||||
docstring))
|
||||
fn-tail))
|
||||
(with-meta (update (meta node) :clj-kondo/ignore #(hooks/vector-node (cons :clojure-lsp/unused-public-var (:children %))))))))}))
|
||||
|
||||
(comment
|
||||
(defn- defenterprise* [form]
|
||||
(hooks/sexpr
|
||||
(:node
|
||||
(defenterprise
|
||||
{:node
|
||||
(hooks/parse-string
|
||||
(with-out-str
|
||||
(clojure.pprint/pprint
|
||||
form)))}))))
|
||||
|
||||
(defn- x []
|
||||
(defenterprise*
|
||||
'(defenterprise score-result
|
||||
"Scoring implementation that adds score for items in official collections."
|
||||
:feature :any
|
||||
:fallback score-result-fallback
|
||||
[result]
|
||||
(conj (scoring/weights-and-scores result)
|
||||
{:weight 2
|
||||
:score (official-collection-score result)
|
||||
:name "official collection score"}
|
||||
{:weight 2
|
||||
:score (verified-score result)
|
||||
:name "verified"}))))
|
||||
|
||||
(defn- y []
|
||||
(defenterprise*
|
||||
'(defenterprise score-result
|
||||
"Score a result, returning a collection of maps with score and weight."
|
||||
metabase-enterprise.search.scoring
|
||||
[result]
|
||||
(weights-and-scores result)))))
|
||||
152
.clj-kondo/hooks/metabase/test/data.clj
Normal file
152
.clj-kondo/hooks/metabase/test/data.clj
Normal file
@@ -0,0 +1,152 @@
|
||||
(ns hooks.metabase.test.data
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[clojure.walk :as walk]))
|
||||
|
||||
(defn- global-dataset-symbols
|
||||
"Dataset definitions defined in [[metabase.test.data.dataset-definitions]]. This is only populated if clj-kondo has
|
||||
cached analysis for that namespace -- so it may or may not be populated. If it is populated we can do a bit of extra
|
||||
linting with that information."
|
||||
[]
|
||||
(not-empty (set (keys (:clj (hooks/ns-analysis 'metabase.test.data.dataset-definitions))))))
|
||||
|
||||
(defn- dataset-type
|
||||
"`dataset` can be one of:
|
||||
|
||||
- a qualified symbol
|
||||
- an unqualified symbol referring to a a var in [[metabase.test.data.dataset-definitions]]
|
||||
- an unqualified symbol (referring to a let-bound value or a var in the current namespace
|
||||
- some sort of non-symbol form like a function call
|
||||
|
||||
We can only determine if an unqualified symbol refers to something in the dataset definitions namespace if there are
|
||||
cached results available from [[global-dataset-symbols]]."
|
||||
[dataset]
|
||||
(let [sexpr (hooks/sexpr dataset)
|
||||
global-defs (global-dataset-symbols)]
|
||||
(cond
|
||||
(not (symbol? sexpr))
|
||||
:non-symbol
|
||||
|
||||
(namespace sexpr)
|
||||
:qualified
|
||||
|
||||
(empty? global-defs)
|
||||
:unqualified/unknown
|
||||
|
||||
(contains? global-defs sexpr)
|
||||
:unqualified/from-dataset-defs-namespace
|
||||
|
||||
;; either something defined in the current namespace or let-bound in the current scope.
|
||||
:else
|
||||
:unqualified/local-def)))
|
||||
|
||||
(defn dataset
|
||||
[{{[_ dataset & body] :children} :node}]
|
||||
(let [noop (constantly nil)
|
||||
body (case (dataset-type dataset)
|
||||
;; non-symbol, qualified symbols, and unqualified symbols from the current namespace/let-bound can all
|
||||
;; get converted from something like
|
||||
;;
|
||||
;; (dataset whatever
|
||||
;; ...)
|
||||
;;
|
||||
;; to
|
||||
;;
|
||||
;; (let [_ whatever]
|
||||
;; ...)
|
||||
(:non-symbol :qualified :unqualified/local-def)
|
||||
(list* (hooks/token-node 'let)
|
||||
(hooks/vector-node [(hooks/token-node '_) dataset])
|
||||
body)
|
||||
|
||||
;; for ones that came from the dataset defs namespace or ones whose origin is unknown, just ignore them
|
||||
;; and generate a `print` form:
|
||||
;;
|
||||
;; (print ...)
|
||||
;;
|
||||
;; (this used to be a `do` form (which makes a lot more semantic sense), but that resulted in warnings
|
||||
;; about unused values
|
||||
(:unqualified/from-dataset-defs-namespace :unqualified/unknown)
|
||||
(list* (hooks/token-node noop)
|
||||
body))]
|
||||
{:node (with-meta (hooks/list-node (with-meta body
|
||||
(meta dataset)))
|
||||
(meta dataset))}))
|
||||
|
||||
(defn- special-token-node?
|
||||
"Whether this node is one of the special symbols like `$field`."
|
||||
[node]
|
||||
(when (hooks/token-node? node)
|
||||
(let [symb (hooks/sexpr node)]
|
||||
(and (symbol? symb)
|
||||
(some (partial str/starts-with? symb)
|
||||
#{"$" "!" "&" "*" "%"})
|
||||
;; ignore args like `%` or `%1` inside function literals. $id forms have to be more than one character long,
|
||||
;; and the first character won't be a number (hopefully -- maybe some DBs allow this -- but we don't use it)
|
||||
(> (count (str symb)) 1)
|
||||
(not (re-find #"^%\d+" (str symb)))))))
|
||||
|
||||
(defn replace-$id-special-tokens
|
||||
"Impl for [[$ids]] and [[mbql-query]]. Walk `form` and look for special tokens like `$field` and replace them with
|
||||
strings so we don't get unresolved symbol errors. Preserves metadata."
|
||||
[form]
|
||||
;; [[walk/postwalk]] seems to preserve its meta so we don't need to do anything special
|
||||
(walk/postwalk
|
||||
(fn [node]
|
||||
(if (special-token-node? node)
|
||||
(-> (hooks/string-node (str (hooks/sexpr node)))
|
||||
(with-meta (meta node)))
|
||||
node))
|
||||
form))
|
||||
|
||||
(defn $ids
|
||||
[{{[_ & args] :children} :node}]
|
||||
;; `$ids` accepts either
|
||||
;;
|
||||
;; ($ids form)
|
||||
;;
|
||||
;; or
|
||||
;;
|
||||
;; ($ids table & body)
|
||||
;;
|
||||
;; table is only relevant for expanding the special tokens so we can ignore it.
|
||||
(let [body (if (= (count args) 1)
|
||||
(first args)
|
||||
(hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node `do)
|
||||
(rest args))))]
|
||||
{:node (replace-$id-special-tokens body)}))
|
||||
|
||||
(defn mbql-query
|
||||
[{{[_ & args] :children} :node}]
|
||||
;; `mbql-query` accepts either
|
||||
;;
|
||||
;; (mbql-query table)
|
||||
;;
|
||||
;; or
|
||||
;;
|
||||
;; (mbql-query table query)
|
||||
;;
|
||||
;; and table may be `nil`.
|
||||
;;
|
||||
;; table is only relevant for expanding the special tokens so we can ignore it either way.
|
||||
(let [[table query] (if (= (count args) 1)
|
||||
[(first args)
|
||||
(hooks/map-node [])]
|
||||
args)]
|
||||
(when-not ((some-fn symbol? nil?) (hooks/sexpr table))
|
||||
(hooks/reg-finding! (assoc (meta table)
|
||||
:message "First arg to mbql-query should be either a table name symbol or nil."
|
||||
:type ::mbql-query-first-arg)))
|
||||
(let [result (replace-$id-special-tokens query)
|
||||
;; HACK I'm not sure WHY it works but I ran into https://github.com/clj-kondo/clj-kondo/issues/1773 when
|
||||
;; trying to get this working -- for some magical reason wrapping the whole thing in a `do` form seems to fix
|
||||
;; it. Once that bug is resolved we can go ahead and remove this line
|
||||
result (with-meta (hooks/list-node (with-meta (list
|
||||
(hooks/token-node 'do)
|
||||
result)
|
||||
(meta query)))
|
||||
(meta query))]
|
||||
{:node result})))
|
||||
60
.clj-kondo/hooks/metabase/test/util.clj
Normal file
60
.clj-kondo/hooks/metabase/test/util.clj
Normal file
@@ -0,0 +1,60 @@
|
||||
(ns hooks.metabase.test.util
|
||||
(:require [clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common :as common]))
|
||||
|
||||
(defn- namespaced-symbol-node? [node]
|
||||
(when (hooks/token-node? node)
|
||||
(let [symb (hooks/sexpr node)]
|
||||
(and (symbol? symb)
|
||||
(namespace symb)))))
|
||||
|
||||
(defn with-temporary-setting-values
|
||||
"Rewrite a form like
|
||||
|
||||
(with-temporary-setting-values [x 1, some.ns/y 2]
|
||||
...)
|
||||
|
||||
as one like
|
||||
|
||||
(let [_ 1, _ some.ns/y, _ 2]
|
||||
...)
|
||||
|
||||
for analysis purposes. We only need to 'capture' namespaced Setting bindings with a `_` so Kondo considers their
|
||||
namespace to be 'used' and to catch undefined var usage."
|
||||
[{:keys [node]}]
|
||||
(let [{[_ bindings & body] :children} node
|
||||
bindings (if (hooks/vector-node? bindings)
|
||||
(hooks/vector-node (into []
|
||||
(mapcat (fn [[setting-name v]]
|
||||
(concat
|
||||
[(with-meta (hooks/token-node '_) (meta setting-name)) v]
|
||||
;; if the setting name is namespace-qualified add a `_`
|
||||
;; entry for it too.
|
||||
(when (namespaced-symbol-node? setting-name)
|
||||
[(with-meta (hooks/token-node '_) (meta setting-name)) setting-name]))))
|
||||
(partition 2 (:children bindings))))
|
||||
bindings)]
|
||||
{:node (-> (list*
|
||||
(hooks/token-node 'let)
|
||||
bindings
|
||||
body)
|
||||
(with-meta (meta body))
|
||||
hooks/list-node
|
||||
(with-meta (meta body)))}))
|
||||
|
||||
(defn with-temp-file
|
||||
[{{[_ {bindings :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[binding filename]]
|
||||
[binding (or (when (and filename
|
||||
(not (nil? (hooks/sexpr filename))))
|
||||
filename)
|
||||
(hooks/string-node "filename"))])))
|
||||
bindings))
|
||||
body))]
|
||||
{:node node*}))
|
||||
5
.clj-kondo/macros/common.clj
Normal file
5
.clj-kondo/macros/common.clj
Normal file
@@ -0,0 +1,5 @@
|
||||
(ns macros.common
|
||||
"Helpers for common Metabase macro shapes so we don't have to define the same macroexpansions over and over.")
|
||||
|
||||
(defn ignore-unused [symb]
|
||||
(vary-meta symb assoc :clj-kondo/ignore [:unused-binding]))
|
||||
12
.clj-kondo/macros/metabase/api/card_test.clj
Normal file
12
.clj-kondo/macros/metabase/api/card_test.clj
Normal file
@@ -0,0 +1,12 @@
|
||||
(ns macros.metabase.api.card-test
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-ordered-items
|
||||
[collection model-and-name-syms & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[model binding]]
|
||||
[(macros.common/ignore-unused binding) model])))
|
||||
model-and-name-syms)
|
||||
~collection
|
||||
~@body))
|
||||
17
.clj-kondo/macros/metabase/api/collection_test.clj
Normal file
17
.clj-kondo/macros/metabase/api/collection_test.clj
Normal file
@@ -0,0 +1,17 @@
|
||||
(ns macros.metabase.api.collection-test
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-collection-hierarchy [bindings & body]
|
||||
`(let [~(macros.common/ignore-unused 'a) nil
|
||||
~(macros.common/ignore-unused 'b) nil
|
||||
~(macros.common/ignore-unused 'c) nil
|
||||
~(macros.common/ignore-unused 'd) nil
|
||||
~(macros.common/ignore-unused 'e) nil
|
||||
~(macros.common/ignore-unused 'f) nil
|
||||
~(macros.common/ignore-unused 'g) nil]
|
||||
~bindings
|
||||
~@body))
|
||||
|
||||
(defmacro with-some-children-of-collection [collection-or-id-or-nil & body]
|
||||
`(let [~(macros.common/ignore-unused '&ids) ~collection-or-id-or-nil]
|
||||
~@body))
|
||||
5
.clj-kondo/macros/metabase/api/common.clj
Normal file
5
.clj-kondo/macros/metabase/api/common.clj
Normal file
@@ -0,0 +1,5 @@
|
||||
(ns macros.metabase.api.common)
|
||||
|
||||
(defmacro define-routes [& args]
|
||||
`(do (def ~'routes "docstring" nil)
|
||||
~@args))
|
||||
13
.clj-kondo/macros/metabase/api/embed_test.clj
Normal file
13
.clj-kondo/macros/metabase/api/embed_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.api.embed-test)
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-card-referencing
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-dashcard-referencing
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
13
.clj-kondo/macros/metabase/api/public_test.clj
Normal file
13
.clj-kondo/macros/metabase/api/public_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.api.public-test)
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-card-referencing
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-dashcard-referencing
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
@@ -1,24 +0,0 @@
|
||||
(ns macros.metabase.collections.api-test)
|
||||
|
||||
(defmacro with-collection-hierarchy! [bindings & body]
|
||||
`(let [~'a nil
|
||||
~'b nil
|
||||
~'c nil
|
||||
~'d nil
|
||||
~'e nil
|
||||
~'f nil
|
||||
~'g nil]
|
||||
~'a
|
||||
~'b
|
||||
~'c
|
||||
~'d
|
||||
~'e
|
||||
~'f
|
||||
~'g
|
||||
~bindings
|
||||
~@body))
|
||||
|
||||
(defmacro with-some-children-of-collection! [collection-or-id-or-nil & body]
|
||||
`(let [~'&ids ~collection-or-id-or-nil]
|
||||
~'&ids
|
||||
~@body))
|
||||
16
.clj-kondo/macros/metabase/domain_entities/malli.clj
Normal file
16
.clj-kondo/macros/metabase/domain_entities/malli.clj
Normal file
@@ -0,0 +1,16 @@
|
||||
(ns macros.metabase.domain-entities.malli)
|
||||
|
||||
(defmacro define-getters-and-setters [schema & defs]
|
||||
`(do
|
||||
(comment ~schema) ;; Reference the schema
|
||||
~@(for [[sym _path] (partition 2 defs)]
|
||||
`(do
|
||||
(defn ~(vary-meta sym
|
||||
assoc :export true)
|
||||
"docs" [_x#] nil)
|
||||
(defn ~(vary-meta (symbol (str "with-" (name sym)))
|
||||
assoc :export true)
|
||||
"docs" [x# _new#] x#)
|
||||
(defn ~(vary-meta (symbol (str (name sym) "-js"))
|
||||
assoc :export true)
|
||||
"docs" [x#] x#)))))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.embedding.api.embed-test)
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-card-referencing!
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-dashcard-referencing!
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
@@ -0,0 +1,4 @@
|
||||
(ns macros.metabase.models.params.chain-filter-test)
|
||||
|
||||
(defmacro chain-filter [& _]
|
||||
`(metabase.models.params.chain-filter/chain-filter nil nil))
|
||||
9
.clj-kondo/macros/metabase/models/user_test.clj
Normal file
9
.clj-kondo/macros/metabase/models/user_test.clj
Normal file
@@ -0,0 +1,9 @@
|
||||
(ns macros.metabase.models.user-test)
|
||||
|
||||
(defmacro with-groups [bindings & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 3)
|
||||
(mapcat (fn [[group-binding properties members]]
|
||||
[group-binding [properties members]])))
|
||||
bindings)
|
||||
~@body))
|
||||
@@ -1,4 +0,0 @@
|
||||
(ns macros.metabase.parameters.chain-filter-test)
|
||||
|
||||
(defmacro chain-filter [& _]
|
||||
`(metabase.parameters.chain-filter/chain-filter nil nil))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.public-sharing.api-test)
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-card-referencing!
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-dashcard-referencing!
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.queries.api.card-test)
|
||||
|
||||
(defmacro with-ordered-items
|
||||
[collection model-and-name-syms & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[model binding]]
|
||||
[binding model])))
|
||||
model-and-name-syms)
|
||||
~collection
|
||||
~@(->> (partition-all 2 model-and-name-syms)
|
||||
(map second))
|
||||
~@body))
|
||||
@@ -1,12 +0,0 @@
|
||||
(ns macros.metabase.query-analysis.models.query-analysis-test)
|
||||
|
||||
(defmacro with-test-setup [& body]
|
||||
`(let [~'card-id 1
|
||||
~'table-id 2
|
||||
~'tax-id 3
|
||||
~'total-id 4]
|
||||
~'card-id
|
||||
~'table-id
|
||||
~'tax-id
|
||||
~'total-id
|
||||
~@body))
|
||||
@@ -1,18 +0,0 @@
|
||||
(ns macros.metabase.query-analysis.task.test-setup)
|
||||
|
||||
(defmacro with-test-setup!
|
||||
"Set up the data required to test the Query Analysis related tasks"
|
||||
[& body]
|
||||
`(let [~'c1 1
|
||||
~'c2 2
|
||||
~'c3 3
|
||||
~'c4 4
|
||||
~'archived 5
|
||||
~'invalid 6]
|
||||
~'c1
|
||||
~'c2
|
||||
~'c3
|
||||
~'c4
|
||||
~'archived
|
||||
~'invalid
|
||||
~@body))
|
||||
13
.clj-kondo/macros/metabase/related_test.clj
Normal file
13
.clj-kondo/macros/metabase/related_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.related-test
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-world [& body]
|
||||
`(let [~(macros.common/ignore-unused 'collection-id) nil
|
||||
~(macros.common/ignore-unused 'metric-id-a) nil
|
||||
~(macros.common/ignore-unused 'metric-id-b) nil
|
||||
~(macros.common/ignore-unused 'segment-id-a) nil
|
||||
~(macros.common/ignore-unused 'segment-id-b) nil
|
||||
~(macros.common/ignore-unused 'card-id-a) nil
|
||||
~(macros.common/ignore-unused 'card-id-b) nil
|
||||
~(macros.common/ignore-unused 'card-id-c) nil]
|
||||
~@body))
|
||||
@@ -1,9 +0,0 @@
|
||||
(ns macros.metabase.server.statistics-handler-test)
|
||||
|
||||
(defmacro with-server
|
||||
[_ _ [connector chan-start-handle chan-finish-handle chan-done-request] & body]
|
||||
`(let [~connector nil
|
||||
~chan-start-handle nil
|
||||
~chan-finish-handle nil
|
||||
~chan-done-request nil]
|
||||
~@body))
|
||||
8
.clj-kondo/macros/metabase/shared/util/namespaces.clj
Normal file
8
.clj-kondo/macros/metabase/shared/util/namespaces.clj
Normal file
@@ -0,0 +1,8 @@
|
||||
(ns macros.metabase.shared.util.namespaces)
|
||||
|
||||
(defmacro import-fn
|
||||
"Kondo macro replacement for [[metabase.shared.util.namespaces/import-fn]]."
|
||||
([sym]
|
||||
`(def ~(-> sym name symbol) "docstring" ~sym))
|
||||
([sym name]
|
||||
`(def ~name "docstring" ~sym)))
|
||||
5
.clj-kondo/macros/metabase/test/persistence.clj
Normal file
5
.clj-kondo/macros/metabase/test/persistence.clj
Normal file
@@ -0,0 +1,5 @@
|
||||
(ns macros.metabase.test.persistence)
|
||||
|
||||
(defmacro with-persistence-enabled [[persist-fn-sym] & body]
|
||||
`(clojure.core/let [~persist-fn-sym (fn [])]
|
||||
~@body))
|
||||
@@ -1,11 +1,11 @@
|
||||
(ns macros.metabase.test.util)
|
||||
|
||||
(defmacro with-temp-env-var-value! [bindings & body]
|
||||
(defmacro with-temp-env-var-value [bindings & body]
|
||||
`((constantly nil)
|
||||
~@(map second (partition-all 2 bindings))
|
||||
~@body))
|
||||
|
||||
(defmacro with-temporary-raw-setting-values [bindings & body]
|
||||
`((constantly nil)
|
||||
~@(map second (partition-all 2 bindings))
|
||||
~@(map second (partition-all 2 bindings))
|
||||
~@body))
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
(ns macros.metabase.users.models.user-test)
|
||||
|
||||
(defmacro with-groups! [bindings & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 3)
|
||||
(mapcat (fn [[group-binding properties members]]
|
||||
[group-binding [properties members]])))
|
||||
bindings)
|
||||
~@body))
|
||||
@@ -1,8 +0,0 @@
|
||||
(ns macros.metabase.util.namespaces)
|
||||
|
||||
(defmacro import-fn
|
||||
"Kondo macro replacement for [[metabase.util.namespaces/import-fn]]."
|
||||
([sym]
|
||||
`(def ~(-> sym name symbol) "docstring" ~sym))
|
||||
([sym name]
|
||||
`(def ~name "docstring" ~sym)))
|
||||
@@ -1,16 +0,0 @@
|
||||
(ns macros.metabase.xrays.domain-entities.malli)
|
||||
|
||||
(defmacro define-getters-and-setters [schema & defs]
|
||||
`(do
|
||||
(comment ~schema) ;; Reference the schema
|
||||
~@(for [[sym _path] (partition 2 defs)]
|
||||
`(do
|
||||
(defn ~(vary-meta sym
|
||||
assoc :export true)
|
||||
"docs" [_x#] nil)
|
||||
(defn ~(vary-meta (symbol (str "with-" (name sym)))
|
||||
assoc :export true)
|
||||
"docs" [x# _new#] x#)
|
||||
(defn ~(vary-meta (symbol (str (name sym) "-js"))
|
||||
assoc :export true)
|
||||
"docs" [x#] x#)))))
|
||||
@@ -1,20 +0,0 @@
|
||||
(ns macros.metabase.xrays.related-test)
|
||||
|
||||
(defmacro with-world [& body]
|
||||
`(let [~'collection-id nil
|
||||
~'metric-id-a nil
|
||||
~'metric-id-b nil
|
||||
~'segment-id-a nil
|
||||
~'segment-id-b nil
|
||||
~'card-id-a nil
|
||||
~'card-id-b nil
|
||||
~'card-id-c nil]
|
||||
~'collection-id
|
||||
~'metric-id-a
|
||||
~'metric-id-b
|
||||
~'segment-id-a
|
||||
~'segment-id-b
|
||||
~'card-id-a
|
||||
~'card-id-b
|
||||
~'card-id-c
|
||||
~@body))
|
||||
@@ -1,5 +0,0 @@
|
||||
(ns macros.metabase-enterprise.database-routing.e2e-test)
|
||||
|
||||
(defmacro with-temp-dbs! [bindings & body]
|
||||
`(let [~@(interleave bindings (repeat nil))]
|
||||
~@body))
|
||||
@@ -1,6 +0,0 @@
|
||||
(ns macros.metabase-enterprise.impersonation.util-test)
|
||||
|
||||
(defmacro with-impersonations! [impersonations-and-attributes-map & body]
|
||||
`(let [~'&group ~impersonations-and-attributes-map]
|
||||
~'&group
|
||||
~@body))
|
||||
@@ -1,18 +0,0 @@
|
||||
(ns macros.metabase-enterprise.query-reference-validation.api-test)
|
||||
|
||||
(defmacro with-test-setup! [& body]
|
||||
`(let [~'card-1 1
|
||||
~'card-2 2
|
||||
~'card-3 3
|
||||
~'card-4 4
|
||||
~'card-5 5
|
||||
~'coll-2 100
|
||||
~'coll-3 200]
|
||||
~'card-1
|
||||
~'card-2
|
||||
~'card-3
|
||||
~'card-4
|
||||
~'card-5
|
||||
~'coll-2
|
||||
~'coll-3
|
||||
~@body))
|
||||
@@ -1,6 +1,6 @@
|
||||
(ns macros.metabase-enterprise.sandbox.test-util)
|
||||
(ns macros.metabase-enterprise.sandbox.test-util
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-gtaps! [gtaps-definition & body]
|
||||
`(let [~'&group ~gtaps-definition]
|
||||
~'&group
|
||||
(defmacro with-gtaps [gtaps-definition & body]
|
||||
`(let [~(macros.common/ignore-unused '&group) ~gtaps-definition]
|
||||
~@body))
|
||||
|
||||
@@ -1,112 +1,58 @@
|
||||
(ns macros.metabase-enterprise.serialization.test-util)
|
||||
(ns macros.metabase-enterprise.serialization.test-util
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-world [& body]
|
||||
`(let [~'db-id nil
|
||||
~'table-id nil
|
||||
~'table nil
|
||||
~'table-id-categories nil
|
||||
~'table-id-users nil
|
||||
~'table-id-checkins nil
|
||||
~'venues-pk-field-id nil
|
||||
~'numeric-field-id nil
|
||||
~'name-field-id nil
|
||||
~'latitude-field-id nil
|
||||
~'longitude-field-id nil
|
||||
~'category-field-id nil
|
||||
~'category-pk-field-id nil
|
||||
~'date-field-id nil
|
||||
~'users-pk-field-id nil
|
||||
~'user-id-field-id nil
|
||||
~'checkins->venues-field-id nil
|
||||
~'last-login-field-id nil
|
||||
~'collection-id nil
|
||||
~'collection-id-nested nil
|
||||
~'user-id-temp nil
|
||||
~'personal-collection-id nil
|
||||
~'pc-felicia-nested-id nil
|
||||
~'pc-nested-id nil
|
||||
~'pc-deeply-nested-id nil
|
||||
~'metric-id nil
|
||||
~'segment-id nil
|
||||
~'dashboard-id nil
|
||||
~'root-dashboard-id nil
|
||||
~'card-id nil
|
||||
~'card-arch-id nil
|
||||
~'card-id-root nil
|
||||
~'card-id-nested nil
|
||||
~'card-id-nested-query nil
|
||||
~'card-id-native-query nil
|
||||
~'card-id-pivot-table nil
|
||||
~'dashcard-id nil
|
||||
~'dashcard-top-level-click-id nil
|
||||
~'dashcard-with-click-actions nil
|
||||
~'dashcard-with-textbox-id nil
|
||||
~'card-id-root-to-collection nil
|
||||
~'card-id-collection-to-root nil
|
||||
~'pulse-id nil
|
||||
~'pulsecard-root-id nil
|
||||
~'pulsecard-collection-id nil
|
||||
~'card-id-template-tags nil
|
||||
~'card-id-filter-agg nil
|
||||
~'card-id-temporal-unit nil
|
||||
~'snippet-id nil
|
||||
~'snippet-collection-id nil
|
||||
~'snippet-nested-collection-id nil
|
||||
~'nested-snippet-id nil
|
||||
~'card-id-with-native-snippet nil
|
||||
~'card-join-card-id nil]
|
||||
~'db-id
|
||||
~'table-id
|
||||
~'table
|
||||
~'table-id-categories
|
||||
~'table-id-users
|
||||
~'table-id-checkins
|
||||
~'venues-pk-field-id
|
||||
~'numeric-field-id
|
||||
~'name-field-id
|
||||
~'latitude-field-id
|
||||
~'longitude-field-id
|
||||
~'category-field-id
|
||||
~'category-pk-field-id
|
||||
~'date-field-id
|
||||
~'users-pk-field-id
|
||||
~'user-id-field-id
|
||||
~'checkins->venues-field-id
|
||||
~'last-login-field-id
|
||||
~'collection-id
|
||||
~'collection-id-nested
|
||||
~'user-id-temp
|
||||
~'personal-collection-id
|
||||
~'pc-felicia-nested-id
|
||||
~'pc-nested-id
|
||||
~'pc-deeply-nested-id
|
||||
~'metric-id
|
||||
~'segment-id
|
||||
~'dashboard-id
|
||||
~'root-dashboard-id
|
||||
~'card-id
|
||||
~'card-arch-id
|
||||
~'card-id-root
|
||||
~'card-id-nested
|
||||
~'card-id-nested-query
|
||||
~'card-id-native-query
|
||||
~'card-id-pivot-table
|
||||
~'dashcard-id
|
||||
~'dashcard-top-level-click-id
|
||||
~'dashcard-with-click-actions
|
||||
~'dashcard-with-textbox-id
|
||||
~'card-id-root-to-collection
|
||||
~'card-id-collection-to-root
|
||||
~'pulse-id
|
||||
~'pulsecard-root-id
|
||||
~'pulsecard-collection-id
|
||||
~'card-id-template-tags
|
||||
~'card-id-filter-agg
|
||||
~'card-id-temporal-unit
|
||||
~'snippet-id
|
||||
~'snippet-collection-id
|
||||
~'snippet-nested-collection-id
|
||||
~'nested-snippet-id
|
||||
~'card-id-with-native-snippet
|
||||
~'card-join-card-id
|
||||
`(let [~(macros.common/ignore-unused 'db-id) nil
|
||||
~(macros.common/ignore-unused 'table-id) nil
|
||||
~(macros.common/ignore-unused 'table) nil
|
||||
~(macros.common/ignore-unused 'table-id-categories) nil
|
||||
~(macros.common/ignore-unused 'table-id-users) nil
|
||||
~(macros.common/ignore-unused 'table-id-checkins) nil
|
||||
~(macros.common/ignore-unused 'venues-pk-field-id) nil
|
||||
~(macros.common/ignore-unused 'numeric-field-id) nil
|
||||
~(macros.common/ignore-unused 'name-field-id) nil
|
||||
~(macros.common/ignore-unused 'latitude-field-id) nil
|
||||
~(macros.common/ignore-unused 'longitude-field-id) nil
|
||||
~(macros.common/ignore-unused 'category-field-id) nil
|
||||
~(macros.common/ignore-unused 'category-pk-field-id) nil
|
||||
~(macros.common/ignore-unused 'date-field-id) nil
|
||||
~(macros.common/ignore-unused 'users-pk-field-id) nil
|
||||
~(macros.common/ignore-unused 'user-id-field-id) nil
|
||||
~(macros.common/ignore-unused 'checkins->venues-field-id) nil
|
||||
~(macros.common/ignore-unused 'last-login-field-id) nil
|
||||
~(macros.common/ignore-unused 'collection-id) nil
|
||||
~(macros.common/ignore-unused 'collection-id-nested) nil
|
||||
~(macros.common/ignore-unused 'user-id-temp) nil
|
||||
~(macros.common/ignore-unused 'personal-collection-id) nil
|
||||
~(macros.common/ignore-unused 'pc-felicia-nested-id) nil
|
||||
~(macros.common/ignore-unused 'pc-nested-id) nil
|
||||
~(macros.common/ignore-unused 'pc-deeply-nested-id) nil
|
||||
~(macros.common/ignore-unused 'metric-id) nil
|
||||
~(macros.common/ignore-unused 'segment-id) nil
|
||||
~(macros.common/ignore-unused 'dashboard-id) nil
|
||||
~(macros.common/ignore-unused 'root-dashboard-id) nil
|
||||
~(macros.common/ignore-unused 'card-id) nil
|
||||
~(macros.common/ignore-unused 'card-arch-id) nil
|
||||
~(macros.common/ignore-unused 'card-id-root) nil
|
||||
~(macros.common/ignore-unused 'card-id-nested) nil
|
||||
~(macros.common/ignore-unused 'card-id-nested-query) nil
|
||||
~(macros.common/ignore-unused 'card-id-native-query) nil
|
||||
~(macros.common/ignore-unused 'dashcard-id) nil
|
||||
~(macros.common/ignore-unused 'dashcard-top-level-click-id) nil
|
||||
~(macros.common/ignore-unused 'dashcard-with-click-actions) nil
|
||||
~(macros.common/ignore-unused 'dashcard-with-textbox-id) nil
|
||||
~(macros.common/ignore-unused 'card-id-root-to-collection) nil
|
||||
~(macros.common/ignore-unused 'card-id-collection-to-root) nil
|
||||
~(macros.common/ignore-unused 'pulse-id) nil
|
||||
~(macros.common/ignore-unused 'pulsecard-root-id) nil
|
||||
~(macros.common/ignore-unused 'pulsecard-collection-id) nil
|
||||
~(macros.common/ignore-unused 'card-id-template-tags) nil
|
||||
~(macros.common/ignore-unused 'card-id-filter-agg) nil
|
||||
~(macros.common/ignore-unused 'card-id-temporal-unit) nil
|
||||
~(macros.common/ignore-unused 'snippet-id) nil
|
||||
~(macros.common/ignore-unused 'snippet-collection-id) nil
|
||||
~(macros.common/ignore-unused 'snippet-nested-collection-id) nil
|
||||
~(macros.common/ignore-unused 'nested-snippet-id) nil
|
||||
~(macros.common/ignore-unused 'card-id-with-native-snippet) nil
|
||||
~(macros.common/ignore-unused 'card-join-card-id) nil]
|
||||
~@body))
|
||||
|
||||
14
.clj-kondo/methodical/methodical/config.edn
Normal file
14
.clj-kondo/methodical/methodical/config.edn
Normal file
@@ -0,0 +1,14 @@
|
||||
{:config-paths ["macros"]
|
||||
|
||||
:lint-as
|
||||
{}
|
||||
|
||||
:hooks
|
||||
{:analyze-call
|
||||
{methodical.core/defmethod hooks.methodical.macros/defmethod
|
||||
methodical.core/defmulti hooks.methodical.macros/defmulti
|
||||
methodical.macros/defmethod hooks.methodical.macros/defmethod
|
||||
methodical.macros/defmulti hooks.methodical.macros/defmulti}
|
||||
|
||||
:macroexpand
|
||||
{methodical.impl.combo.operator/defoperator macros.methodical.impl.combo.operator/defoperator}}}
|
||||
68
.clj-kondo/methodical/methodical/hooks/methodical/core.clj
Normal file
68
.clj-kondo/methodical/methodical/hooks/methodical/core.clj
Normal file
@@ -0,0 +1,68 @@
|
||||
(ns hooks.methodical.core
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn add-next-method [fn-tail]
|
||||
(if (hooks/vector-node? (first fn-tail))
|
||||
(let [[args & body] fn-tail]
|
||||
(list*
|
||||
(-> (hooks/vector-node
|
||||
(cons (hooks/token-node 'next-method)
|
||||
(:children args)))
|
||||
(with-meta (meta args)))
|
||||
;; so Kondo stops complaining about it being unused.
|
||||
(hooks/token-node 'next-method)
|
||||
body))
|
||||
(for [list-node fn-tail]
|
||||
(hooks/list-node (add-next-method (:children list-node))))))
|
||||
|
||||
(defn defmethod
|
||||
[{{[_ multimethod & [first-arg :as args]] :children, :as node} :node}]
|
||||
#_(clojure.pprint/pprint (hooks/sexpr node))
|
||||
(let [[aux-qualifier dispatch-value & fn-tail] (if (#{:before :after :around} (hooks/sexpr first-arg))
|
||||
(cons (hooks/sexpr first-arg) (rest args))
|
||||
(cons nil args))
|
||||
fn-tail (if (contains? #{:around nil} aux-qualifier)
|
||||
(add-next-method fn-tail)
|
||||
fn-tail)
|
||||
result (hooks/list-node
|
||||
(list* (hooks/token-node 'clojure.core/defmethod)
|
||||
multimethod
|
||||
dispatch-value
|
||||
fn-tail))]
|
||||
#_(println "=>")
|
||||
#_(clojure.pprint/pprint (hooks/sexpr result))
|
||||
{:node result}))
|
||||
|
||||
(defn defmulti
|
||||
[{{[_ multimethod-name & args] :children, :as node} :node}]
|
||||
#_(clojure.pprint/pprint (hooks/sexpr node))
|
||||
(let [[docstring & args] (if (hooks/string-node? (first args))
|
||||
args
|
||||
(cons nil args))
|
||||
[attribute-map & args] (if (hooks/map-node? (first args))
|
||||
args
|
||||
(cons nil args))
|
||||
;; if there wasn't a positional dispatch function arg passed just use (constantly nil) so Kondo won't complain
|
||||
[dispatch-fn & kv-options] (if (odd? (count args))
|
||||
args
|
||||
(cons (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'clojure.core/constantly)
|
||||
(hooks/token-node 'nil)))
|
||||
args))]
|
||||
(let [defmulti-form (hooks/list-node
|
||||
(filter
|
||||
some?
|
||||
[(hooks/token-node 'clojure.core/defmulti)
|
||||
multimethod-name
|
||||
docstring
|
||||
attribute-map
|
||||
dispatch-fn]))
|
||||
result (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'do)
|
||||
defmulti-form
|
||||
kv-options))]
|
||||
#_(println "=>")
|
||||
#_(clojure.pprint/pprint (hooks/sexpr result))
|
||||
{:node result})))
|
||||
185
.clj-kondo/methodical/methodical/hooks/methodical/macros.clj
Normal file
185
.clj-kondo/methodical/methodical/hooks/methodical/macros.clj
Normal file
@@ -0,0 +1,185 @@
|
||||
(ns hooks.methodical.macros
|
||||
(:refer-clojure :exclude [defmulti defmethod])
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
;;; The code below is basically simulating the spec for parsing defmethod args without using spec. It uses a basic
|
||||
;;; backtracking algorithm to achieve a similar result. Parsing defmethod args is kinda complicated.
|
||||
;;;
|
||||
;;; Unfortunately this is hardcoded to `:before`, `:after`, and `:around` as the only allowed qualifiers for now... at
|
||||
;;; some point in the future we'll have to figure out how to fix this and support other qualifiers too.
|
||||
|
||||
(defn- bindings-vector? [x]
|
||||
(and (hooks/vector-node? x)
|
||||
(every? (some-fn hooks/token-node?
|
||||
hooks/map-node?
|
||||
hooks/vector-node?)
|
||||
(:children x))))
|
||||
|
||||
(defn- single-arity-fn-tail? [args]
|
||||
(bindings-vector? (first args)))
|
||||
|
||||
(defn- n-arity-fn-tail? [args]
|
||||
(and (seq args)
|
||||
(every? (fn [x]
|
||||
(and (hooks/list-node? x)
|
||||
(single-arity-fn-tail? (:children x))))
|
||||
args)))
|
||||
|
||||
(defn- fn-tail? [args]
|
||||
(or (single-arity-fn-tail? args)
|
||||
(n-arity-fn-tail? args)))
|
||||
|
||||
(defn- qualifier? [x]
|
||||
(and (hooks/keyword-node? x)
|
||||
(#{:before :after :around} (hooks/sexpr x))))
|
||||
|
||||
(defn- dispatch-value?
|
||||
"A dispatch value can be anything except for qualifier keyword or a list that looks like part of a n-arity function tail
|
||||
e.g. `([x] x)`."
|
||||
[x]
|
||||
(and (not (qualifier? x))
|
||||
(or (not (hooks/list-node? x))
|
||||
(not (single-arity-fn-tail? (:children x))))))
|
||||
|
||||
(defonce ^:private backtrack (Exception.))
|
||||
|
||||
(defn- parse-defmethod-args
|
||||
([unparsed]
|
||||
(let [parses (atom [])]
|
||||
(try
|
||||
(parse-defmethod-args parses {} unparsed)
|
||||
(catch Exception _
|
||||
(when (zero? (count @parses))
|
||||
(throw (ex-info (format "Unable to parse defmethod args: %s" (pr-str (mapv hooks/sexpr unparsed)))
|
||||
{:args (mapv hooks/sexpr unparsed)})))
|
||||
(when (> (count @parses) 1)
|
||||
(throw (ex-info (format "Ambiguous defmethod args: %s" (pr-str (mapv hooks/sexpr unparsed)))
|
||||
{:args (mapv hooks/sexpr unparsed)
|
||||
:parses @parses})))
|
||||
(first @parses)))))
|
||||
|
||||
([parses parsed unparsed]
|
||||
(cond
|
||||
(and (not (contains? parsed :qualifier))
|
||||
(qualifier? (first unparsed)))
|
||||
(try
|
||||
(parse-defmethod-args parses (assoc parsed :qualifier (first unparsed)) (rest unparsed))
|
||||
(catch Exception _
|
||||
(parse-defmethod-args parses (assoc parsed :qualifier nil) unparsed)))
|
||||
|
||||
(and (not (contains? parsed :dispatch-value))
|
||||
(dispatch-value? (first unparsed)))
|
||||
(parse-defmethod-args parses (assoc parsed :dispatch-value (first unparsed)) (rest unparsed))
|
||||
|
||||
(not (contains? parsed :dispatch-value))
|
||||
(throw backtrack)
|
||||
|
||||
(and (not (contains? parsed :unique-key))
|
||||
(:qualifier parsed) ; can only have unique keys for aux methods
|
||||
(not (hooks/string-node? (first unparsed)))
|
||||
(not (hooks/list-node? (first unparsed)))
|
||||
(not (hooks/vector-node? (first unparsed))))
|
||||
(try
|
||||
(parse-defmethod-args parses (assoc parsed :unique-key (first unparsed)) (rest unparsed))
|
||||
(catch Exception _
|
||||
(parse-defmethod-args parses (assoc parsed :unique-key nil) unparsed)))
|
||||
|
||||
(and (not (contains? parsed :docstring))
|
||||
(hooks/string-node? (first unparsed)))
|
||||
(try
|
||||
(parse-defmethod-args parses (assoc parsed :docstring (first unparsed)) (rest unparsed))
|
||||
(catch Exception _
|
||||
(parse-defmethod-args parses (assoc parsed :docstring nil) unparsed)))
|
||||
|
||||
(fn-tail? unparsed)
|
||||
(do
|
||||
(swap! parses conj (assoc parsed :fn-tail unparsed))
|
||||
(throw backtrack))
|
||||
|
||||
:else
|
||||
(throw backtrack))))
|
||||
|
||||
(defn defmethod
|
||||
[{{[_ multimethod & args] :children, :as node} :node}]
|
||||
(#_println)
|
||||
#_(clojure.pprint/pprint (hooks/sexpr node))
|
||||
(let [parsed (parse-defmethod-args args)]
|
||||
#_(doseq [[k v] parsed]
|
||||
(println \newline k '=> (pr-str (some-> v hooks/sexpr))))
|
||||
(let [fn-tail (:fn-tail parsed)
|
||||
other-stuff (dissoc parsed :fn-tail)
|
||||
result (hooks/list-node
|
||||
(concat
|
||||
[(hooks/token-node 'do)
|
||||
multimethod]
|
||||
(filter some? (vals other-stuff))
|
||||
[(-> (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'fn)
|
||||
(hooks/token-node (if (contains? #{nil :around} (some-> (:qualifier parsed) hooks/sexpr))
|
||||
'next-method
|
||||
'__FN__NAME__THAT__YOU__CANNOT__REFER__TO__))
|
||||
fn-tail))
|
||||
(vary-meta update :clj-kondo/ignore conj :redundant-fn-wrapper))]))]
|
||||
#_(println "=>")
|
||||
#_(clojure.pprint/pprint (hooks/sexpr result))
|
||||
{:node result})))
|
||||
|
||||
;;; this stuff is for debugging things to make sure we didn't do something dumb
|
||||
(comment
|
||||
(defn defmethod* [form]
|
||||
(binding [*print-meta* true]
|
||||
(clojure.pprint/pprint
|
||||
(hooks/sexpr (:node (defmethod {:node (hooks/parse-string (str form))}))))))
|
||||
|
||||
(defmethod* '(defmethod mf :second [& _] 2))
|
||||
|
||||
(defmethod* '(m/defmethod multi-arity :k
|
||||
([x]
|
||||
{:x x})
|
||||
([x y]
|
||||
{:x x, :y y})))
|
||||
|
||||
(defmethod* '(m/defmethod mf1 :docstring
|
||||
"Docstring"
|
||||
[_x]))
|
||||
|
||||
(defmethod* '(m/defmethod mf1 :around :dispatch-value
|
||||
"Docstring"
|
||||
[x]
|
||||
(next-method x))))
|
||||
|
||||
(defn defmulti
|
||||
[{{[_ multimethod-name & args] :children, :as node} :node}]
|
||||
#_(clojure.pprint/pprint (hooks/sexpr node))
|
||||
(let [[docstring & args] (if (hooks/string-node? (first args))
|
||||
args
|
||||
(cons nil args))
|
||||
[attribute-map & args] (if (hooks/map-node? (first args))
|
||||
args
|
||||
(cons nil args))
|
||||
;; if there wasn't a positional dispatch function arg passed just use (constantly nil) so Kondo won't complain
|
||||
[dispatch-fn & kv-options] (if (odd? (count args))
|
||||
args
|
||||
(cons (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'clojure.core/constantly)
|
||||
(hooks/token-node 'nil)))
|
||||
args))]
|
||||
(let [defmulti-form (hooks/list-node
|
||||
(filter
|
||||
some?
|
||||
[(hooks/token-node 'clojure.core/defmulti)
|
||||
multimethod-name
|
||||
docstring
|
||||
attribute-map
|
||||
dispatch-fn]))
|
||||
result (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'do)
|
||||
defmulti-form
|
||||
kv-options))]
|
||||
#_(println "=>")
|
||||
#_(clojure.pprint/pprint (hooks/sexpr result))
|
||||
{:node result})))
|
||||
@@ -0,0 +1,7 @@
|
||||
(ns macros.methodical.impl.combo.operator)
|
||||
|
||||
;; not exactly what actually happens but this is close enough to be able to lint it
|
||||
(defmacro defoperator [operator-name [methods-binding invoke-binding] & body]
|
||||
`(defmethod methodical.impl.combo.operator/operator ~(keyword operator-name)
|
||||
[~methods-binding ~invoke-binding]
|
||||
~@body))
|
||||
2
.clj-kondo/metosin/malli/config.edn
Normal file
2
.clj-kondo/metosin/malli/config.edn
Normal file
@@ -0,0 +1,2 @@
|
||||
{:lint-as {malli.experimental/defn schema.core/defn}
|
||||
:linters {:unresolved-symbol {:exclude [(malli.core/=>)]}}}
|
||||
62
.clj-kondo/potemkin/potemkin/config.edn
Normal file
62
.clj-kondo/potemkin/potemkin/config.edn
Normal file
@@ -0,0 +1,62 @@
|
||||
{:lint-as {potemkin.collections/compile-if clojure.core/if
|
||||
potemkin.collections/reify-map-type clojure.core/reify
|
||||
potemkin.collections/def-map-type clj-kondo.lint-as/def-catch-all
|
||||
potemkin.collections/def-derived-map clj-kondo.lint-as/def-catch-all
|
||||
|
||||
potemkin.types/reify+ clojure.core/reify
|
||||
potemkin.types/defprotocol+ clojure.core/defprotocol
|
||||
potemkin.types/deftype+ clojure.core/deftype
|
||||
potemkin.types/defrecord+ clojure.core/defrecord
|
||||
potemkin.types/definterface+ clojure.core/defprotocol
|
||||
potemkin.types/extend-protocol+ clojure.core/extend-protocol
|
||||
potemkin.types/def-abstract-type clj-kondo.lint-as/def-catch-all
|
||||
|
||||
potemkin.utils/doit clojure.core/doseq
|
||||
potemkin.utils/doary clojure.core/doseq
|
||||
potemkin.utils/condp-case clojure.core/condp
|
||||
potemkin.utils/fast-bound-fn clojure.core/bound-fn
|
||||
|
||||
potemkin.walk/prewalk clojure.walk/prewalk
|
||||
potemkin.walk/postwalk clojure.walk/postwalk
|
||||
potemkin.walk/walk clojure.walk/walk
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;; top-level from import-vars
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Have hooks
|
||||
;;potemkin/import-fn potemkin.namespaces/import-fn
|
||||
;;potemkin/import-macro potemkin.namespaces/import-macro
|
||||
;;potemkin/import-def potemkin.namespaces/import-def
|
||||
|
||||
;; Internal, not transitive
|
||||
;;potemkin/unify-gensyms potemkin.macros/unify-gensyms
|
||||
;;potemkin/normalize-gensyms potemkin.macros/normalize-gensyms
|
||||
;;potemkin/equivalent? potemkin.macros/equivalent?
|
||||
|
||||
potemkin/condp-case clojure.core/condp
|
||||
potemkin/doit potemkin.utils/doit
|
||||
potemkin/doary potemkin.utils/doary
|
||||
|
||||
potemkin/def-abstract-type clj-kondo.lint-as/def-catch-all
|
||||
potemkin/reify+ clojure.core/reify
|
||||
potemkin/defprotocol+ clojure.core/defprotocol
|
||||
potemkin/deftype+ clojure.core/deftype
|
||||
potemkin/defrecord+ clojure.core/defrecord
|
||||
potemkin/definterface+ clojure.core/defprotocol
|
||||
potemkin/extend-protocol+ clojure.core/extend-protocol
|
||||
|
||||
potemkin/reify-map-type clojure.core/reify
|
||||
potemkin/def-derived-map clj-kondo.lint-as/def-catch-all
|
||||
potemkin/def-map-type clj-kondo.lint-as/def-catch-all}
|
||||
|
||||
;; leave import-vars alone, kondo special-cases it
|
||||
:hooks {:macroexpand {#_#_potemkin.namespaces/import-vars potemkin.namespaces/import-vars
|
||||
potemkin.namespaces/import-fn potemkin.namespaces/import-fn
|
||||
potemkin.namespaces/import-macro potemkin.namespaces/import-macro
|
||||
potemkin.namespaces/import-def potemkin.namespaces/import-def
|
||||
|
||||
#_#_potemkin/import-vars potemkin.namespaces/import-vars
|
||||
potemkin/import-fn potemkin.namespaces/import-fn
|
||||
potemkin/import-macro potemkin.namespaces/import-macro
|
||||
potemkin/import-def potemkin.namespaces/import-def}}}
|
||||
56
.clj-kondo/potemkin/potemkin/potemkin/namespaces.clj
Normal file
56
.clj-kondo/potemkin/potemkin/potemkin/namespaces.clj
Normal file
@@ -0,0 +1,56 @@
|
||||
(ns potemkin.namespaces
|
||||
(:require [clj-kondo.hooks-api :as api]))
|
||||
|
||||
(defn import-macro*
|
||||
([sym]
|
||||
`(def ~(-> sym name symbol) ~sym))
|
||||
([sym name]
|
||||
`(def ~name ~sym)))
|
||||
|
||||
(defmacro import-fn
|
||||
([sym]
|
||||
(import-macro* sym))
|
||||
([sym name]
|
||||
(import-macro* sym name)))
|
||||
|
||||
(defmacro import-macro
|
||||
([sym]
|
||||
(import-macro* sym))
|
||||
([sym name]
|
||||
(import-macro* sym name)))
|
||||
|
||||
(defmacro import-def
|
||||
([sym]
|
||||
(import-macro* sym))
|
||||
([sym name]
|
||||
(import-macro* sym name)))
|
||||
|
||||
#_
|
||||
(defmacro import-vars
|
||||
"Imports a list of vars from other namespaces."
|
||||
[& syms]
|
||||
(let [unravel (fn unravel [x]
|
||||
(if (sequential? x)
|
||||
(->> x
|
||||
rest
|
||||
(mapcat unravel)
|
||||
(map
|
||||
#(symbol
|
||||
(str (first x)
|
||||
(when-let [n (namespace %)]
|
||||
(str "." n)))
|
||||
(name %))))
|
||||
[x]))
|
||||
syms (mapcat unravel syms)
|
||||
result `(do
|
||||
~@(map
|
||||
(fn [sym]
|
||||
(let [vr (resolve sym)
|
||||
m (meta vr)]
|
||||
(cond
|
||||
(nil? vr) `(throw (ex-info (format "`%s` does not exist" '~sym) {}))
|
||||
(:macro m) `(def ~(-> sym name symbol) ~sym)
|
||||
(:arglists m) `(def ~(-> sym name symbol) ~sym)
|
||||
:else `(def ~(-> sym name symbol) ~sym))))
|
||||
syms))]
|
||||
result))
|
||||
1
.clj-kondo/prismatic/schema/config.edn
Normal file
1
.clj-kondo/prismatic/schema/config.edn
Normal file
@@ -0,0 +1 @@
|
||||
{:lint-as {schema.test/deftest clojure.test/deftest}}
|
||||
@@ -1,52 +0,0 @@
|
||||
(ns hooks.clojure.core.defmulti
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]))
|
||||
|
||||
(defn- check-arglists [report-node arglists]
|
||||
(letfn [(reg-bad-arglists! []
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta report-node)
|
||||
:message ":arglists should be a quoted list of vectors [:metabase/check-defmulti-arglists]"
|
||||
:type :metabase/check-defmulti-arglists)))
|
||||
(reg-bad-arg! []
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta report-node)
|
||||
:message ":arglists should contain actual arg names, not underscore (unused) symbols [:metabase/check-defmulti-arglists]"
|
||||
:type :metabase/check-defmulti-arglists)))
|
||||
(underscore-arg? [arg]
|
||||
(and (symbol? arg)
|
||||
(str/starts-with? arg "_")))
|
||||
(check-arglist [arglist]
|
||||
(cond
|
||||
(not (vector? arglist)) (reg-bad-arglists!)
|
||||
(some underscore-arg? arglist) (reg-bad-arg!)))]
|
||||
(if-not (and (seq? arglists)
|
||||
(= (first arglists) 'quote)
|
||||
(seq (second arglists)))
|
||||
(reg-bad-arglists!)
|
||||
(let [[_quote arglists] arglists]
|
||||
(doseq [arglist arglists]
|
||||
(check-arglist arglist))))))
|
||||
|
||||
(defn- defmulti-check-for-arglists-metadata
|
||||
"Make sure a [[defmulti]] has an attribute map with `:arglists` metadata."
|
||||
[node]
|
||||
(let [[_defmulti _symb & args] (:children node)
|
||||
[_docstring & args] (if (hooks/string-node? (first args))
|
||||
args
|
||||
(cons nil args))
|
||||
attr-map (when (hooks/map-node? (first args))
|
||||
(first args))
|
||||
arglists (some-> attr-map hooks/sexpr :arglists seq)]
|
||||
(if-not (seq? arglists) ; should be a list or at least list-like
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta node)
|
||||
:message "All defmultis should have an attribute map with :arglists metadata. [:metabase/check-defmulti-arglists]"
|
||||
:type :metabase/check-defmulti-arglists))
|
||||
(check-arglists attr-map arglists))))
|
||||
|
||||
(defn lint-defmulti [x]
|
||||
(defmulti-check-for-arglists-metadata (:node x))
|
||||
x)
|
||||
@@ -1,76 +0,0 @@
|
||||
(ns hooks.clojure.core.defn
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]))
|
||||
|
||||
(defn- end-with-exclamation?
|
||||
[s]
|
||||
(str/ends-with? s "!"))
|
||||
|
||||
(defn- explicitly-safe? [config qualified-symbol]
|
||||
(contains? (get-in config [:linters :metabase/test-helpers-use-non-thread-safe-functions :explicitly-safe])
|
||||
qualified-symbol))
|
||||
|
||||
(defn- explicitly-unsafe? [config qualified-symbol]
|
||||
(contains? (get-in config [:linters :metabase/validate-deftest :parallel/unsafe]) qualified-symbol))
|
||||
|
||||
(defn- unsafe? [config qualified-symbol]
|
||||
(and (or (end-with-exclamation? qualified-symbol)
|
||||
(explicitly-unsafe? config qualified-symbol))
|
||||
(not (explicitly-safe? config qualified-symbol))))
|
||||
|
||||
(defn- non-thread-safe-form-should-end-with-exclamation*
|
||||
[{[defn-or-defmacro form-name] :children, :as node} config]
|
||||
(when-not (and (:string-value form-name)
|
||||
(end-with-exclamation? (:string-value form-name)))
|
||||
(letfn [(walk [f form]
|
||||
(f form)
|
||||
(doseq [child (:children form)]
|
||||
(walk f child)))
|
||||
(check-node [form]
|
||||
(when-let [qualified-symbol (hooks.common/node->qualified-symbol form)]
|
||||
(when (unsafe? config qualified-symbol)
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta form-name)
|
||||
:message (format "The name of this %s should end with `!` because it contains calls to non thread safe form `%s`. [:metabase/test-helpers-use-non-thread-safe-functions]"
|
||||
(:string-value defn-or-defmacro) qualified-symbol)
|
||||
:type :metabase/test-helpers-use-non-thread-safe-functions)))))]
|
||||
(walk check-node node))
|
||||
node))
|
||||
|
||||
(defn non-thread-safe-form-should-end-with-exclamation
|
||||
"Used to ensure defn and defmacro in test namespace to have name ending with `!` if it's non-thread-safe.
|
||||
A function or a macro can be defined as 'not thread safe' when their funciton name ends with a `!`.
|
||||
|
||||
Only used in tests to identify thread-safe/non-thread-safe test helpers. See #37126"
|
||||
[{:keys [node cljc lang config]}]
|
||||
(when (or (not cljc)
|
||||
(= lang :clj))
|
||||
(non-thread-safe-form-should-end-with-exclamation* node config))
|
||||
{:node node})
|
||||
|
||||
(comment
|
||||
(require '[clj-kondo.core :as clj-kondo])
|
||||
(def form (str '(defmacro a
|
||||
[x]
|
||||
`(fun-call x))))
|
||||
|
||||
(def form "(defmacro a
|
||||
[x]
|
||||
`(some! ~x))")
|
||||
|
||||
(def form "(defun f
|
||||
[x]
|
||||
(let [g! (fn [] 1)]
|
||||
(g!)))")
|
||||
|
||||
(str (hooks/parse-string form))
|
||||
(hooks/sexpr (hooks/parse-string form))
|
||||
|
||||
(binding [hooks/*reload* true]
|
||||
(-> form
|
||||
(with-in-str (clj-kondo/run! {:lint ["-"]}))
|
||||
:findings))
|
||||
|
||||
(do (non-thread-safe-form-should-end-with-exclamation* (hooks/parse-string form)) nil))
|
||||
@@ -1,119 +0,0 @@
|
||||
(ns hooks.clojure.core.ns
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]
|
||||
[hooks.common.modules :as modules]))
|
||||
|
||||
(defn- ns-form-node->require-node [ns-form-node]
|
||||
(some (fn [node]
|
||||
(when (and (hooks/list-node? node)
|
||||
(let [first-child (first (:children node))]
|
||||
(and (hooks/keyword-node? first-child)
|
||||
(= (hooks/sexpr first-child) :require))))
|
||||
node))
|
||||
(:children ns-form-node)))
|
||||
|
||||
(defn- lint-require-shapes [ns-form-node]
|
||||
(doseq [node (-> ns-form-node
|
||||
ns-form-node->require-node
|
||||
:children
|
||||
rest)]
|
||||
(cond
|
||||
(not (hooks/vector-node? node))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "All :required namespaces should be wrapped in vectors [:metabase/require-shape-checker]"
|
||||
:type :metabase/require-shape-checker))
|
||||
|
||||
(hooks/vector-node? (second (:children node)))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "Don't use prefix forms inside :require [:metabase/require-shape-checker]"
|
||||
:type :metabase/require-shape-checker)))))
|
||||
|
||||
(defn- lint-requires-on-new-lines [ns-form-node]
|
||||
(let [[require-keyword first-require] (-> ns-form-node
|
||||
ns-form-node->require-node
|
||||
:children)]
|
||||
(when-let [require-keyword-line (:row (meta require-keyword))]
|
||||
(when-let [first-require-line (:row (meta first-require))]
|
||||
(when (= require-keyword-line first-require-line)
|
||||
(hooks/reg-finding! (assoc (meta first-require)
|
||||
:message "Put your requires on a newline from the :require keyword [:metabase/require-shape-checker]"
|
||||
:type :metabase/require-shape-checker)))))))
|
||||
|
||||
(defn- require-node->namespace-symb-nodes [require-node]
|
||||
(let [[_ns & args] (:children require-node)]
|
||||
(into []
|
||||
;; prefixed namespace forms are NOT SUPPORTED!!!!!!!!1
|
||||
(keep (fn [node]
|
||||
(cond
|
||||
(hooks/vector-node? node)
|
||||
;; propagate the metadata attached to this vector in case there's a `:clj-kondo/ignore` form.
|
||||
(let [symbol-node (first (:children node))]
|
||||
(hooks.common/merge-ignored-linters symbol-node require-node node))
|
||||
|
||||
;; this should also be dead code since we require requires to be vectors
|
||||
(hooks/token-node? node)
|
||||
(hooks.common/merge-ignored-linters node require-node)
|
||||
|
||||
:else
|
||||
(printf "Don't know how to figure out what namespace is being required in %s\n" (pr-str node)))))
|
||||
args)))
|
||||
|
||||
(defn- ns-form-node->ns-symb-node [ns-form-node]
|
||||
(some (fn [node]
|
||||
(when (and (hooks/token-node? node)
|
||||
(not= (hooks/sexpr node) 'ns))
|
||||
node))
|
||||
(:children ns-form-node)))
|
||||
|
||||
(defn- ns-form-node->ns-symb [ns-form-node]
|
||||
(some-> (ns-form-node->ns-symb-node ns-form-node)
|
||||
hooks/sexpr))
|
||||
|
||||
(defn- lint-modules [ns-form-node config]
|
||||
(let [ns-symb (ns-form-node->ns-symb ns-form-node)]
|
||||
(when-not (modules/ignored-namespace? config ns-symb)
|
||||
(when-let [current-module (modules/module ns-symb)]
|
||||
(let [required-namespace-symb-nodes (-> ns-form-node
|
||||
ns-form-node->require-node
|
||||
require-node->namespace-symb-nodes)]
|
||||
(doseq [node required-namespace-symb-nodes
|
||||
:when (not (contains? (hooks.common/ignored-linters node) :metabase/modules))
|
||||
:let [required-namespace (hooks/sexpr node)
|
||||
error (modules/usage-error config current-module required-namespace)]
|
||||
:when error]
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message error
|
||||
:type :metabase/modules))))))))
|
||||
|
||||
(defn- lint-namespace-name [ns-form-node]
|
||||
(when-let [ns-symb-node (ns-form-node->ns-symb-node ns-form-node)]
|
||||
(when-not (contains? (hooks.common/ignored-linters ns-symb-node) :metabase/namespace-name)
|
||||
(let [parts (str/split (str (hooks/sexpr ns-symb-node)) #"\.")]
|
||||
(when (and (<= (count parts) 2)
|
||||
(#{"metabase" "metabase-enterprise"} (first parts))
|
||||
;; exclude test namespaces for now
|
||||
(not (str/ends-with? (last parts) "test")))
|
||||
(hooks/reg-finding! (assoc (meta ns-symb-node)
|
||||
:message "Metabase namespaces should have the form metabase[-enterprise].<module>.* [:metabase/namespace-name]"
|
||||
:type :metabase/namespace-name)))))))
|
||||
|
||||
(defn lint-ns [x]
|
||||
(doto (:node x)
|
||||
lint-require-shapes
|
||||
lint-requires-on-new-lines
|
||||
(lint-modules (modules/config x))
|
||||
lint-namespace-name)
|
||||
x)
|
||||
|
||||
(comment
|
||||
(lint-ns
|
||||
{:node (hooks/parse-string
|
||||
(pr-str
|
||||
'(ns hooks.clojure.core.ns
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]
|
||||
[hooks.common.modules :as modules]))))}))
|
||||
@@ -1,75 +0,0 @@
|
||||
(ns hooks.clojure.core.require
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common.modules :as modules]))
|
||||
|
||||
(defn- lint* [node current-ns config required-namespaces]
|
||||
{:pre [(map? node)
|
||||
(simple-symbol? current-ns)
|
||||
(map? config)
|
||||
(sequential? required-namespaces)
|
||||
(every? simple-symbol? required-namespaces)]}
|
||||
(when-not (modules/ignored-namespace? config current-ns)
|
||||
;; ignore namespaces outside of the module system
|
||||
(when-let [current-module (modules/module current-ns)]
|
||||
(doseq [required-namespace required-namespaces]
|
||||
;; ignore namespaces outside of the module system.
|
||||
(when (modules/module required-namespace)
|
||||
(when-let [error (modules/usage-error config current-module required-namespace)]
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message error
|
||||
:type :metabase/modules))))))))
|
||||
|
||||
(defn- unwrap-require [node]
|
||||
(case (hooks/tag node)
|
||||
;; some.namespace of some.namespace/some-var
|
||||
:token
|
||||
(let [sexpr (hooks/sexpr node)]
|
||||
(if (qualified-symbol? sexpr)
|
||||
(symbol (namespace sexpr))
|
||||
sexpr))
|
||||
|
||||
;; (quote some.namespace)
|
||||
:list
|
||||
(let [[x y] (:children node)]
|
||||
(when (and (hooks/token-node? x)
|
||||
(= (hooks/sexpr x) 'quote))
|
||||
(recur y)))
|
||||
|
||||
;; 'some.namespace
|
||||
:quote
|
||||
(recur (first (:children node)))
|
||||
|
||||
;; something like [some.namespace :as whatever]
|
||||
:vector
|
||||
(recur (first (:children node)))))
|
||||
|
||||
(comment
|
||||
(unwrap-require (-> "(requiring-resolve 'metabase.notification.payload.execute/execute-dashboard)"
|
||||
hooks/parse-string
|
||||
:children
|
||||
second)))
|
||||
|
||||
(defn- lint-require* [node current-ns config]
|
||||
(let [[_require & args] (:children node)
|
||||
required-namespaces (->> (keep unwrap-require args)
|
||||
;; `require` runs through `cc/load-libs` which allows
|
||||
;; #{:as :reload :reload-all :require :use :verbose :refer :as-alias}
|
||||
;; as args. If you use a REPL tool that applies clj-kondo linting (e.g., Clojure MCP)
|
||||
;; you may wish to `:reload` or `:reload-all` a namespace. So we shouldn't assume all
|
||||
;; args are keywords:
|
||||
(remove keyword?))]
|
||||
(lint* node current-ns config required-namespaces)))
|
||||
|
||||
(defn- lint-requiring-resolve* [node current-ns config]
|
||||
(let [[_requiring-resolve symb-node] (:children node)
|
||||
required-namespace (unwrap-require symb-node)]
|
||||
(lint* node current-ns config [required-namespace])))
|
||||
|
||||
(defn lint-require [x]
|
||||
(lint-require* (:node x) (:ns x) (modules/config x))
|
||||
x)
|
||||
|
||||
(defn lint-requiring-resolve [x]
|
||||
(lint-requiring-resolve* (:node x) (:ns x) (modules/config x))
|
||||
x)
|
||||
@@ -1,156 +0,0 @@
|
||||
(ns hooks.clojure.test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]))
|
||||
|
||||
(defn- warn-about-disallowed-parallel-forms [form config]
|
||||
(letfn [(error! [form message]
|
||||
(hooks/reg-finding! (assoc (meta form)
|
||||
:message message
|
||||
:type :metabase/validate-deftest)))
|
||||
(f [form]
|
||||
(when-let [qualified-symbol (hooks.common/node->qualified-symbol form)]
|
||||
(cond
|
||||
(contains? (:parallel/unsafe config) qualified-symbol)
|
||||
(error! form (format "%s is not allowed inside a ^:parallel test or test fixture [:metabase/validate-deftest]" qualified-symbol))
|
||||
|
||||
(and (not (contains? (:parallel/safe config) qualified-symbol))
|
||||
(str/ends-with? (name qualified-symbol) "!"))
|
||||
(error! form (format "destructive functions like %s are not allowed inside a ^:parallel test or test fixture. If this should be allowed, add it to the whitelist in the Kondo config file [:metabase/validate-deftest]"
|
||||
qualified-symbol)))))
|
||||
(walk [form]
|
||||
(f form)
|
||||
(doseq [child (:children form)]
|
||||
(walk child)))]
|
||||
(walk form)))
|
||||
|
||||
(defn- deftest-check-parallel
|
||||
"1. Check if test is marked ^:parallel / ^:synchronized correctly
|
||||
2. Make sure disallowed forms are not used in ^:parallel tests"
|
||||
[{[_ test-name & body] :children, :as _node} config]
|
||||
(let [test-metadata (:meta test-name)
|
||||
metadata-sexprs (map hooks/sexpr test-metadata)
|
||||
combined-metadata (transduce
|
||||
(map (fn [x]
|
||||
(if (map? x)
|
||||
x
|
||||
{x true})))
|
||||
(completing merge)
|
||||
{}
|
||||
metadata-sexprs)
|
||||
parallel? (:parallel combined-metadata)
|
||||
synchronized? (:synchronized combined-metadata)]
|
||||
(when (and parallel? synchronized?)
|
||||
(hooks/reg-finding! (assoc (meta test-name)
|
||||
:message "Test should not be marked both ^:parallel and ^:synchronized"
|
||||
:type :metabase/validate-deftest)))
|
||||
;; only when the custom `:metabase/deftest-not-marked-parallel-or-synchronized` is enabled: complain if tests are
|
||||
;; not explicitly marked `^:parallel` or `^:synchronized`. This is mostly to encourage people to mark everything
|
||||
;; `^:parallel` in places like `metabase.lib` tests unless there is a really good reason not to.
|
||||
(when-not (or parallel? synchronized?)
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta test-name)
|
||||
:message "Test should be marked either ^:parallel or ^:synchronized"
|
||||
:type :metabase/deftest-not-marked-parallel-or-synchronized)))
|
||||
(when parallel?
|
||||
(doseq [form body]
|
||||
(warn-about-disallowed-parallel-forms form config)))))
|
||||
|
||||
(defn- deftest-check-not-horrifically-long
|
||||
[node {:keys [max-length], :as _config}]
|
||||
(let [{:keys [row end-row]} (meta node)]
|
||||
(when (and row end-row)
|
||||
(let [num-lines (- end-row row)]
|
||||
(when (and max-length
|
||||
(>= num-lines max-length))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message (str (format "This test is horrifically long, it's %d lines! 😱 " num-lines)
|
||||
"Do you really want to try to debug it if it fails? 💀 "
|
||||
"Split it up into smaller tests! 🥰")
|
||||
:type :metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests)))))))
|
||||
|
||||
(defn- ignore? [node error-type]
|
||||
(contains? (hooks.common/ignored-linters node) error-type))
|
||||
|
||||
(defn deftest-check-no-driver-keywords [node {:keys [drivers], :as _config}]
|
||||
;; fail fast after we see the first error, where we see one hardcoded driver name there are likely several more and we
|
||||
;; don't need multiple warnings for a single test.
|
||||
(letfn [(f [node]
|
||||
(when (and (hooks/keyword-node? node)
|
||||
(set? drivers)
|
||||
(contains? drivers (hooks/sexpr node)))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message (format "Do not hardcode driver name %s in driver tests! [:metabase/disallow-hardcoded-driver-names-in-tests]"
|
||||
(hooks/sexpr node))
|
||||
:type :metabase/disallow-hardcoded-driver-names-in-tests))
|
||||
::error))
|
||||
(walk [node]
|
||||
(cond
|
||||
(ignore? node :metabase/disallow-hardcoded-driver-names-in-tests)
|
||||
nil
|
||||
|
||||
(= (f node) ::error)
|
||||
::error
|
||||
|
||||
:else
|
||||
(reduce
|
||||
(fn [_acc child]
|
||||
(when (= (walk child) ::error)
|
||||
(reduced ::error)))
|
||||
nil
|
||||
(:children node))))]
|
||||
(walk node)))
|
||||
|
||||
(defn deftest [{:keys [node cljc lang config]}]
|
||||
;; run [[deftest-check-parallel]] only once... if this is a `.cljc` file only run it for the `:clj` analysis, no point
|
||||
;; in running it twice.
|
||||
(when (or (not cljc)
|
||||
(= lang :clj))
|
||||
(deftest-check-parallel node (get-in config [:linters :metabase/validate-deftest])))
|
||||
(deftest-check-not-horrifically-long node (get-in config [:linters :metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests]))
|
||||
(deftest-check-no-driver-keywords node (get-in config [:linters :metabase/disallow-hardcoded-driver-names-in-tests]))
|
||||
{:node node})
|
||||
|
||||
;;; this is a hacky way to determine whether these namespaces are required in the `ns` form or not... basically `:ns`
|
||||
;;; will come back as `nil` if they are not.
|
||||
(defn- approximately-equal-ns-required? []
|
||||
(= (:ns (hooks/resolve {:name 'metabase.test-runner.assert-exprs.approximately-equal/=?-report}))
|
||||
'metabase.test-runner.assert-exprs.approximately-equal))
|
||||
|
||||
(defn- malli-equals-ns-required? []
|
||||
(= (:ns (hooks/resolve {:name 'metabase.test-runner.assert-exprs.malli-equals/malli=-report}))
|
||||
'metabase.test-runner.assert-exprs.malli-equals))
|
||||
|
||||
(defn- warn-about-missing-test-expr-requires-in-cljs [{:keys [children], :as _is-node}]
|
||||
(let [[_is assertion-node] children]
|
||||
(when (hooks/list-node? assertion-node)
|
||||
(let [[assertion-symb-node] (:children assertion-node)]
|
||||
(when (hooks/token-node? assertion-symb-node)
|
||||
(let [assertion-symb-token (hooks/sexpr assertion-symb-node)
|
||||
warn! (fn [ns-to-require]
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta assertion-symb-node)
|
||||
:message (format "You must require %s to use %s in ClojureScript"
|
||||
ns-to-require
|
||||
assertion-symb-token)
|
||||
:type :metabase/missing-test-expr-requires-in-cljs)))]
|
||||
(condp = assertion-symb-token
|
||||
'=?
|
||||
(when-not (approximately-equal-ns-required?)
|
||||
(warn! 'metabase.test-runner.assert-exprs.approximately-equal))
|
||||
|
||||
'malli=
|
||||
(when-not (malli-equals-ns-required?)
|
||||
(warn! 'metabase.test-runner.assert-exprs.malli-equals))
|
||||
|
||||
nil)))))))
|
||||
|
||||
(defn is [{:keys [node lang]}]
|
||||
(when (= lang :cljs)
|
||||
(warn-about-missing-test-expr-requires-in-cljs node))
|
||||
{:node node})
|
||||
|
||||
(defn use-fixtures [{:keys [node config]}]
|
||||
(warn-about-disallowed-parallel-forms node config)
|
||||
{:node node})
|
||||
@@ -1,537 +0,0 @@
|
||||
(ns hooks.common
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.pprint]
|
||||
[clojure.set :as set]))
|
||||
|
||||
(defn with-macro-meta
|
||||
"When introducing internal nodes (let, defn, etc) it is important to provide a meta of an existing token
|
||||
as the current version of kondo will use the whole form by default."
|
||||
[new-node hook-node]
|
||||
(with-meta new-node (meta (first (:children hook-node)))))
|
||||
|
||||
;;; Kondo stores information about ignored linters as rewrite-clj forms matching how they are specified in the code, so
|
||||
;;; either
|
||||
;;;
|
||||
;;; <vector-node [<token-node :discouraged-namespace>]>
|
||||
;;;
|
||||
;;; or
|
||||
;;;
|
||||
;;; <token-node :discouraged-namespace>
|
||||
;;;
|
||||
;;; these are stored in
|
||||
;;;
|
||||
;;; (-> node meta :clj-kondo/ignore :linters).
|
||||
;;;
|
||||
;;; These are not a convenient shape(s) to work with, so the next few functions abstract that shape out and work with a
|
||||
;;; set of keywords like
|
||||
;;;
|
||||
;;; #{:discouraged-namespace}
|
||||
;;;
|
||||
;;; instead.
|
||||
|
||||
(defn- unwrap-ignored-linters [linters]
|
||||
(cond
|
||||
(or (hooks/vector-node? linters)
|
||||
(hooks/list-node? linters))
|
||||
(into #{}
|
||||
(map hooks/sexpr)
|
||||
(:children linters))
|
||||
|
||||
(hooks/keyword-node? linters)
|
||||
#{(hooks/sexpr linters)}
|
||||
|
||||
(and (set? linters)
|
||||
(every? keyword? linters))
|
||||
linters
|
||||
|
||||
(and (sequential? linters)
|
||||
(every? keyword? linters))
|
||||
(into #{} linters)
|
||||
|
||||
(keyword? linters)
|
||||
#{linters}))
|
||||
|
||||
(defn- wrap-ignored-linters [linters]
|
||||
(hooks/vector-node (mapv hooks/keyword-node linters)))
|
||||
|
||||
(defn ignored-linters
|
||||
"Return a set of keywords all ignored linters associated with a node by looking at its metadata."
|
||||
[node]
|
||||
(or (some-> node meta :clj-kondo/ignore :linters unwrap-ignored-linters)
|
||||
;; deal with the old pre-2024.09 shape mentioned in https://github.com/clj-kondo/clj-kondo/issues/2414
|
||||
(some-> node :meta first hooks/sexpr :clj-kondo/ignore unwrap-ignored-linters)))
|
||||
|
||||
(defn update-ignored-linters
|
||||
"Update the `:clj-kondo/ignore` linters in `node`'s metadata. `linters` should be a set of keywords."
|
||||
[node f & args]
|
||||
(letfn [(apply-f [linters]
|
||||
(apply f linters args))
|
||||
(update-linters [linters]
|
||||
(-> (or (unwrap-ignored-linters linters)
|
||||
#{})
|
||||
apply-f
|
||||
wrap-ignored-linters))]
|
||||
(vary-meta node update-in [:clj-kondo/ignore :linters] update-linters)))
|
||||
|
||||
(defn merge-ignored-linters
|
||||
"Merge the ignored linters from `source-nodes` into `node`. You can use this to propagate
|
||||
ignored linters from parent nodes to child nodes."
|
||||
[node & source-nodes]
|
||||
(if-let [linters (not-empty (transduce (map ignored-linters) set/union #{} source-nodes))]
|
||||
(update-ignored-linters node set/union linters)
|
||||
node))
|
||||
|
||||
;;; This stuff is to help debug hooks. Trace a function and it will pretty-print the before & after sexprs.
|
||||
;;;
|
||||
;;; (hooks.common/trace #'calculate-bird-scarcity)
|
||||
|
||||
#_{:clj-kondo/ignore [:discouraged-var]}
|
||||
(defn- trace* [f]
|
||||
{:pre [(fn? f)]}
|
||||
(fn traced-fn [node]
|
||||
(println \newline)
|
||||
(clojure.pprint/pprint (hooks/sexpr (:node node)))
|
||||
(let [node* (f node)]
|
||||
(println '=>)
|
||||
(clojure.pprint/pprint (hooks/sexpr (:node node*)))
|
||||
node*)))
|
||||
|
||||
;;; this doesn't seem to work with SCI. Use [[defn-traced]] instead.
|
||||
|
||||
;; (defn trace [varr]
|
||||
;; {:pre [(var? varr)]}
|
||||
;; (when-not (::traced? (meta varr))
|
||||
;; (alter-var-root varr trace*)
|
||||
;; (alter-meta! varr assoc ::traced? true)
|
||||
;; (println "Traced" varr)))
|
||||
|
||||
(defmacro defn-traced
|
||||
"Replace a hook [[defn]] with this to trace the before and after sexprs."
|
||||
[fn-name & fn-tail]
|
||||
`(do
|
||||
(defn fn# ~@fn-tail)
|
||||
(def ~fn-name (trace* fn#))))
|
||||
|
||||
;;;; Common hook definitions
|
||||
|
||||
(defn do*
|
||||
"This is the same idea as [[clojure.core/do]] but doesn't cause Kondo to complain about redundant dos or unused
|
||||
values."
|
||||
[{{[_ & args] :children, :as node} :node}]
|
||||
(let [node* (-> (hooks/list-node
|
||||
(list*
|
||||
(update-ignored-linters (hooks/token-node 'do) conj :redundant-do)
|
||||
(for [arg args]
|
||||
(update-ignored-linters arg conj :unused-value))))
|
||||
(with-meta (meta node)))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-one-binding
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [x]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil]
|
||||
...)
|
||||
|
||||
Binding is optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[x] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-two-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [x y]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil, y nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[x y] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or y (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-three-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [x y z]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil, y nil, z nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[x y z] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or y (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or z (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-four-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [a b c d]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [a nil, b nil, c nil, d nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[a b c d] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or d (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-five-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [a b c d e]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [a nil, b nil, c nil, d nil, e nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[a b c d e] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or e (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-six-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [a b c d e f]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [a nil, b nil, c nil, d nil, e nil, f nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[a b c d e f] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or e (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or f (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-seven-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro [a b c d e f g]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [a nil, b nil, c nil, d nil, e nil, f nil, g nil]
|
||||
...)
|
||||
|
||||
All bindings are optional and `_` will be substituted if not supplied."
|
||||
[{{[_ {[a b c d e f g] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or e (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or f (hooks/token-node '_)) (hooks/token-node 'nil)
|
||||
(or g (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-one-top-level-binding
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro x
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil]
|
||||
...)"
|
||||
[{{[_ x & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[x (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-two-top-level-bindings
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro x y
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [x nil, y nil]
|
||||
...)"
|
||||
[{{[_ x y & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[x (hooks/token-node 'nil)
|
||||
y (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn let-one-with-optional-value
|
||||
"This is exactly like [[clojure.core/let]] but the right-hand side of the binding, `value`, is optional, and only one
|
||||
binding can be established.
|
||||
|
||||
(some-macro [x] ...)
|
||||
=>
|
||||
(let [x nil] ...)
|
||||
|
||||
(some-macro [x 100] ...)
|
||||
=>
|
||||
(let [x 100] ...)"
|
||||
[{{[_ {[binding value] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[binding (or value (hooks/token-node 'nil))])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn- let-second-inner [body bindings]
|
||||
(let [binding-infos (for [[model {[binding value] :children}] (partition 2 bindings)]
|
||||
{:model model
|
||||
:binding binding
|
||||
:value (or value
|
||||
(hooks/token-node 'nil))})]
|
||||
(-> (hooks/vector-node
|
||||
[(hooks/vector-node (map :model binding-infos))
|
||||
(-> (hooks/list-node (list* (hooks/token-node `let)
|
||||
(hooks/vector-node (mapcat (juxt :binding :value) binding-infos))
|
||||
body))
|
||||
(with-meta (meta body)))])
|
||||
(with-meta (meta body)))))
|
||||
|
||||
(defn let-second
|
||||
"Helper for macros that have a shape like
|
||||
|
||||
(my-macro x [y]
|
||||
...)
|
||||
|
||||
where the let is for second arg.
|
||||
|
||||
=>
|
||||
|
||||
(let [y nil]
|
||||
...)"
|
||||
[{:keys [node]}]
|
||||
(let [[_ first-arg-ref binding+opts & body] (:children node)]
|
||||
{:node (let-second-inner body [first-arg-ref binding+opts])}))
|
||||
|
||||
(defn let-with-optional-value-for-last-binding
|
||||
"This is exactly like [[clojure.core/let]] but the right-hand side of the *last* binding, `value`, is optional.
|
||||
|
||||
(some-macro [x] ...)
|
||||
=>
|
||||
(let [x nil] ...)
|
||||
|
||||
(some-macro [x 100] ...)
|
||||
=>
|
||||
(let [x 100] ...)"
|
||||
[{{[_ {bindings :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[binding value]]
|
||||
[binding (or value (hooks/token-node 'nil))])))
|
||||
bindings))
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-ignored-first-arg
|
||||
"For macros like
|
||||
|
||||
(discard-setting-changes [setting-1 setting-2]
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(do ...)
|
||||
|
||||
where the first arg ought to be ignored for linting purposes."
|
||||
[{{[_ _x & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'do)
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-used-first-arg
|
||||
"For macros like
|
||||
|
||||
(with-drivers (filter pred? some-drivers)
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [_1234 (filter pred? some-drivers)]
|
||||
...)
|
||||
|
||||
where the first arg should be linted and appear to be used."
|
||||
[{{[_ arg & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node [(hooks/token-node (gensym "_"))
|
||||
arg])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-used-first-two-args
|
||||
"For macros like
|
||||
|
||||
(with-limit-and-offset limit offset
|
||||
...)
|
||||
|
||||
=>
|
||||
|
||||
(let [_123 limit
|
||||
[_456 offset]
|
||||
...)
|
||||
|
||||
where the first arg should be linted and appear to be used."
|
||||
[{{[_ x y & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node [(hooks/token-node (gensym "_")) x
|
||||
(hooks/token-node (gensym "_")) y])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn with-vec-first-binding
|
||||
"For macros like
|
||||
|
||||
(with-temp-file [binding \"temp.txt\" \"content\"]
|
||||
(slurp binding))
|
||||
|
||||
=>
|
||||
(let [binding nil]
|
||||
(slurp binding))
|
||||
|
||||
where the first arg is a vector of bindings."
|
||||
[{{[_ {[x & _args] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn node->qualified-symbol
|
||||
"If the node is a symbol, return it as a qualified symbol if it's not already. Otherwise return nil."
|
||||
[node]
|
||||
(try
|
||||
(when (hooks/token-node? node)
|
||||
(let [sexpr (hooks/sexpr node)]
|
||||
(when (symbol? sexpr)
|
||||
(let [resolved (hooks/resolve {:name sexpr})]
|
||||
(cond
|
||||
(and resolved (:ns resolved))
|
||||
(symbol (name (:ns resolved)) (name (:name resolved)))
|
||||
|
||||
;; if it wasn't resolved but is still qualified it's probably using the full namespace name rather than an
|
||||
;; alias.
|
||||
(qualified-symbol? sexpr)
|
||||
sexpr)))))
|
||||
;; some symbols like `*count/Integer` aren't resolvable.
|
||||
(catch Exception _
|
||||
nil)))
|
||||
|
||||
(defn format-string-specifier-count
|
||||
"Number of things like `%s` in a format string, not counting newlines (`%n`) or escaped percent signs (`%%`). For
|
||||
checking the number of args to something that takes a format string."
|
||||
[format-string]
|
||||
(count (re-seq #"(?<!%)%(?![%n])" format-string)))
|
||||
|
||||
(comment
|
||||
;; should be 1
|
||||
(format-string-specifier-count "%s %%")
|
||||
|
||||
;; should be 2
|
||||
(format-string-specifier-count "%s %%%n%s")
|
||||
|
||||
;; should be 0
|
||||
(format-string-specifier-count "%n%%%%")
|
||||
|
||||
;; should be 1
|
||||
(format-string-specifier-count "%-02d"))
|
||||
|
||||
(defn add-lsp-ignore-unused-public-var-metadata
|
||||
"Add
|
||||
|
||||
^{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
|
||||
metadata to a node to suppress LSP warnings."
|
||||
[node]
|
||||
(update-ignored-linters node conj :clojure-lsp/unused-public-var))
|
||||
@@ -1,81 +0,0 @@
|
||||
(ns hooks.common.modules)
|
||||
|
||||
(defn ignored-namespace? [config ns-symb]
|
||||
(some
|
||||
(fn [pattern-str]
|
||||
(re-find (re-pattern pattern-str) (str ns-symb)))
|
||||
(:ignored-namespace-patterns config)))
|
||||
|
||||
(defn config [{:keys [config], :as _hook-input}]
|
||||
(merge (get-in config [:linters :metabase/modules])
|
||||
(select-keys config [:metabase/modules])))
|
||||
|
||||
(defn module
|
||||
"E.g.
|
||||
|
||||
(module 'metabase.qp.middleware.wow) => 'qp
|
||||
(module 'metabase-enterprise.whatever.core) => enterprise/whatever"
|
||||
[ns-symb]
|
||||
{:pre [(simple-symbol? ns-symb)]}
|
||||
(or (some->> (re-find #"^metabase-enterprise\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
(symbol "enterprise"))
|
||||
(some-> (re-find #"^metabase\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
symbol)))
|
||||
|
||||
(defn module-api-namespaces
|
||||
"Set of API namespace symbols for a given module. `:any` means you can use anything, there are no API namespaces for
|
||||
this module (yet). If unspecified, the default is just the `<module>.core` namespace."
|
||||
[config module]
|
||||
(let [module-config (get-in config [:metabase/modules module :api])]
|
||||
(cond
|
||||
(= module-config :any)
|
||||
nil
|
||||
|
||||
(set? module-config)
|
||||
module-config
|
||||
|
||||
:else
|
||||
(let [ns-prefix (if (= (namespace module) "enterprise")
|
||||
(str "metabase-enterprise." (name module))
|
||||
(name module))]
|
||||
#{(symbol (str ns-prefix ".api"))
|
||||
(symbol (str ns-prefix ".core"))
|
||||
(symbol (str ns-prefix ".init"))}))))
|
||||
|
||||
(defn allowed-modules
|
||||
"Set of namespace symbols that `module` is allowed to use. `:any` means it's allowed to use anything."
|
||||
[config module]
|
||||
(get-in config [:metabase/modules module :uses]))
|
||||
|
||||
(defn allowed-module? [config module required-module]
|
||||
(let [allowed-modules (allowed-modules config module)]
|
||||
(or (= allowed-modules :any)
|
||||
(contains? (set allowed-modules) required-module))))
|
||||
|
||||
(defn allowed-module-namespace? [config ns-symb]
|
||||
(let [module (module ns-symb)
|
||||
module-api-namespaces (module-api-namespaces config module)]
|
||||
(or (empty? module-api-namespaces)
|
||||
(contains? module-api-namespaces ns-symb))))
|
||||
|
||||
(defn usage-error
|
||||
"Find usage errors when a `required-namespace` is required in the `current-module`. Returns a string describing the
|
||||
error type if there is one, otherwise `nil` if there are no errors."
|
||||
[config current-module required-namespace]
|
||||
;; ignore stuff not in a module i.e. non-Metabase stuff.
|
||||
(when-let [required-module (module required-namespace)]
|
||||
(when-not (= current-module required-module)
|
||||
(cond
|
||||
(not (allowed-module? config current-module required-module))
|
||||
(format "Module %s should not be used in the %s module. [:metabase/modules %s :uses]"
|
||||
required-module
|
||||
current-module
|
||||
current-module)
|
||||
|
||||
(not (allowed-module-namespace? config required-namespace))
|
||||
(format "Namespace %s is not an allowed external API namespace for the %s module. [:metabase/modules %s :api]"
|
||||
required-namespace
|
||||
required-module
|
||||
required-module)))))
|
||||
@@ -1,26 +0,0 @@
|
||||
(ns hooks.malli.error
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common]))
|
||||
|
||||
(defn humanize [x]
|
||||
(letfn [(check-node [node]
|
||||
(when (hooks/list-node? node)
|
||||
(let [[_ child] (:children node)]
|
||||
(when (hooks/list-node? child)
|
||||
(let [[symb] (:children child)]
|
||||
(when-let [qualified-symb (hooks.common/node->qualified-symbol symb)]
|
||||
(when ('#{malli.core/validate metabase.util.malli.registry/validate}
|
||||
qualified-symb)
|
||||
(hooks/reg-finding! (assoc (meta symb)
|
||||
:message "Use malli.error/humanize with explain, NOT with validate. [:metabase/check-me-humanize]"
|
||||
:type :metabase/check-me-humanize)))))))))]
|
||||
(check-node (:node x))
|
||||
x))
|
||||
|
||||
(comment
|
||||
(humanize {:node (hooks/parse-string
|
||||
(pr-str '(malli.error/humanize
|
||||
(malli.core/validate
|
||||
metabase.sync.sync-metadata.dbms-version/DBMSVersion
|
||||
metabase.driver.druid.sync-test/dbms-version))))}))
|
||||
@@ -1,56 +0,0 @@
|
||||
(ns hooks.metabase.api.macros
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[hooks.common]))
|
||||
|
||||
(defn defendpoint
|
||||
[arg]
|
||||
(letfn [(update-defendpoint [node]
|
||||
(let [[defendpoint method route & more] (:children node)
|
||||
[result-schema & more] (if (= (api/sexpr (first more)) :-)
|
||||
(drop 1 more)
|
||||
(cons nil more))
|
||||
[_docstring & more] (if (api/string-node? (first more))
|
||||
more
|
||||
(cons nil more))
|
||||
[metadata & more] (if (api/map-node? (first more))
|
||||
more
|
||||
(cons nil more))
|
||||
[params & body] more
|
||||
[bindings schemas] (when (api/vector-node? params)
|
||||
(loop [bindings [], schemas [], [x y & more] (:children params)]
|
||||
(cond
|
||||
(not x)
|
||||
[bindings schemas]
|
||||
|
||||
(= (api/sexpr x) :-)
|
||||
(recur bindings (conj schemas y) more)
|
||||
|
||||
:else
|
||||
(recur (conj bindings x) schemas (cons y more)))))]
|
||||
(-> (api/list-node
|
||||
(list
|
||||
(api/token-node 'do)
|
||||
defendpoint
|
||||
method
|
||||
route
|
||||
(api/list-node
|
||||
(list*
|
||||
(api/token-node 'do)
|
||||
(filter some? (list* metadata result-schema schemas))))
|
||||
(api/list-node
|
||||
(list*
|
||||
(api/token-node `let)
|
||||
(api/vector-node (into []
|
||||
cat
|
||||
[(mapcat (fn [a-binding]
|
||||
[a-binding (api/map-node {})])
|
||||
(take 4 bindings))
|
||||
(mapcat (fn [a-binding]
|
||||
[a-binding (api/list-node (list
|
||||
(api/token-node 'clojure.core/constantly)
|
||||
(api/token-node 'nil)))])
|
||||
(drop 4 bindings))]))
|
||||
body))))
|
||||
(with-meta (meta node)))))]
|
||||
(update arg :node update-defendpoint)))
|
||||
@@ -1,14 +0,0 @@
|
||||
(ns hooks.metabase.app-db.schema-migrations-test.impl
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn test-migrations
|
||||
[{{[_ migration-range {[binding] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node `let)
|
||||
(hooks/vector-node
|
||||
[binding (hooks/list-node (list (hooks/token-node `fn)
|
||||
(hooks/vector-node [(hooks/token-node '&) (hooks/token-node '_args)])))])
|
||||
migration-range
|
||||
body))]
|
||||
{:node node*}))
|
||||
@@ -1,25 +0,0 @@
|
||||
(ns hooks.metabase.channel.template.handlebars-helper
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common]))
|
||||
|
||||
(defn defhelper
|
||||
"defhelper hook"
|
||||
[{:keys [node]}]
|
||||
(let [[_ helper-name description argvec & body] (:children node)
|
||||
helper-fn-name (symbol (str (hooks/sexpr helper-name) "*"))]
|
||||
{:node (-> (hooks/list-node
|
||||
(list (hooks/token-node 'do)
|
||||
(hooks/list-node
|
||||
(list (hooks/token-node 'defn)
|
||||
(hooks/token-node helper-fn-name)
|
||||
description
|
||||
argvec
|
||||
(hooks/list-node body)))
|
||||
(hooks/list-node
|
||||
(list (hooks/token-node 'defn)
|
||||
helper-name
|
||||
description
|
||||
argvec
|
||||
(hooks/list-node body)))))
|
||||
(with-meta (meta node)))}))
|
||||
@@ -1,18 +0,0 @@
|
||||
(ns hooks.metabase.driver
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]))
|
||||
|
||||
(defn register!
|
||||
"Register the driver keyword so LSP can jump to it."
|
||||
[x]
|
||||
(letfn [(update-driver-keyword [k]
|
||||
(api/reg-keyword! k 'metabase.driver/register!))
|
||||
(update-children [[_register! k :as children]]
|
||||
(if (api/keyword-node? k)
|
||||
(update (vec children) 1 update-driver-keyword)
|
||||
children))
|
||||
(update-node [node]
|
||||
(if (api/list-node? node)
|
||||
(update node :children update-children)
|
||||
node))]
|
||||
(update x :node update-node)))
|
||||
@@ -1,18 +0,0 @@
|
||||
(ns hooks.metabase.driver.mongo-test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common]))
|
||||
|
||||
(defn with-describe-table-for-sample
|
||||
"Forward bindings for results of functions used in mongo's describe-table implementation."
|
||||
[{{[_ _ & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[(hooks/token-node 'dbfields) (hooks/token-node '(delay nil))
|
||||
(hooks/token-node 'ftree) (hooks/token-node '(delay nil))
|
||||
(hooks/token-node 'nested-fields) (hooks/token-node '(delay nil))])
|
||||
body))]
|
||||
;; Avoid warnings when not all from the dbfields, ftree and nested-fields are used.
|
||||
{:node (hooks.common/update-ignored-linters node* conj :unused-binding)}))
|
||||
@@ -1,69 +0,0 @@
|
||||
(ns hooks.metabase.legacy-mbql.schemas.macros
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn- unwrap-defclause-clause-name
|
||||
"The `clause-name-form` can be either a plain symbol, or a vector like `[var-name mbql-clause-name]`. For the vector
|
||||
form, we are only interested in the `var-name`. For a plain symbol, the two names are the same. Return a token node."
|
||||
[clause-name-form]
|
||||
(-> (if (hooks/vector-node? clause-name-form)
|
||||
(first (:children clause-name-form))
|
||||
clause-name-form)
|
||||
(with-meta (meta clause-name-form))))
|
||||
|
||||
(defn- collect-defclause-body-schemas
|
||||
"Collect the argument schemas in the body of a `defclause` form into a vector node.
|
||||
|
||||
The body is a sequence of <argument-name> <argument-schema> pairs. We can ignore the argument names.
|
||||
|
||||
<argument-schema> can optionally be wrapped be `(optional ...)` or `(rest ...)`... we can ignore these."
|
||||
[arg-specs]
|
||||
(hooks/vector-node
|
||||
(for [[_arg-name arg-schema] (partition-all 2 arg-specs)
|
||||
:let [arg-schema (if (and (hooks/list-node? arg-schema)
|
||||
('#{optional rest} (hooks/sexpr (first (:children arg-schema)))))
|
||||
(-> (second (:children arg-schema))
|
||||
(with-meta (meta arg-schema)))
|
||||
arg-schema)]]
|
||||
arg-schema)))
|
||||
|
||||
(defn defclause
|
||||
"e.g.
|
||||
|
||||
(defclause [ag:var var] field-or-expression FieldOrExpressionDef)
|
||||
=>
|
||||
(def ag:var [FieldOrExpressionDef])"
|
||||
[{:keys [node]}]
|
||||
(let [[_defclause clause-name-form & arg-specs] (:children node)]
|
||||
{:node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'def)
|
||||
(unwrap-defclause-clause-name clause-name-form)
|
||||
(hooks/string-node "Docstring.")
|
||||
(collect-defclause-body-schemas arg-specs)))
|
||||
(with-meta (meta node)))}))
|
||||
|
||||
(comment
|
||||
(defn- defclause* [form]
|
||||
(hooks/sexpr
|
||||
(:node
|
||||
(defclause
|
||||
{:node
|
||||
(hooks/parse-string
|
||||
(with-out-str
|
||||
(clojure.pprint/pprint
|
||||
form)))}))))
|
||||
|
||||
(defn- x []
|
||||
(defclause* '(defclause [ag:var var]
|
||||
field-or-expression FieldOrExpressionDef)))
|
||||
;; =>
|
||||
(def ag:var "Docstring." [FieldOrExpressionDef])
|
||||
|
||||
(defn- y []
|
||||
(defclause* '(defclause var
|
||||
x (some-list-schema)
|
||||
y (optional FieldOrExpressionDef)
|
||||
z (rest (some-other-schema)))))
|
||||
;; =>
|
||||
(def var "Docstring." [(some-list-schema) FieldOrExpressionDef (some-other-schema)]))
|
||||
@@ -1,21 +0,0 @@
|
||||
(ns hooks.metabase.lib.schema.mbql-clause
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]))
|
||||
|
||||
(defn define-mbql-clause
|
||||
"Register the schema keyword so LSP can jump to it."
|
||||
[x]
|
||||
(letfn [(update-def-keyword [k]
|
||||
(-> (api/keyword-node (keyword "mbql.clause" (name (api/sexpr k))))
|
||||
(with-meta (meta k))
|
||||
(api/reg-keyword! 'metabase.lib.schema.mbql-clause/define-mbql-clause)))
|
||||
(update-children [[_def k :as children]]
|
||||
(if (api/keyword-node? k)
|
||||
(update (vec children) 1 update-def-keyword)
|
||||
children))
|
||||
(update-node [node]
|
||||
(if (api/list-node? node)
|
||||
(-> (api/list-node (update-children (:children node)))
|
||||
(with-meta (meta node)))
|
||||
node))]
|
||||
(update x :node update-node)))
|
||||
@@ -1,12 +0,0 @@
|
||||
(ns hooks.metabase.lib.test-util.macros
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn with-testing-against-standard-queries [{{[_ & args] :children} :node}]
|
||||
(let [[query-sym & body] args
|
||||
node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[query-sym (hooks/token-node 'nil)])
|
||||
body))]
|
||||
{:node node*}))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user