Compare commits

..

1 Commits

Author SHA1 Message Date
Denis Berezin
754dd7725f Remove a bit of dead code 2023-06-06 02:27:47 +03:00
15875 changed files with 1348092 additions and 2636788 deletions

View File

@@ -1 +0,0 @@
Fix flakey test $ARGUMENTS

View File

@@ -1 +0,0 @@
Fix issue $ARGUMENTS

View File

@@ -1 +0,0 @@
Fetch unresolved comments for this branch's PR, then fix them

View File

@@ -1 +0,0 @@
Repro issue $ARGUMENTS in a failing test

View File

@@ -0,0 +1 @@
{:lint-as {babashka.fs/with-temp-dir clojure.core/let}}

View File

@@ -0,0 +1 @@
{:hooks {:macroexpand {sci.core/copy-ns sci.core/copy-ns}}}

View 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)]))

View 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}))

View 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}}}

View File

@@ -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))}))

View File

@@ -0,0 +1,4 @@
{:hooks
{:analyze-call
{com.gfredericks.test.chuck.clojure-test/checking
clj-kondo.com.gfredericks.test.chuck.checking/checking}}}

View File

@@ -0,0 +1,2 @@
{:lint-as
{humane-are.core/are+ clojure.test/are}}

View 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}}}

View File

@@ -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))

View File

@@ -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 <>)))))

View File

@@ -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*}))

View File

@@ -0,0 +1,5 @@
(ns macros.toucan.models)
(defmacro defmodel
[model _table-name]
`(def ~model "Docstring." ~(keyword (name model))))

View File

@@ -0,0 +1,4 @@
(ns macros.toucan2.common)
(defn ignore-unused [symb]
(vary-meta symb assoc :clj-kondo/ignore [:unused-binding]))

View File

@@ -0,0 +1,6 @@
(ns macros.toucan2.execute)
(defmacro with-call-count
[[call-count-fn-binding] & body]
`(let [~call-count-fn-binding (fn [])]
~@body))

View File

@@ -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)))

View File

@@ -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)))

View File

@@ -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)))

View File

@@ -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)))

View File

@@ -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))

View File

@@ -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)))

View File

@@ -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)))

View File

@@ -0,0 +1,4 @@
{:linters
{:unresolved-symbol
{:exclude
[(clojure.test/is [partial= re= schema= =?])]}}}

View 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}}

View File

@@ -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})))

View File

@@ -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

View File

@@ -1,144 +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/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!}}}}

View 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
View 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*}))

View 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))])}))

View 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*}))

View 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*}))

View 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)]))

View 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}))

View 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)))))))

View File

@@ -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)))))

View 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})))

View 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*}))

View 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]))

View 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))

View 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))

View File

@@ -0,0 +1,5 @@
(ns macros.metabase.api.common)
(defmacro define-routes [& args]
`(do (def ~'routes "docstring" nil)
~@args))

View 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))

View 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))

View File

@@ -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))

View 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#)))))

View File

@@ -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))

View File

@@ -0,0 +1,4 @@
(ns macros.metabase.models.params.chain-filter-test)
(defmacro chain-filter [& _]
`(metabase.models.params.chain-filter/chain-filter nil nil))

View 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))

View File

@@ -1,4 +0,0 @@
(ns macros.metabase.parameters.chain-filter-test)
(defmacro chain-filter [& _]
`(metabase.parameters.chain-filter/chain-filter nil nil))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View 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))

View File

@@ -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))

View 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)))

View 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))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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)))

View File

@@ -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#)))))

View File

@@ -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))

View File

@@ -1,5 +0,0 @@
(ns macros.metabase-enterprise.database-routing.e2e-test)
(defmacro with-temp-dbs! [bindings & body]
`(let [~@(interleave bindings (repeat nil))]
~@body))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View 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}}}

View 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})))

View 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})))

View File

@@ -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))

View File

@@ -0,0 +1,2 @@
{:lint-as {malli.experimental/defn schema.core/defn}
:linters {:unresolved-symbol {:exclude [(malli.core/=>)]}}}

View 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}}}

View 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))

View File

@@ -0,0 +1 @@
{:lint-as {schema.test/deftest clojure.test/deftest}}

View File

@@ -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)

View File

@@ -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))

View File

@@ -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]))))}))

View File

@@ -1,69 +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)]
(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)

View File

@@ -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})

View File

@@ -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))

View File

@@ -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)))))

View File

@@ -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))))}))

View File

@@ -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)))

View File

@@ -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*}))

View File

@@ -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)))}))

View File

@@ -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)))

View File

@@ -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)]))

View File

@@ -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)))

View File

@@ -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*}))

View File

@@ -1,19 +0,0 @@
(ns hooks.metabase.model-persistence.test-util
(:require
[clj-kondo.hooks-api :as hooks]))
(defn with-persistence-enabled! [x]
(letfn [(update-node [node]
(let [[_symb bindings & body] (:children node)
[fn-binding] (:children bindings)]
(-> (hooks/list-node
(list*
(hooks/token-node `let)
(hooks/vector-node
[fn-binding (hooks/list-node
(list
(hooks/token-node `fn)
(hooks/vector-node [])))])
body))
(with-meta (meta node)))))]
(update x :node update-node)))

Some files were not shown because too many files have changed in this diff Show More