Compare commits

..

55 Commits

Author SHA1 Message Date
Nicolò Pretto
7ca963d0eb hackathon: embedding sdk plugin 2024-03-07 09:20:41 +03:00
Denis Berezin
7eeae6f210 Working solution 2024-03-06 16:25:13 +03:00
Denis Berezin
cb53be1b17 Add api key env 2024-03-06 09:52:34 +03:00
Oisin Coveney
324dd8cf9a Add toggles + switchers for themes, parameters, and styling options 2024-03-01 18:51:43 +03:00
Oisin Coveney
bfb46008a9 remove location from SDK Dashboard 2024-03-01 12:45:00 +03:00
Oisin Coveney
2cb07d3734 Continue static dashboards 2024-02-28 19:43:03 +01:00
Oisin Coveney
3984950693 Continue moving location props out of dashboard components 2024-02-28 12:51:02 +01:00
Oisin Coveney
5e6b42b4ee Move types around on EmbedFrame 2024-02-27 18:52:32 +01:00
Oisin Coveney
e8813d5d09 Add really dirty version of dashboards 2024-02-27 18:16:30 +01:00
Oisin Coveney
1ed70d8c6b save progress on dashboard 2024-02-27 09:53:17 +01:00
Oisin Coveney
da41baa8c5 Formatting and make the style leak flag more useful 2024-02-23 12:38:39 +00:00
Oisin Coveney
ba5940934d Formatting 2024-02-22 12:23:42 +00:00
Oisin Coveney
27007c93ef Quick font adjustment 2024-02-22 10:55:06 +00:00
Oisin Coveney
2c8396318c Add style leak flag to indicate whether a font is leaky 2024-02-21 21:02:11 +00:00
Oisin Coveney
5b14bfa69a Add tailwind and modify app to show off a little more 2024-02-21 20:36:30 +00:00
Oisin Coveney
a03ffb1059 Ensure that public hooks are exposed 2024-02-21 13:39:34 +00:00
Oisin Coveney
1a16919b24 Remove apiUrl from EmbeddingContext 2024-02-21 11:16:34 +00:00
Oisin Coveney
10df8c09f3 Separate hooks and components into public and private folders 2024-02-21 11:04:32 +00:00
Oisin Coveney
a82d370f78 Remove log file from font util API 2024-02-21 10:17:02 +00:00
Oisin Coveney
2228762fa2 Add font selector component to host app and hook for getting and setting fonts within the EmbeddingContext 2024-02-20 18:55:51 +00:00
Oisin Coveney
c129197c6a Reconcile new auth loading, fonts, and config 2024-02-20 09:36:58 +00:00
Oisin Coveney
3502e49eb3 Hide JWT 2024-02-19 20:27:41 +00:00
Denis Berezin
6f37b13e85 Update for default settings 2024-02-15 00:57:25 +07:00
Denis Berezin
96a76bf8d0 Add dynamic api key / jwt usage based on env 2024-02-14 21:27:31 +07:00
Oisin Coveney
66aa4e7da3 Move backend folder to new repo and return files to their rightful place 2024-02-13 17:18:07 +01:00
Oisin Coveney
b7b7d56ab7 Add simple README and redux 2024-02-13 11:06:58 +01:00
Oisin Coveney
69700f56fd Add backend 2024-02-12 17:19:44 +01:00
Oisin Coveney
97d9792dda Add backend 2024-02-12 17:16:20 +01:00
Oisin Coveney
52771825f6 Add embedding check for auth/sso endpoint 2024-02-12 14:39:01 +01:00
Oisin Coveney
51d4b37074 Merge remote-tracking branch 'origin/master' into embedding-sdk-poc-denis 2024-02-12 13:13:42 +01:00
Oisin Coveney
d7a37b178c Move some files around and include the nodejs backend as a new folder 2024-02-09 16:44:50 +01:00
Oisin Coveney
2a0f647c31 Add hooks for managing data and tokens 2024-02-08 20:39:48 +01:00
Oisin Coveney
efa83b6265 Add ability to use session tokens with JWT SSO 2024-02-07 19:05:01 +01:00
Denis Berezin
f9a3177314 Fix user loading, demo prep 2024-02-02 05:10:36 +07:00
Denis Berezin
ac53cf512f Merge branch 'embedding-sdk-poc-denis' of github.com:metabase/metabase into embedding-sdk-poc-denis
# Conflicts:
#	frontend/src/embedding-sdk/QueryVisualization.tsx
#	frontend/src/embedding-sdk/SdkProvider.tsx
#	host-app-example/src/App.css
#	host-app-example/src/App.js
#	host-app-example/src/ChooseQuestionInput.js
2024-02-02 03:50:06 +07:00
Denis Berezin
b10d5dab59 Update demo 2024-02-02 02:11:44 +07:00
Oisin Coveney
ac9b9620d8 Remove unneeded loads for settings + properties 2024-02-01 18:01:27 +00:00
Oisin Coveney
d34503373d Adjust to make sure that fonts are initialized 2024-02-01 17:55:12 +00:00
Oisin Coveney
71952ab1a3 Add question list hook and move MetabaseProvider to root 2024-02-01 17:46:34 +00:00
Oisin Coveney
9bdd7e926c Add question search hook and question list 2024-02-01 16:33:17 +00:00
Oisin Coveney
eb8e199625 Change import order to make things work 2024-02-01 16:31:17 +00:00
Oisin Coveney
3718f2d3ae Remove useApi 2024-02-01 14:13:15 +00:00
Oisin Coveney
b704429325 Remove unneeded files, add hooks, change styles 2024-02-01 14:09:38 +00:00
Oisin Coveney
7653d2fe06 Move css to styled components 2024-02-01 12:51:51 +00:00
Oisin Coveney
27b7ade789 Merge changes 2024-02-01 11:33:25 +00:00
Denis Berezin
7f38319360 Add fonts loading 2024-01-31 23:50:29 +07:00
Denis Berezin
f612a5e5e7 Fix css and emotion global styles 2024-01-30 23:33:39 +07:00
Oisin Coveney
1bb09a7c4c Allow for changes in visualization type 2024-01-29 15:12:51 -06:00
Denis Berezin
470cc7d9b1 Add basic css loading 2024-01-30 03:08:19 +07:00
Denis Berezin
a4599b3432 Fix api data loading 2024-01-30 03:02:25 +07:00
Denis Berezin
17aa273aea Add api token usage 2024-01-29 23:05:50 +07:00
Denis Berezin
95c356ec88 Fixes 2024-01-27 01:02:23 +07:00
Denis Berezin
5330bb6b2f Add bundling and sample app 2024-01-27 00:10:16 +07:00
Denis Berezin
f451285bef Add basic question and visualization bar 2024-01-26 01:48:30 +07:00
Denis Berezin
25cd3c4f54 Draft sdk structure 2024-01-24 22:36:13 +07:00
17203 changed files with 1552360 additions and 2662957 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

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,200 @@
(ns hooks.clojure.core
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]))
(defn- node->qualified-symbol [node]
(try
(when (hooks/token-node? node)
(let [sexpr (hooks/sexpr node)]
(when (symbol? sexpr)
(let [resolved (hooks/resolve {:name sexpr})]
(when-not (= :clj-kondo/unknown-namespace (:ns resolved))
(symbol (name (:ns resolved)) (name (:name resolved))))))))
;; some symbols like `*count/Integer` aren't resolvable.
(catch Exception _
nil)))
(def ^:private white-card-symbols
'#{;; 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!
;; 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/perform-action!
metabase.analytics.snowplow-test/fake-track-event-impl!
metabase.analytics.snowplow/track-event-impl!
metabase.api.public-test/add-card-to-dashboard!
metabase.cmd.dump-to-h2/dump-to-h2!
metabase.cmd.load-from-h2/load-from-h2!
metabase.core/ensure-audit-db-installed!
metabase.db.schema-migrations-test.impl/run-migrations-in-range!
metabase.db.setup/migrate!
metabase.db.setup/setup-db!
metabase.db/setup-db!
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.email-test/reset-inbox!
metabase.email/send-email!
metabase.models.action/insert!
metabase.models.collection.graph-test/clear-graph-revisions!
metabase.models.collection.graph-test/do-with-n-temp-users-with-personal-collections!
metabase.models.field-values/create-or-update-full-field-values!
metabase.models.model-index/add-values!
metabase.models.moderation-review/create-review!
metabase.models.on-demand-test/add-dashcard-with-parameter-mapping!
metabase.models.permissions/grant-application-permissions!
metabase.models.permissions/grant-collection-read-permissions!
metabase.models.permissions/grant-collection-readwrite-permissions!
metabase.models.permissions/grant-full-data-permissions!
metabase.models.permissions/grant-native-readwrite-permissions!
metabase.models.permissions/grant-permissions!
metabase.models.permissions/revoke-application-permissions!
metabase.models.permissions/revoke-data-perms!
metabase.models.permissions/update-data-perms-graph!
metabase.models.permissions/update-group-permissions!
metabase.models.persisted-info/ready-database!
metabase.models.revision/revert!
metabase.models.setting-test/test-user-local-allowed-setting!
metabase.models.setting-test/test-user-local-only-setting!
metabase.models.setting.cache/restore-cache!
metabase.models.setting/set!
metabase.models.setting/validate-settings-formatting!
metabase.permissions.test-util/with-restored-perms!
metabase.pulse/send-notifications!
metabase.pulse/send-pulse!
metabase.query-processor.streaming.interface/begin!
metabase.query-processor.streaming.interface/finish!
metabase.query-processor.streaming.interface/write-row!
metabase.sample-data/try-to-extract-sample-database!
metabase.setup/create-token!
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.sync/sync-database!
metabase.task.index-values/job-init!
metabase.task.persist-refresh/job-init!
metabase.task.persist-refresh/refresh-tables!
metabase.task.persist-refresh/schedule-persistence-for-database!
metabase.task/delete-task!
metabase.test.data.bigquery-cloud-sdk/execute!
metabase.test.data.impl/copy-db-tables-and-fields!
metabase.test.data.impl/get-or-create-database!
metabase.test.data.impl/get-or-create-test-data-db!
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.integrations.ldap/start-ldap-server!
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-test/set-local-infile!
metabase.util.files/create-dir-if-not-exists!
metabase.util.ssh-test/start-mock-servers!
metabase.util.ssh-test/stop-mock-servers!})
(defn- end-with-exclamation?
[s]
(str/ends-with? s "!"))
(defn- non-thread-safe-form-should-end-with-exclamation*
[{[defn-or-defmacro form-name] :children, :as node}]
(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)))]
(walk (fn [form]
(when-let [qualified-symbol (node->qualified-symbol form)]
(when (and (not (contains? white-card-symbols qualified-symbol))
(end-with-exclamation? 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`."
(:string-value defn-or-defmacro) qualified-symbol)
:type :metabase/test-helpers-use-non-thread-safe-functions)))))
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]}]
(when (or (not cljc)
(= lang :clj))
(non-thread-safe-form-should-end-with-exclamation* node))
{: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

@@ -0,0 +1,253 @@
(ns hooks.clojure.test
(:require [clj-kondo.hooks-api :as hooks]
[clojure.string :as str]))
(def ^:private disallowed-parallel-forms
"Things you should not be allowed to use inside parallel tests. Besides these, anything ending in `!` not whitelisted
in [[allowed-parallel-forms]] is not allowed."
'#{clojure.core/alter-var-root
clojure.core/with-redefs
clojure.core/with-redefs-fn
metabase-enterprise.sandbox.test-util/with-gtaps
metabase-enterprise.sandbox.test-util/with-gtaps-for-user
metabase-enterprise.sandbox.test-util/with-user-attributes
metabase-enterprise.test/with-gtaps
metabase-enterprise.test/with-gtaps-for-user
metabase-enterprise.test/with-user-attributes
metabase.actions.test-util/with-actions
metabase.actions.test-util/with-actions-disabled
metabase.actions.test-util/with-actions-enabled
metabase.actions.test-util/with-actions-test-data
metabase.actions.test-util/with-actions-test-data-and-actions-enabled
metabase.actions.test-util/with-actions-test-data-tables
metabase.email-test/with-expected-messages
metabase.email-test/with-fake-inbox
metabase.test.data.users/with-group
metabase.test.data.users/with-group-for-user
metabase.test.persistence/with-persistence-enabled
metabase.test.util.log/with-log-level
metabase.test.util.log/with-log-messages-for-level
metabase.test.util.misc/with-single-admin-user
metabase.test.util.timezone/with-system-timezone-id
metabase.test.util/with-all-users-permission
metabase.test.util/with-column-remappings
metabase.test.util/with-discarded-collections-perms-changes
metabase.test.util/with-env-keys-renamed-by
metabase.test.util/with-locale
metabase.test.util/with-non-admin-groups-no-root-collection-for-namespace-perms
metabase.test.util/with-non-admin-groups-no-root-collection-perms
metabase.test.util/with-temp-env-var-value
metabase.test.util/with-temp-vals-in-db
metabase.test.util/with-temporary-raw-setting-values
metabase.test.util/with-temporary-setting-values
metabase.test.util/with-user-in-groups
metabase.test/with-actions
metabase.test/with-actions-disabled
metabase.test/with-actions-enabled
metabase.test/with-actions-test-data
metabase.test/with-actions-test-data-and-actions-enabled
metabase.test/with-actions-test-data-tables
metabase.test/with-all-users-permission
metabase.test/with-column-remappings
metabase.test/with-discarded-collections-perms-changes
metabase.test/with-env-keys-renamed-by
metabase.test/with-expected-messages
metabase.test/with-fake-inbox
metabase.test/with-group
metabase.test/with-group-for-user
metabase.test/with-locale
metabase.test/with-log-level
metabase.test/with-log-messages-for-level
metabase.test/with-non-admin-groups-no-root-collection-for-namespace-perms
metabase.test/with-non-admin-groups-no-root-collection-perms
metabase.test/with-persistence-enabled
metabase.test/with-single-admin-user
metabase.test/with-system-timezone-id
metabase.test/with-temp-env-var-value
metabase.test/with-temp-vals-in-db
metabase.test/with-temporary-raw-setting-values
metabase.test/with-temporary-setting-values
metabase.test/with-user-in-groups})
;;; TODO -- we should disallow `metabase.test/user-http-request` with any method other than `:get`
(def ^:private allowed-parallel-forms
"These fns are destructive, but are probably fine inside ^:parallel tests because it usually means you're doing
something to an atom or something like that."
'#{clojure.core/assoc!
clojure.core/compare-and-set!
clojure.core/conj!
clojure.core/disj!
clojure.core/dissoc!
clojure.core/persistent!
clojure.core/pop!
clojure.core/reset!
clojure.core/reset-vals!
clojure.core/run!
clojure.core/swap!
clojure.core/swap-vals!
clojure.core/volatile!
clojure.core/vreset!
clojure.core/vswap!
clojure.core.async/<!
clojure.core.async/<!!
clojure.core.async/>!
clojure.core.async/>!!
clojure.core.async/alt!
clojure.core.async/alt!!
clojure.core.async/alts!
clojure.core.async/alts!!
clojure.core.async/close!
clojure.core.async/ioc-alts!
clojure.core.async/offer!
clojure.core.async/onto-chan!
clojure.core.async/onto-chan!!
clojure.core.async/poll!
clojure.core.async/put!
clojure.core.async/take!
clojure.core.async/to-chan!
clojure.core.async/to-chan!!
metabase.driver.sql-jdbc.execute/execute-prepared-statement!
metabase.query-processor/process-query-and-save-execution!
metabase.query-processor/process-query-and-save-with-max-results-constraints!
metabase.query-processor.store/store-database!})
(defn- node->qualified-symbol [node]
(try
(when (hooks/token-node? node)
(let [sexpr (hooks/sexpr node)]
(when (symbol? sexpr)
(when-let [resolved (hooks/resolve {:name sexpr})]
(symbol (name (:ns resolved)) (name (:name resolved)))))))
;; some symbols like `*count/Integer` aren't resolvable.
(catch Exception _
nil)))
(defn- warn-about-disallowed-parallel-forms [form]
(letfn [(error! [form message]
(hooks/reg-finding! (assoc (meta form)
:message message
:type :metabase/validate-deftest)))
(f [form]
(when-let [qualified-symbol (node->qualified-symbol form)]
(cond
(disallowed-parallel-forms qualified-symbol)
(error! form (format "%s is not allowed inside a ^:parallel test or test fixture" qualified-symbol))
(and (not (allowed-parallel-forms 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 .clj-kondo/hooks/clojure/test.clj"
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}]
(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)))))
(def ^:private number-of-lines-for-a-test-to-be-considered-horrifically-long
200)
(defn- deftest-check-not-horrifically-long
[node]
(let [{:keys [row end-row]} (meta node)]
(when (and row end-row)
(let [num-lines (- end-row row)]
(when (>= num-lines number-of-lines-for-a-test-to-be-considered-horrifically-long)
(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 deftest [{:keys [node cljc lang]}]
;; 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))
(deftest-check-not-horrifically-long node)
{: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- warn-about-schema= [{[_is assertion-node] :children, :as _is-node}]
(let [{[assertion-symb-node] :children} assertion-node]
(when (and (hooks/token-node? assertion-symb-node)
(= (hooks/sexpr assertion-symb-node) 'schema=))
(hooks/reg-finding! (assoc (meta assertion-symb-node)
:message "Use =? or malli= instead of schema="
:type :metabase/warn-about-schema=)))))
(defn is [{:keys [node lang]}]
(when (= lang :cljs)
(warn-about-missing-test-expr-requires-in-cljs node))
(warn-about-schema= node)
{:node node})
(defn use-fixtures [{:keys [node]}]
(warn-about-disallowed-parallel-forms node)
{:node node})

371
.clj-kondo/hooks/common.clj Normal file
View File

@@ -0,0 +1,371 @@
(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-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*}))

View File

@@ -0,0 +1,28 @@
(ns hooks.metabase.api.automagic-dashboards-test
(:require [clj-kondo.hooks-api :as hooks]))
(comment
(def input (hooks/parse-string
"(with-indexed-model [{:keys [model model-index model-index-value]}
{:query :some-query}]
:foo :bar :body)"))
(println (hooks/sexpr input))
(let [f (load-file ".clj-kondo/hooks/metabase/api/automagic_dashboards_test.clj")]
(f {:node input})))
(defn with-indexed-model
[{node :node}]
(let [[_macro-call binding-and-query-info & body] (:children node)
[binding query-info] (:children binding-and-query-info)
node* (hooks/list-node
[(hooks/token-node 'do-with-testing-model)
query-info
(hooks/list-node
(list* (hooks/token-node 'fn)
(hooks/vector-node [binding])
body))])]
#_(println (hooks/sexpr node*))
{: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 [(hooks/token-node '&) (hooks/token-node '_args)])))])
migration-range
body))]
{:node node*}))

View File

@@ -0,0 +1,12 @@
(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

@@ -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,264 @@
(ns hooks.metabase.models.setting
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common :as common]))
(def ^:private ignored-implicit-export?
'#{active-users-count
admin-email
analytics-uuid
anon-tracking-enabled
api-key
attachment-table-row-limit
audit-max-retention-days
bcc-enabled?
breakout-bin-width
check-for-updates
cloud-gateway-ips
config-from-file-sync-databases
custom-homepage
custom-homepage-dashboard
database-enable-actions
deprecation-notice-version
dismissed-custom-dashboard-toast
email-configured?
email-from-address
email-from-name
email-reply-to
email-smtp-host
email-smtp-password
email-smtp-port
email-smtp-security
email-smtp-username
embedding-app-origin
embedding-secret-key
enable-password-login
enable-public-sharing
enable-query-caching
engines
enum-cardinality-threshold
follow-up-email-sent
ga-code
ga-enabled
google-auth-auto-create-accounts-domain
google-auth-client-id
google-auth-configured
google-auth-enabled
has-sample-database?
has-user-setup
help-link
help-link-custom-destination
instance-creation
is-hosted?
is-metabot-enabled
jdbc-data-warehouse-max-connection-pool-size
jdbc-data-warehouse-unreturned-connection-timeout-seconds
jwt-attribute-email
jwt-attribute-firstname
jwt-attribute-groups
jwt-attribute-lastname
jwt-configured
jwt-enabled
jwt-group-mappings
jwt-group-sync
jwt-identity-provider-uri
jwt-user-provisioning-enabled?
jwt-shared-secret
last-acknowledged-version
ldap-attribute-email
ldap-attribute-firstname
ldap-attribute-lastname
ldap-bind-dn
ldap-configured?
ldap-enabled
ldap-group-base
ldap-group-mappings
ldap-group-membership-filter
ldap-group-sync
ldap-host
ldap-password
ldap-port
ldap-user-provisioning-enabled?
ldap-security
ldap-sync-user-attributes
ldap-sync-user-attributes-blacklist
ldap-user-base
ldap-user-filter
load-analytics-content
map-tile-server-url
metabot-default-embedding-model
metabot-feedback-url
metabot-get-prompt-templates-url
metabot-prompt-generator-token-limit
multi-setting-read-only
notification-link-base-url
num-metabot-choices
openai-api-key
openai-available-models
openai-model
openai-organization
other-sso-enabled?
password-complexity
persist-models-enabled
persisted-model-refresh-cron-schedule
premium-embedding-token
prometheus-server-port
query-caching-max-kb
query-caching-max-ttl
query-caching-min-ttl
query-caching-ttl-ratio
redirect-all-requests-to-https
reset-token-ttl-hours
retired-setting
retry-initial-interval
retry-max-attempts
retry-max-interval-millis
retry-multiplier
retry-randomization-factor
saml-application-name
saml-attribute-email
saml-attribute-firstname
saml-attribute-group
saml-attribute-lastname
saml-configured
saml-enabled
saml-group-mappings
saml-group-sync
saml-identity-provider-certificate
saml-identity-provider-issuer
saml-identity-provider-uri
saml-keystore-alias
saml-keystore-password
saml-keystore-path
saml-user-provisioning-enabled?
send-email-on-first-login-from-new-device
send-new-sso-user-admin-email?
session-cookie-samesite
session-cookies
session-timeout
setup-token
show-database-syncing-modal
show-metabase-links
site-url
site-uuid
site-uuid-for-premium-features-token-checks
site-uuid-for-unsubscribing-url
site-uuid-for-version-info-fetching
slack-app-token
slack-cached-channels-and-usernames
slack-channels-and-usernames-last-updated
slack-files-channel
slack-token
slack-token-valid?
snowplow-available
snowplow-enabled
snowplow-url
sql-jdbc-fetch-size
ssh-heartbeat-interval-sec
ssl-certificate-public-key
startup-time-millis
token-features
token-status
toucan-name
uncached-setting
uploads-table-prefix
user-visibility
version
version-info
version-info-last-checked})
(defn- defsetting-lint [node setting-name docstring options-list]
(let [anon-binding (common/with-macro-meta (hooks/token-node '_) 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 %))))))]
(when (nil? (second (drop-while (comp not #{[:k :export?]} first) options-list)))
(when-not (contains? ignored-implicit-export? (:value setting-name))
(hooks/reg-finding! (assoc (meta node)
:message "Setting definition must provide an explicit value for :export? indicating whether the setting should be exported or not with serialization."
:type :metabase/defsetting-must-specify-export))))
{:node (-> (list
(hooks/token-node 'let)
;; include description and the options map so they can get validated as well.
(hooks/vector-node
[anon-binding docstring
anon-binding (hooks/map-node options-list)])
getter-node
setter-node)
hooks/list-node
(with-meta (meta node)))}))
(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 docstring & options] (rest (:children node))]
(defsetting-lint node setting-name docstring options)))
(defn define-multi-setting
"Rewrite a [[metabase.models.define-multi-setting]] form like
(defsetting my-setting \"Description\" :key :type :boolean)
as
(let [_ \"Description\"
_ {:type :boolean, :multi-thunk :key}]
(defn my-setting \"Docstring.\" [])
(defn my-setting! \"Docstring.\" [_value-or-nil]))
for linting purposes."
[{:keys [node]}]
(let [[setting-name docstring thunk & options] (rest (:children node))]
(defsetting-lint node setting-name docstring (concat options [(hooks/token-node :multi-thunk) thunk]))))
(comment
(defn- defsetting* [form]
(hooks/sexpr
(:node
(defsetting
{:node
(hooks/parse-string
(with-out-str
#_{:clj-kondo/ignore [:unresolved-namespace]}
(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,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,59 @@
(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 'card-id-pivot-table) 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

@@ -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,75 +0,0 @@
(ns hooks.clojure.core.require
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common.modules :as modules]))
(defn- lint* [node current-ns config required-namespaces]
{:pre [(map? node)
(simple-symbol? current-ns)
(map? config)
(sequential? required-namespaces)
(every? simple-symbol? required-namespaces)]}
(when-not (modules/ignored-namespace? config current-ns)
;; ignore namespaces outside of the module system
(when-let [current-module (modules/module current-ns)]
(doseq [required-namespace required-namespaces]
;; ignore namespaces outside of the module system.
(when (modules/module required-namespace)
(when-let [error (modules/usage-error config current-module required-namespace)]
(hooks/reg-finding! (assoc (meta node)
:message error
:type :metabase/modules))))))))
(defn- unwrap-require [node]
(case (hooks/tag node)
;; some.namespace of some.namespace/some-var
:token
(let [sexpr (hooks/sexpr node)]
(if (qualified-symbol? sexpr)
(symbol (namespace sexpr))
sexpr))
;; (quote some.namespace)
:list
(let [[x y] (:children node)]
(when (and (hooks/token-node? x)
(= (hooks/sexpr x) 'quote))
(recur y)))
;; 'some.namespace
:quote
(recur (first (:children node)))
;; something like [some.namespace :as whatever]
:vector
(recur (first (:children node)))))
(comment
(unwrap-require (-> "(requiring-resolve 'metabase.notification.payload.execute/execute-dashboard)"
hooks/parse-string
:children
second)))
(defn- lint-require* [node current-ns config]
(let [[_require & args] (:children node)
required-namespaces (->> (keep unwrap-require args)
;; `require` runs through `cc/load-libs` which allows
;; #{:as :reload :reload-all :require :use :verbose :refer :as-alias}
;; as args. If you use a REPL tool that applies clj-kondo linting (e.g., Clojure MCP)
;; you may wish to `:reload` or `:reload-all` a namespace. So we shouldn't assume all
;; args are keywords:
(remove keyword?))]
(lint* node current-ns config required-namespaces)))
(defn- lint-requiring-resolve* [node current-ns config]
(let [[_requiring-resolve symb-node] (:children node)
required-namespace (unwrap-require symb-node)]
(lint* node current-ns config [required-namespace])))
(defn lint-require [x]
(lint-require* (:node x) (:ns x) (modules/config x))
x)
(defn lint-requiring-resolve [x]
(lint-requiring-resolve* (:node x) (:ns x) (modules/config x))
x)

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,18 +0,0 @@
(ns hooks.metabase.driver.mongo-test
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common]))
(defn with-describe-table-for-sample
"Forward bindings for results of functions used in mongo's describe-table implementation."
[{{[_ _ & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(hooks/token-node 'dbfields) (hooks/token-node '(delay nil))
(hooks/token-node 'ftree) (hooks/token-node '(delay nil))
(hooks/token-node 'nested-fields) (hooks/token-node '(delay nil))])
body))]
;; Avoid warnings when not all from the dbfields, ftree and nested-fields are used.
{:node (hooks.common/update-ignored-linters node* conj :unused-binding)}))

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

View File

@@ -1,21 +0,0 @@
(ns hooks.metabase.models.interface
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common]))
(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 (meta node))
hooks.common/add-lsp-ignore-unused-public-var-metadata)]
{:node node}))

View File

@@ -1,65 +0,0 @@
(ns hooks.metabase.premium-features.defenterprise
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common]))
(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 (meta node))
hooks.common/add-lsp-ignore-unused-public-var-metadata)))}))
(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

@@ -1,17 +0,0 @@
(ns hooks.metabase.search.api-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

@@ -1,310 +0,0 @@
(ns hooks.metabase.settings.models.setting
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common :as common]))
(defn lint-defsetting-namespace [node context]
(when-not (re-matches #"^metabase(?:-enterprise)?\.[^\.]+\.settings$" (name (:ns context)))
(hooks/reg-finding! (assoc (meta node)
:message "All defsettings should live in metabase[-enterprise].<module>.settings namespaces"
:type :metabase/defsetting-namespace))))
;;; TODO -- move this into a Kondo config file in `.clj-kondo/config/`
(def ^:private ignored-implicit-export?
'#{active-users-count
admin-email
analytics-uuid
anon-tracking-enabled
api-key
attachment-table-row-limit
audit-max-retention-days
bcc-enabled?
breakout-bin-width
check-for-updates
cloud-gateway-ips
config-from-file-sync-databases
custom-homepage
custom-homepage-dashboard
database-enable-actions
deprecation-notice-version
dismissed-custom-dashboard-toast
email-configured?
email-from-address
email-from-name
email-reply-to
email-smtp-host
email-smtp-password
email-smtp-port
email-smtp-security
email-smtp-username
embedding-app-origin
embedding-secret-key
enable-password-login
enable-public-sharing
enable-query-caching
engines
enum-cardinality-threshold
follow-up-email-sent
google-auth-auto-create-accounts-domain
google-auth-client-id
google-auth-configured
google-auth-enabled
has-sample-database?
has-user-setup
help-link
help-link-custom-destination
instance-creation
is-hosted?
is-metabot-enabled
jdbc-data-warehouse-max-connection-pool-size
jdbc-data-warehouse-unreturned-connection-timeout-seconds
jwt-attribute-email
jwt-attribute-firstname
jwt-attribute-groups
jwt-attribute-lastname
jwt-configured
jwt-enabled
jwt-group-mappings
jwt-group-sync
jwt-identity-provider-uri
jwt-user-provisioning-enabled?
jwt-shared-secret
last-acknowledged-version
last-used-native-database-id
ldap-attribute-email
ldap-attribute-firstname
ldap-attribute-lastname
ldap-bind-dn
ldap-configured?
ldap-enabled
ldap-group-base
ldap-group-mappings
ldap-group-membership-filter
ldap-group-sync
ldap-host
ldap-password
ldap-port
ldap-user-provisioning-enabled?
ldap-security
ldap-sync-user-attributes
ldap-sync-user-attributes-blacklist
ldap-user-base
ldap-user-filter
load-analytics-content
map-tile-server-url
metabot-default-embedding-model
metabot-feedback-url
metabot-get-prompt-templates-url
metabot-prompt-generator-token-limit
multi-setting-read-only
notebook-native-preview-shown
notebook-native-preview-sidebar-width
notification-link-base-url
num-metabot-choices
openai-api-key
openai-available-models
openai-model
openai-organization
other-sso-enabled?
password-complexity
persist-models-enabled
persisted-model-refresh-cron-schedule
premium-embedding-token
prometheus-server-port
query-caching-max-kb
query-caching-max-ttl
query-caching-min-ttl
query-caching-ttl-ratio
redirect-all-requests-to-https
reset-token-ttl-hours
retired-setting
retry-initial-interval
retry-max-attempts
retry-max-interval-millis
retry-multiplier
retry-randomization-factor
saml-application-name
saml-attribute-email
saml-attribute-firstname
saml-attribute-group
saml-attribute-lastname
saml-configured
saml-enabled
saml-group-mappings
saml-group-sync
saml-identity-provider-certificate
saml-identity-provider-issuer
saml-identity-provider-uri
saml-keystore-alias
saml-keystore-password
saml-keystore-path
saml-user-provisioning-enabled?
sdk-iframe-embed-setup-settings
send-email-on-first-login-from-new-device
send-new-sso-user-admin-email?
session-cookie-samesite
session-cookies
session-timeout
setup-token
show-database-syncing-modal
show-metabase-links
site-url
site-uuid
site-uuid-for-premium-features-token-checks
site-uuid-for-unsubscribing-url
site-uuid-for-version-info-fetching
slack-app-token
slack-cached-channels-and-usernames
slack-channels-and-usernames-last-updated
slack-files-channel
slack-token
slack-token-valid?
snowplow-available
snowplow-enabled
snowplow-url
sql-jdbc-fetch-size
ssh-heartbeat-interval-sec
ssl-certificate-public-key
startup-time-millis
token-features
token-status
toucan-name
trial-banner-dismissal-timestamp
uncached-setting
user-visibility
version
version-info
version-info-last-checked})
(defn- lint-defsetting-export [node]
(let [[_defsetting setting-name _docstring & options] (:children node)]
(when (nil? (second (drop-while (comp not #{[:k :export?]} first) options)))
(when-not (contains? ignored-implicit-export? (:value setting-name))
(hooks/reg-finding! (assoc (meta node)
:message "Setting definition must provide an explicit value for :export? indicating whether the setting should be exported or not with serialization."
:type :metabase/defsetting-must-specify-export))))))
(defn- defsetting-replacement-node [node]
(let [[_defsetting setting-name docstring & options-list] (:children node)
anon-binding (common/with-macro-meta (hooks/token-node '_) 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 (meta node))
common/add-lsp-ignore-unused-public-var-metadata)]
(-> (hooks/list-node
(list
(hooks/token-node 'let)
;; include description and the options map so they can get validated as well.
(hooks/vector-node
[anon-binding docstring
anon-binding (hooks/map-node options-list)])
(hooks/reg-keyword! (-> (hooks/keyword-node (keyword (hooks/sexpr setting-name)))
(with-meta (meta setting-name)))
'metabase.settings.models.setting/defsetting)
getter-node
setter-node))
(with-meta (meta node)))))
(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], :as context}]
(lint-defsetting-namespace node context)
(lint-defsetting-export node)
(update context :node defsetting-replacement-node))
(defn define-multi-setting-replacement-node [node]
(let [[defsetting setting-name docstring thunk & options] (:children node)]
(-> (hooks/list-node
(list*
defsetting
setting-name
docstring
(hooks/token-node :multi-thunk) thunk
options))
(with-meta (meta node))
defsetting-replacement-node)))
(defn define-multi-setting
"Rewrite a [[metabase.models.define-multi-setting]] form like
(defsetting my-setting \"Description\" :key :type :boolean)
as
(let [_ \"Description\"
_ {:type :boolean, :multi-thunk :key}]
(defn my-setting \"Docstring.\" [])
(defn my-setting! \"Docstring.\" [_value-or-nil]))
for linting purposes."
[{:keys [node], :as context}]
(update context :node define-multi-setting-replacement-node))
(defn define-multi-setting-impl
[context]
(letfn [(update-node [node]
(let [[_define-multi-setting-impl setting-name & more] (:children node)]
(-> (hooks/list-node
(list*
(hooks/token-node 'do)
;; For a qualified setting name like `oss-namespace/setting`, then leave the symbol as is, so the
;; usage of that namespace/var gets recorded. Otherwise for an unqualified name convert it to a
;; keyword so we don't get unresolved var errors.
(if (and (hooks/token-node? setting-name)
(qualified-symbol? (hooks/sexpr setting-name)))
setting-name
(-> (hooks/keyword-node (keyword (hooks/sexpr setting-name)))
(with-meta (meta setting-name))))
more))
(with-meta (meta node)))))]
(update context :node update-node)))
(comment
(defn- defsetting* [form]
(hooks/sexpr
(:node
(defsetting
{:node
(hooks/parse-string
(with-out-str
#_{:clj-kondo/ignore [:unresolved-namespace]}
(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.app-db.core/db-is-set-up?))
0
(cached-active-users-count)))))))

View File

@@ -1,153 +0,0 @@
(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- local-def?
[ns* sexpr]
(= ns* (:ns (hooks/resolve {:name sexpr}))))
(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]]."
[this-ns dataset]
(let [sexpr (hooks/sexpr dataset)
global-defs (global-dataset-symbols)]
(cond
(not (symbol? sexpr))
:non-symbol
(namespace sexpr)
:qualified
(contains? global-defs sexpr)
:unqualified/from-dataset-defs-namespace
(local-def? this-ns sexpr)
:unqualified/local-def
:else
:unqualified/unknown)))
(defn dataset
[{{[_ dataset & body] :children, :as node} :node this-ns :ns}]
(let [node' (case (dataset-type this-ns 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)
(hooks/list-node
(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 form like
;;
;; (do ...)
(:unqualified/from-dataset-defs-namespace :unqualified/unknown)
(hooks/list-node
(list*
(hooks/token-node `do)
body)))]
{:node (with-meta node' (meta node))}))
(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 :metabase/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

@@ -1,49 +0,0 @@
(ns hooks.metabase.test.data.datasets
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.set :as set]
[hooks.common]))
(defn- only-core-drivers?
"Whether we're for sure only testing against the 'core' drivers that are also app DB types: `:postgres`, `:mysql`, or
`:h2`. Tests like these are ok to mark `:mb/driver-tests` since we always run the full test suite against app DB
drivers anyway."
[node]
(let [drivers (hooks/sexpr (second (:children node)))
drivers (cond
(keyword? drivers) #{drivers}
(set? drivers) drivers
:else nil)]
(when drivers
(empty? (set/difference drivers #{:h2 :mysql :postgres})))))
(defn- validate-mb-driver-tests [x]
(when-not (:mb/driver-tests (meta (:ns x)))
(hooks/reg-finding!
(assoc (meta (:ns x))
:message (str "Namespaces that run driver tests (test-driver or test-drivers) should be marked"
" ^:mb/driver-tests [:metabase/validate-mb-driver-tests]")
:type :metabase/validate-mb-driver-tests))))
(defn- validate-ns-tags [x]
(when-not (only-core-drivers? (:node x))
(validate-mb-driver-tests x)))
(defn test-drivers [x]
(validate-ns-tags x)
(letfn [(update-node [node]
(let [[test-drivers drivers-expr & body] (:children node)]
(-> (hooks/list-node
(list*
(hooks/token-node 'do)
test-drivers
(-> drivers-expr
(hooks.common/update-ignored-linters conj :unused-value))
body))
(with-meta (meta node)))))]
(update x :node update-node)))
(defn test-driver
"We can use the same hook for `test-driver` as we use for `test-drivers`."
[x]
(test-drivers x))

View File

@@ -1,14 +0,0 @@
(ns hooks.metabase.test.gentest
(:require
[clj-kondo.hooks-api :as hooks]))
(defn iterate*
"(iterate {:as limit-spec} [s1 val1 & more] body-form body-form...)"
[{{:keys [children] :as node} :node :as orig}]
(let [[token limit-spec bindings & body-nodes] children
node* (-> (hooks/list-node
(list token limit-spec
(hooks/list-node (list* (hooks/token-node 'let) bindings
body-nodes))))
(with-meta (meta node)))]
(assoc orig :node node*)))

View File

@@ -1,60 +0,0 @@
(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

@@ -1,15 +0,0 @@
(ns hooks.metabase.test.util.generators.jvm
(:require
[clj-kondo.hooks-api :as hooks]))
(defn with-random-cards
"(with-random-cards <metadata-provider> <integer-count-value> & body-forms)"
[{{:keys [children] :as node} :node :as orig}]
(let [[token num-cards & body-nodes] children
node* (-> (hooks/list-node
(list token num-cards
(hooks/list-node (list* (hooks/token-node 'let)
(hooks/vector-node [(hooks/token-node '&cards) (hooks/list-node ())])
body-nodes))))
(with-meta (meta node)))]
(assoc orig :node node*)))

View File

@@ -1,31 +0,0 @@
(ns hooks.metabase.util
(:require
[clj-kondo.hooks-api :as api]
[hooks.common]))
(defn format-color [{:keys [node], :as x}]
(let [[_format-color _color format-string-node & args] (:children node)]
(when (api/string-node? format-string-node)
(let [expected-arg-count (hooks.common/format-string-specifier-count (api/sexpr format-string-node))
actual-arg-count (count args)]
(when-not (= expected-arg-count actual-arg-count)
(api/reg-finding! (assoc (meta node)
:message (format "metabase.util/format-color format string expects %d arguments instead of %d."
expected-arg-count
actual-arg-count)
:type :format))))))
x)
(defn case-enum
"Lint `(case-enum ...)` like `(condp = ...)`."
[x]
(letfn [(update-node [node]
(let [[_case-enum x & more] (:children node)]
(-> (api/list-node
(list*
(api/token-node 'clojure.core/condp)
(api/token-node 'clojure.core/=)
x
more))
(with-meta (meta node)))))]
(update x :node update-node)))

View File

@@ -1,74 +0,0 @@
(ns hooks.metabase.util.i18n
(:require
[clj-kondo.hooks-api :as api]
[clojure.string :as str]
[hooks.common]))
(defn- valid-apostrophes?
"Returns true if `s` contains only doubled quotes (''') or complete placeholder-escaping quotes ('{...}'), false otherwise."
[s]
(loop [chars (seq s)]
(if (empty? chars)
true
(let [c (first chars)]
(if (= c \')
(cond
;; Doubled quotes: skip both
(and (next chars) (= (second chars) \'))
(recur (nthrest chars 2))
;; Placeholder escape. We allow for intentional escaping, like either '{0}` or '{{`, but not 'arbitrary
;; strings' So basically, just require the *next* character to be a `{`, find the next single-quote, and
;; continue on from there.
;;
;; Technically a string like "foo bar's happy but i'm not" is totally fine, but we'll say it's invalid
;; because... that inner string literal is probably not what we actually want. So since we've never done
;; this intentionally as far as I can tell, let's just remove the possibility to avoid ambiguity.
(= (second chars) \{)
(let [at-closing (drop-while #(not= \' %) (next chars))]
(if (seq at-closing)
(recur (next at-closing))
false))
;; Unpaired single quote: invalid
:else
false)
(recur (rest chars)))))))
(defn format-str-arg-node->str
[node]
(cond
;; it's just a string literal
(api/string-node? node) (api/sexpr node)
;; it's `(str "some" "strings")`
(and (api/list-node? node)
(= 'str (api/sexpr (first (:children node))))
(every? api/string-node? (rest (:children node))))
(apply str (map api/sexpr (rest (:children node))))
;; it's something else, no good
:else
(do
(api/reg-finding!
(assoc (meta node)
:message "Must be a string literal or a call to `str` with string literal arguments."
:type :metabase/validate-string-or-str-args-to-i18n))
nil)))
(defn strict-apostrophes
"Checks the node to make sure it's not a i18n call that will result in dropped apostrophes."
[{:keys [node]}]
(let [format-str-arg (second (:children node))
;; `format-str-arg` must be either a `"string"` or `(str "a call " "to" "str")` (to allow for splitting long
;; strings into shorter lines)
format-str (format-str-arg-node->str format-str-arg)]
(when-not (valid-apostrophes? format-str)
(api/reg-finding!
(assoc (meta node)
:message "Format string contains invalid single quote usage. Use '' for literals or '{...}' for escaping."
:type :metabase/validate-escaped-single-quotes-in-i18n))))
{:node node})
(def tru strict-apostrophes)
(def trs strict-apostrophes)
(def deferred-tru strict-apostrophes)
(def deferred-trs strict-apostrophes)

View File

@@ -1,124 +0,0 @@
(ns hooks.metabase.util.log
(:require
[clj-kondo.hooks-api :as api]
[clojure.string :as str]
[clojure.walk :as walk]
[hooks.common]))
;;;; Checks for both log + logf
(defn- disallow-i18n [node]
(walk/postwalk
(fn [x]
(when-let [qualified-symbol (hooks.common/node->qualified-symbol x)]
(when (and (= (namespace qualified-symbol) "metabase.util.i18n")
(or (str/includes? (name qualified-symbol) "tru")
(str/includes? (name qualified-symbol) "trs")))
(api/reg-finding! (assoc (meta x)
:message "do not i18n the logs!"
:type :metabase/validate-logging)))))
node))
(defn- check-position-of-exception-arg
"Tell people to do (info e \"whatever\") instead of (info \"whatever\" e)."
[node]
(let [[f _first-arg & args] (:children node)]
(doseq [arg args]
(when (and (api/token-node? arg)
(= (api/sexpr arg) 'e))
(api/reg-finding! (assoc (meta arg)
:message (format "pass exceptions as the first arg to %s" (api/sexpr f))
:type :metabase/validate-logging))))))
(defn- common-checks [node]
(check-position-of-exception-arg node)
(disallow-i18n node))
(defn- check-for-i18n-format-specifiers
"Make sure people aren't using things like {0} in their log messages."
[node string-node]
(when (re-find #"\{\d+\}" (api/sexpr string-node))
(api/reg-finding! (assoc (meta node)
:message "this looks like an i18n format string. Don't use identifiers like {0} in logging."
:type :metabase/validate-logging))))
;;; log only
(defn- check-not-format-string
"Make sure we're not using something like log/trace with a format string."
[{[f] :children, :as node} string-node]
(when (pos? (hooks.common/format-string-specifier-count (api/sexpr string-node)))
(api/reg-finding! (assoc (meta node)
:message (format "%s used with a format string, use %s instead."
(api/sexpr f)
(str (api/sexpr f) "f"))
:type :metabase/validate-logging))))
(defn- format-node? [node]
(when (api/list-node? node)
(let [[x] (:children node)]
(and (api/token-node? x)
(or (= (hooks.common/node->qualified-symbol x) 'clojure.core/format)
(= (api/sexpr x) 'format))))))
(defn- warn-against-log+format-instead-of-logf
"Tell people to use things like infof instead of info + format."
[{[f] :children, :as node} [x :as args]]
(when (and (= (count args) 1)
(api/list-node? x))
(when (format-node? x)
(api/reg-finding! (assoc (meta node)
:message (format "Use %s instead of %s + format"
(str (api/sexpr f) "f")
(api/sexpr f))
:type :metabase/validate-logging)))))
(defn info
"Valid log, debug, etc."
[{:keys [node], :as x}]
(common-checks node)
(let [[_f & args] (:children node)
[_e & [x :as args]] (if (and (api/token-node? (first args))
(or (api/string-node? (second args))
(format-node? (second args))))
args
(cons nil args))]
(when (api/string-node? x)
(check-for-i18n-format-specifiers node x)
(check-not-format-string node x))
(warn-against-log+format-instead-of-logf node args))
x)
;;;; logf only
(defn- check-format-string-arg-count
"Make sure the number of args passed to something like tracef match the number of format string specifiers."
[{[f] :children, :as node} format-string args]
(let [expected-arg-count (hooks.common/format-string-specifier-count (api/sexpr format-string))
actual-arg-count (count args)]
(when (zero? expected-arg-count)
(api/reg-finding! (assoc (meta node)
:message (format "Don't use %s with no format string arguments, use %s instead."
(api/sexpr f)
(str/replace (str (api/sexpr f)) #"f$" ""))
:type :metabase/validate-logging)))
(when-not (= expected-arg-count actual-arg-count)
(api/reg-finding! (assoc (meta node)
:message (format "log format string expects %d arguments instead of %d."
expected-arg-count
actual-arg-count)
:type :metabase/validate-logging)))))
(defn infof
"Valid logf, debugf, etc."
[{:keys [node], :as x}]
(common-checks node)
(let [[_f & args] (:children node)
[_e format-string & args] (if (api/string-node? (first args))
(cons nil args)
args)]
;; TODO -- maybe we can error if you did not pass a format string, e.g. (log/infof 1 2) is obviously incorrect.
(when (api/string-node? format-string)
(check-for-i18n-format-specifiers node format-string)
(check-format-string-arg-count node format-string args)))
x)

View File

@@ -1,26 +0,0 @@
(ns hooks.metabase.util.log.capture
(:require
[clj-kondo.hooks-api :as api]))
(defn with-log-messages-for-level [x]
(letfn [(update-binding [[lhs-node _rhs-node]]
[lhs-node (api/list-node
(list
(api/token-node 'clojure.core/fn)
(api/vector-node [])))])
(update-bindings [bindings-node]
(-> (api/vector-node
(into []
(comp (partition-all 2)
(mapcat update-binding))
(:children bindings-node)))
(with-meta (meta bindings-node))))
(update-node [node]
(let [[_with-log-messages-for-level bindings & body] (:children node)]
(-> (api/list-node
(list*
(api/token-node 'clojure.core/let)
(update-bindings bindings)
body))
(with-meta (meta node)))))]
(update x :node update-node)))

View File

@@ -1,16 +0,0 @@
(ns hooks.metabase.util.timing
"Custom CLJ Kondo hooks for timing-related linting rules."
(:require [clj-kondo.hooks-api :as api]))
(defn discourage-millis-duration
"Discourage slow and unreliable duration calculations using System/currentTimeMillis "
[{:keys [node]}]
(let [sexpr (api/sexpr node)]
(when (and (list? sexpr) (= (first sexpr) '-) (>= (count sexpr) 2))
(let [first-arg (second sexpr)]
(when (= first-arg (list 'System/currentTimeMillis))
(api/reg-finding!
(assoc (meta node)
:message "Avoid System/currentTimeMillis for calculating durations. Use u/start-timer and u/since-ms for more reliability."
:type :metabase/discourage-millis-duration
:level :warning)))))))

View File

@@ -1,29 +0,0 @@
(ns hooks.metabase.xrays.api.automagic-dashboards-test
(:require
[clj-kondo.hooks-api :as hooks]))
(comment
(def input (hooks/parse-string
"(with-indexed-model! [{:keys [model model-index model-index-value]}
{:query :some-query}]
:foo :bar :body)"))
(println (hooks/sexpr input))
(let [f (load-file ".clj-kondo/hooks/metabase/xrays/api/automagic_dashboards_test.clj")]
(f {:node input})))
(defn with-indexed-model!
[{node :node}]
(let [[_macro-call binding-and-query-info & body] (:children node)
[binding query-info] (:children binding-and-query-info)
node* (hooks/list-node
[(hooks/token-node 'do-with-indexed-model!)
query-info
(hooks/list-node
(list* (hooks/token-node 'fn)
(hooks/vector-node [binding])
body))])]
#_(println (hooks/sexpr node*))
{:node node*}))

View File

@@ -1,63 +0,0 @@
(ns hooks.clojure.core.defmulti-test
(:require
[clj-kondo.hooks-api :as hooks]
[clj-kondo.impl.utils]
[clojure.test :refer :all]
[hooks.clojure.core.defmulti]))
(defn- lint-defmulti [form]
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/check-defmulti-arglists {:level :warning}}}
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(hooks.clojure.core.defmulti/lint-defmulti {:node (hooks/parse-string (pr-str form))})
@(:findings clj-kondo.impl.utils/*ctx*)))
(deftest ^:parallel defmulti-missing-arglists-test
(testing "missing arglists"
(is (=? [{:message "All defmultis should have an attribute map with :arglists metadata. [:metabase/check-defmulti-arglists]"}]
(lint-defmulti '(defmulti expand-visualization
(fn [card _ _]
(-> card :visualization first))))))))
(deftest ^:parallel defmulti-invalid-arglists-test
(testing "invalid arglists"
(are [form] (=? [{:message ":arglists should be a quoted list of vectors [:metabase/check-defmulti-arglists]"}]
(lint-defmulti form))
'(defmulti expand-visualization
{:arglists '()}
(fn [card _ _]
(-> card :visualization first)))
'(defmulti expand-visualization
{:arglists ([a])} ; unquoted
(fn [card _ _]
(-> card :visualization first)))
'(defmulti expand-visualization
{:arglists '[(a)]}
(fn [card _ _]
(-> card :visualization first)))
'(defmulti expand-visualization
{:arglists '((a))}
(fn [card _ _]
(-> card :visualization first))))))
(deftest ^:parallel defmulti-arglists-with-underscore-args-test
(testing "arglists, but some symbols are _"
(are [form] (=? [{:message ":arglists should contain actual arg names, not underscore (unused) symbols [:metabase/check-defmulti-arglists]"}]
(lint-defmulti form))
'(defmulti expand-visualization
{:arglists '([card _ _])}
(fn [card _ _]
(-> card :visualization first)))
'(defmulti expand-visualization
{:arglists '([card _bad _arg])}
(fn [card _ _]
(-> card :visualization first))))))
(deftest ^:parallel defmulti-arglists-ok-test
(testing "good"
(is (= []
(lint-defmulti '(defmulti expand-visualization
{:arglists '([card x y])}
(fn [card _ _]
(-> card :visualization first))))))))

View File

@@ -1,76 +0,0 @@
(ns hooks.clojure.core.ns-test
(:require
[clj-kondo.hooks-api :as hooks]
[clj-kondo.impl.utils]
[clojure.test :refer :all]
[hooks.clojure.core.ns]))
(defn- lint-ns [form config]
(binding [clj-kondo.impl.utils/*ctx* {:config config
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(hooks.clojure.core.ns/lint-ns {:node (hooks/parse-string
(binding [*print-meta* true]
(pr-str form)))
:config config})
@(:findings clj-kondo.impl.utils/*ctx*)))
(defn- lint-modules [form config]
(lint-ns form (assoc-in config [:linters :metabase/modules :level] :warning)))
(deftest ^:parallel module-checker-allowed-modules-test
(are [task-namespace] (=? [{:message "Module task should not be used in the api module. [:metabase/modules api :uses]"
:type :metabase/modules}]
(lint-modules
'(ns metabase.api.search
(:require
[metabase.request.core :as request]
[task-namespace :as task]))
'{:metabase/modules {api {:uses #{request}}
request {:api #{metabase.request.core}}}}))
metabase.task
metabase.task.core))
(deftest ^:parallel module-checker-api-namespaces-test
(is (=? [{:message "Namespace metabase.search.config is not an allowed external API namespace for the search module. [:metabase/modules search :api]"
:type :metabase/modules}]
(lint-modules
'(ns metabase.api.search
(:require
[metabase.search.config :as search.config]
[metabase.search.core :as search]))
'{:metabase/modules {api {:uses #{search}}
search {:api #{metabase.search.core}}}}))))
(deftest ^:parallel module-checker-api-namespaces-ignore-test
(is (empty? (lint-modules
'(ns metabase.api.search
(:require
^{:clj-kondo/ignore [:metabase/modules]}
[metabase.search.config :as search.config]
[metabase.search.core :as search]))
'{:metabase/modules {api {:uses #{search}}
search {:api #{metabase.search.core}}}}))))
(deftest ^:parallel module-checker-enterprise-test
(is (=? [{:message "Module search should not be used in the enterprise/search module. [:metabase/modules enterprise/search :uses]"
:type :metabase/modules}]
(lint-modules
'(ns metabase-enterprise.search.core
(:require
[metabase.search.core :as search]))
'{:metabase/modules {search {:api #{metabase.search.core}}}}))))
(deftest ^:parallel require-newlines-test
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/require-shape-checker {:level :warnings}}}
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(hooks.clojure.core.ns/lint-ns {:node (hooks/parse-string "
(ns metabase.task.cache
(:require [metabase.task.bunny]
[metabase.task.rabbit]))")})
(is (=? [{:message "Put your requires on a newline from the :require keyword [:metabase/require-shape-checker]",
:type :metabase/require-shape-checker}]
@(:findings clj-kondo.impl.utils/*ctx*)))))

View File

@@ -1,55 +0,0 @@
(ns hooks.clojure.test-test
(:require
[clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]
[clojure.edn :as edn]
[clojure.test :refer :all]
[hooks.clojure.test]))
(set! *warn-on-reflection* true)
(defn- deftest-warnings
[form]
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/disallow-hardcoded-driver-names-in-tests {:level :warning}}}
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(hooks.clojure.test/deftest {:node (api/parse-string form)
:config {:linters
{:metabase/disallow-hardcoded-driver-names-in-tests
{:drivers
#{:athena}}}}})
(mapv :message @(:findings clj-kondo.impl.utils/*ctx*))))
(deftest ^:parallel disallow-hardcoded-driver-names-in-tests-test
(is (= []
(deftest-warnings
"(mt/test-drivers (mt/normal-drivers)
(do-something))")))
(is (= ["Do not hardcode driver name :athena in driver tests! [:metabase/disallow-hardcoded-driver-names-in-tests]"]
(deftest-warnings
"(mt/test-drivers (mt/normal-drivers)
(when-not (= driver/*driver* :athena)
(do-something)))")))
(testing "make sure :clj-kondo/ignore is propagated correctly"
(is (= []
(deftest-warnings
"(mt/test-drivers (mt/normal-drivers)
#_{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
(when-not (= driver/*driver* :athena)
(do-something)))")))))
(deftest ^:parallel check-driver-keywords-test
(testing "Make sure we keep hooks.clojure.test/driver-keywords up to date"
(let [driver-keywords (-> (slurp ".clj-kondo/config.edn")
edn/read-string
(get-in [:linters :metabase/disallow-hardcoded-driver-names-in-tests :drivers]))
driver-modules (->> (slurp "modules/drivers/deps.edn")
edn/read-string
:deps
vals
(keep (comp keyword :local/root))
(into #{}))]
(doseq [driver driver-modules]
(is (contains? driver-keywords driver)
(format "hooks.clojure.test/driver-keywords should contain %s, please add it" driver))))))

View File

@@ -1,10 +0,0 @@
(ns hooks.common.modules-test
(:require
[clojure.test :refer :all]
[hooks.common.modules]))
(deftest ^:parallel module-test
(are [symb expected] (= expected
(hooks.common.modules/module symb))
'metabase.qp.middleware.wow 'qp
'metabase-enterprise.whatever.core 'enterprise/whatever))

View File

@@ -1,48 +0,0 @@
(ns hooks.common-test
(:require
[clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]
[clojure.test :refer :all]
[hooks.common]))
(deftest ^:parallel ignored-linters-test
(doseq [ignored [":metabase/modules"
"[:metabase/modules]"
"(:metabase/modules)"]
prefix ["#_"
"^"]
:let [s (format "%s{:clj-kondo/ignore %s} [metabase.search.config :as search.config]"
prefix
ignored)]]
(testing (pr-str s)
(is (= #{:metabase/modules}
(hooks.common/ignored-linters (api/parse-string s)))))))
(deftest ^:parallel merge-ignored-linters-test
(let [node-1 (api/parse-string "#_{:clj-kondo/ignore [:a]} [:a]")
node-2 (api/parse-string "#_{:clj-kondo/ignore [:b]} [:b]")
node-3 (api/parse-string "#_{:clj-kondo/ignore [:c]} [:c]")
node-1' (hooks.common/merge-ignored-linters node-1 node-2 node-3)]
(is (= #{:a :b :c}
(hooks.common/ignored-linters node-1')))
(testing "Updated metadata should match the shape Kondo expects"
(is (=? {:clj-kondo/ignore {:linters api/vector-node?}}
(meta node-1')))
(let [children (-> node-1' meta :clj-kondo/ignore :linters :children)]
(is (= (count children) 3))
(is (every? api/keyword-node? children))
(is (= [:a :b :c]
(sort (map api/sexpr children))))))))
(deftest ^:parallel node->qualified-symbol-test
(binding [clj-kondo.impl.utils/*ctx* {:namespaces (atom nil)}]
(is (= 'metabase.util.i18n/tru
(hooks.common/node->qualified-symbol (api/parse-string "metabase.util.i18n/tru"))))))
(deftest ^:parallel node->qualified-symbol-test-2
(binding [clj-kondo.impl.utils/*ctx* {:namespaces (atom {:clj {:clj '{hooks.common-test {:qualify-ns {i18n metabase.util.i18n}}}}})
:lang :clj
:base-lang :clj
:ns {:name 'hooks.common-test}}]
(is (= 'metabase.util.i18n/tru
(hooks.common/node->qualified-symbol (api/parse-string "i18n/tru"))))))

View File

@@ -1,71 +0,0 @@
(ns hooks.metabase.api.macros-test
(:require
[clj-kondo.hooks-api :as api]
[clojure.test :refer :all]
[hooks.metabase.api.macros]))
(defn- is-valid-node [x]
(is (api/node? x))
(doseq [child (:children x)]
(is-valid-node child)))
(deftest ^:parallel defendpoint-test
(letfn [(defendpoint [form]
(let [node (-> form pr-str api/parse-string)]
(is-valid-node node)
(-> {:node node}
hooks.metabase.api.macros/defendpoint
:node
api/sexpr)))]
(are [form expected] (= expected
(defendpoint form))
'(api.macros/defendpoint :post "/" :- [:map [:collection_id :int]]
"Create a new [[Timeline]]."
[_route-params
_query-params
{:keys [icon], collection-id :collection_id, :as body} :- [:map
[:name ms/NonBlankString]
[:default {:optional true} [:maybe :boolean]]]]
(body icon)
(body collection-id))
'(do
api.macros/defendpoint
:post
"/"
(do
[:map [:collection_id :int]]
[:map
[:name ms/NonBlankString]
[:default {:optional true} [:maybe :boolean]]])
(clojure.core/let [_route-params {}
_query-params {}
{:keys [icon], collection-id :collection_id, :as body} {}]
(body icon)
(body collection-id)))
'(api.macros/defendpoint :post "/"
[_route-params
_query-params
body
request
respond
raise]
(try
(respond :x)
(catch Throwable e
(raise e))))
'(do
api.macros/defendpoint
:post
"/"
(do)
(clojure.core/let [_route-params {}
_query-params {}
body {}
request {}
respond (clojure.core/constantly nil)
raise (clojure.core/constantly nil)]
(try
(respond :x)
(catch Throwable e
(raise e))))))))

View File

@@ -1,19 +0,0 @@
(ns hooks.metabase.model-persistence.test-util-test
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.test :refer :all]
[hooks.metabase.model-persistence.test-util]))
(deftest ^:parallel with-persistence-enabled-test
(is (= '(clojure.core/let [persist-models! (clojure.core/fn [])]
(persist-models!)
:wow)
(as-> '(mt/with-persistence-enabled! [persist-models!]
(persist-models!)
:wow) <>
(pr-str <>)
(hooks/parse-string <>)
{:node <>}
(hooks.metabase.model-persistence.test-util/with-persistence-enabled! <>)
(:node <>)
(hooks/sexpr <>)))))

View File

@@ -1,34 +0,0 @@
(ns hooks.metabase.settings.models.setting-test
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.test :refer :all]
[hooks.metabase.settings.models.setting]))
#_{:clj-kondo/ignore [:metabase/validate-deftest]}
(deftest ^:parallel defsetting-test
(let [node (-> '(defsetting active-users-count
(deferred-tru "Cached number of active users. Refreshed every 5 minutes.")
:visibility :admin
:type :integer
:default 0
:getter (fn []
(if-not ((requiring-resolve 'metabase.app-db.core/db-is-set-up?))
0
(cached-active-users-count))))
pr-str
hooks/parse-string)]
(is (= '(let [_ (deferred-tru "Cached number of active users. Refreshed every 5 minutes.")
_ {:default 0
:type :integer
:getter (fn []
(if-not ((requiring-resolve 'metabase.app-db.core/db-is-set-up?))
0
(cached-active-users-count)))
:visibility :admin}]
:active-users-count
(defn active-users-count "Docstring." [])
(defn active-users-count! "Docstring." [_value-or-nil]))
(-> {:node node, :ns 'hooks.metabase.settings.models.setting-test}
hooks.metabase.settings.models.setting/defsetting
:node
hooks/sexpr)))))

View File

@@ -1,33 +0,0 @@
(ns hooks.metabase.util.i18n-test
(:require [clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]
[clojure.string :as str]
[clojure.test :refer [deftest testing is are]]
[hooks.metabase.util.i18n]))
(defn- warnings
[form]
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/validate-string-or-str-args-to-i18n {:level :warning}
:metabase/validate-escaped-single-quotes-in-i18n {:level :warning}}}
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(hooks.metabase.util.i18n/strict-apostrophes {:node (api/parse-string (pr-str form))})
(mapv :message @(:findings clj-kondo.impl.utils/*ctx*))))
(deftest ^:parallel strict-apostrophes-warns
(are [form] (= ["Format string contains invalid single quote usage. Use '' for literals or '{...}' for escaping."]
(warnings (quote form)))
(metabase.util.i18n/tru "this isn't good bob")
(metabase.util.i18n/tru "'this is not good either'")
(metabase.util.i18n/tru "''this isn't''")
(metabase.util.i18n/tru "nope you can not escape 'like {{0}} so {{1}}'")
(metabase.util.i18n/tru (str "it works" "when strings" "are concatenated'"))))
(deftest ^:parallel strict-apostrophes-doesnt-warn-on-good-ones
(are [form] (= []
(warnings (quote form)))
(metabase.util.i18n/tru "this isn''t bad")
(metabase.util.i18n/tru (str "not sure why you'" "'d do this" "... but ok"))
(metabase.util.i18n/tru "you can '{{0}}' do things like that")
(metabase.util.i18n/tru "or '{{ this or }}}}}}}}}}' that")))

View File

@@ -1,19 +0,0 @@
(ns hooks.metabase.util.log.capture-test
(:require
[clj-kondo.hooks-api :as api]
[clojure.test :refer :all]
[hooks.metabase.util.log.capture]))
(deftest ^:parallel with-log-messages-for-level-test
(is (= '(clojure.core/let [messages-1 (clojure.core/fn [])
messages-2 (clojure.core/fn [])]
(concat (messages-1)
(messages-2)))
(-> (hooks.metabase.util.log.capture/with-log-messages-for-level
{:node (-> '(with-log-messages-for-level [messages-1 [my.namespace :debug]
messages-2 :info]
(concat (messages-1) (messages-2)))
pr-str
api/parse-string)})
:node
api/sexpr))))

View File

@@ -1,77 +0,0 @@
(ns hooks.metabase.util.log-test
(:require
[clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]
[clojure.string :as str]
[clojure.test :refer :all]
[hooks.metabase.util.log]))
(defn- warnings
[form]
(let [f (if (str/ends-with? (name (first form)) "f")
hooks.metabase.util.log/infof
hooks.metabase.util.log/info)]
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/validate-logging {:level :warning}}}
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(f {:node (api/parse-string (pr-str form))})
(mapv :message @(:findings clj-kondo.impl.utils/*ctx*)))))
(deftest ^:parallel warn-on-missing-format-args-test
(testing "should fail, missing a format arg"
(are [form] (= ["log format string expects 1 arguments instead of 0."]
(warnings (quote form)))
(metabase.util.log/warnf "Migration lock was acquired after %d retries.")
(metabase.util.log/warnf e "Migration lock was acquired after %d retries."))))
(deftest ^:parallel warn-on-too-many-format-args-test-1
(testing "should fail, too many format args"
(are [form] (= ["log format string expects 1 arguments instead of 2."]
(warnings (quote form)))
(metabase.util.log/warnf "Migration lock was acquired after %d retries." 1 2)
(metabase.util.log/warnf e "Migration lock was acquired after %d retries." 1 2))))
(deftest ^:parallel warn-on-too-many-format-args-test-2
(testing "should fail, too many format args"
(are [form] (= ["this looks like an i18n format string. Don't use identifiers like {0} in logging."
"Don't use metabase.util.log/warnf with no format string arguments, use metabase.util.log/warn instead."
"log format string expects 0 arguments instead of 1."]
(warnings (quote form)))
(metabase.util.log/warnf "Migration lock was acquired after {0} retries." 1)
(metabase.util.log/warnf e "Migration lock was acquired after {0} retries." 1))))
(deftest ^:parallel warn-on-format-args-without-logf-test
(testing "should fail, has format args but is warn rather than warnf"
(are [form] (= ["metabase.util.log/warn used with a format string, use metabase.util.log/warnf instead."]
(warnings (quote form)))
(metabase.util.log/warn "Migration lock was acquired after %d retries." 1)
(metabase.util.log/warn e "Migration lock was acquired after %d retries." 1))))
(deftest ^:parallel warn-on-format-without-logf-test
(testing "should fail, has format args but is warn rather than warnf"
(are [form] (= ["Use metabase.util.log/warnf instead of metabase.util.log/warn + format"]
(warnings (quote form)))
(metabase.util.log/warn (format "Migration lock was acquired after %d retries." 1))
(metabase.util.log/warn e (format "Migration lock was acquired after %d retries." 1)))))
(deftest ^:parallel warn-on-logf-with-no-format-args-test
(testing "should fail, has format args but is warn rather than warnf"
(are [form] (= ["Don't use metabase.util.log/warnf with no format string arguments, use metabase.util.log/warn instead."]
(warnings (quote form)))
(metabase.util.log/warnf "Migration lock cleared.")
(metabase.util.log/warnf e "Migration lock cleared."))))
(deftest ^:parallel warn-on-i18n-test-1
(testing "should fail -- should not use i18n/tru or i18n/trs"
(are [form] (= ["do not i18n the logs!"]
(warnings (quote form)))
(metabase.util.log/warnf (metabase.util.i18n/tru "Migration lock was acquired after {0} retries." 1))
(metabase.util.log/warnf e (metabase.util.i18n/tru "Migration lock was acquired after {0} retries." 1)))))
(deftest ^:parallel warn-on-i18n-test-2
(testing "should fail -- should not use i18n/tru or i18n/trs"
(are [form] (= ["do not i18n the logs!"]
(warnings (quote form)))
(metabase.util.log/warn (metabase.util.i18n/trs "Migration lock was acquired after {0} retries." 1))
(metabase.util.log/warn e (metabase.util.i18n/trs "Migration lock was acquired after {0} retries." 1)))))

View File

@@ -1,360 +1,31 @@
;; Cljfmt config. See https://github.com/weavejester/cljfmt#formatting-options
{:sort-ns-references?
true
:function-arguments-indentation
:community
:parallel?
true
:paths
["src"
"test"
"enterprise/backend"
"bin/build"
"bin/lint-migrations-file"
"bin/release-list"
"modules/drivers"
".clj-kondo/src"
".clj-kondo/test"
#_"dev"]
;; See https://github.com/weavejester/cljfmt/blob/master/cljfmt/resources/cljfmt/indents/clojure.clj for the default
;; definitions
;;
;; [[:block 0]]
;; ^ number of special args
;;
;; `:block` indentation means format with all arguments indented 2 spaces except for the 'special' arguments which
;; normally are supposed to go on the first line. e.g. `let` gets [[:block 1]] because it has one 'special' arg.
;;
;; [[:inner 0]]
;; ^ depth
;; [[:inner 2 0]]
;; ^ argument index to restrict this rule to
;;
;; `:inner` indentation is like `:defn` indentation. `[[:inner 0]]` (default for `defn`) means all arguments at depth
;; zero get indented 2 spaces if not on the first line regardless of number of elements on the first line.
:extra-indents
{;; clojure.core stuff
fn* [[:inner 0]]
let* [[:block 1]]
with-meta [[:default]]
;; library stuff. Libraries that specify their own `:style/indent` specs are included in the section after this
;; since it is generated automatically.
clojure.core.logic/fresh [[:block 1]]
clojure.core.logic/matcha [[:block 1]]
clojure.core.logic/run [[:block 2]]
clojure.core.match/match [[:block 1]]
clojure.test.check.properties/for-all [[:block 1]]
clojure.test.check.properties/for-all* [[:block 1]]
clojure.test.check/quick-check [[:block 1]]
clojure.tools.macro/macrolet [[:block 1] [:inner 2 0]]
clojurewerkz.quartzite.jobs/build [[:default]]
clojurewerkz.quartzite.jobs/with-description [[:default]]
clojurewerkz.quartzite.jobs/with-identity [[:default]]
clojurewerkz.quartzite.schedule.cron/schedule [[:default]]
clojurewerkz.quartzite.schedule.cron/with-misfire-handling-instruction-do-nothing [[:default]]
clojurewerkz.quartzite.triggers/build [[:default]]
clojurewerkz.quartzite.triggers/with-description [[:default]]
clojurewerkz.quartzite.triggers/with-identity [[:default]]
clojurewerkz.quartzite.triggers/with-priority [[:default]]
clojurewerkz.quartzite.triggers/with-schedule [[:default]]
def-map-type [[:block 2] [:inner 1]]
defprotocol+ [[:block 1] [:inner 1]]
defrecord+ [[:block 2] [:inner 1]]
deftype+ [[:block 2] [:inner 1]]
net.cgrand.macrovich/case [[:block 0]]
potemkin.namespaces/import-vars [[:default]]
potemkin.types/def-map-type [[:block 2] [:inner 1]]
potemkin.types/defprotocol+ [[:block 1] [:inner 1]]
potemkin.types/defrecord+ [[:block 2] [:inner 1]]
potemkin.types/deftype+ [[:block 2] [:inner 1]]
potemkin/def-map-type [[:block 2] [:inner 1]]
potemkin/defprotocol+ [[:block 1] [:inner 1]]
potemkin/defrecord+ [[:block 2] [:inner 1]]
potemkin/deftype+ [[:block 2] [:inner 1]]
potemkin/import-vars [[:default]]
taoensso.nippy/extend-freeze [[:inner 0]]
taoensso.nippy/extend-thaw [[:inner 0]]
throttle.core/with-throttling [[:block 1]]
;; the rest of the stuff in this list is generated automatically based on `:style/indent` specs
;; by running [[dev.generate-cljfmt-config/specs]]... use that code to update this list
metabase-enterprise.cache.config-test/with-temp-persist-models [[:block 1]]
metabase-enterprise.impersonation.util-test/with-impersonations! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-gtaps! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-user-attributes [[:block 2]]
metabase-enterprise.serialization.test-util/with-dbs [[:inner 0]]
metabase-enterprise.serialization.test-util/with-random-dump-dir [[:block 1]]
metabase-enterprise.serialization.test-util/with-world [[:block 0]]
metabase-enterprise.sso.integrations.jwt-test/with-users-with-email-deleted [[:block 1]]
metabase-enterprise.test/with-gtaps! [[:inner 0]]
metabase-enterprise.test/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.test/with-user-attributes [[:block 2]]
metabase.actions.actions-test/with-actions-test-data-and-actions-permissively-enabled [[:block 0]]
metabase.actions.test-util/with-actions [[:block 1]]
metabase.actions.test-util/with-actions-disabled [[:block 0]]
metabase.actions.test-util/with-actions-enabled [[:block 0]]
metabase.actions.test-util/with-actions-test-data [[:inner 0]]
metabase.actions.test-util/with-actions-test-data-and-actions-enabled [[:block 0]]
metabase.actions.test-util/with-actions-test-data-tables [[:block 1]]
metabase.actions.test-util/with-empty-db [[:inner 0]]
metabase.actions.test-util/with-temp-test-data [[:inner 0]]
metabase.api.common/check [[:default]]
metabase.api.common/define-routes [[:block 0]]
metabase.api.common/let-404 [[:block 1]]
metabase.app-db.core/with-application-db [[:inner 0]]
metabase.app-db.liquibase/with-liquibase [[:block 1]]
metabase.app-db.liquibase/with-scope-locked [[:block 1]]
metabase.app-db.schema-migrations-test.impl/test-migrations [[:block 2]]
metabase.cmd.copy/step [[:block 1]]
metabase.cmd.copy/with-connection-rollback-only [[:block 1]]
metabase.cmd.copy/with-disabled-db-constraints [[:block 2]]
metabase.collections.api-test/with-collection-hierarchy [[:block 1]]
metabase.collections.api-test/with-french-user-and-personal-collection [[:block 2]]
metabase.collections.api-test/with-some-children-of-collection [[:block 1]]
metabase.collections.models.collection-test/with-collection-hierarchy [[:block 1]]
metabase.collections.models.collection-test/with-current-user-perms-for-collections [[:block 1]]
metabase.collections.models.collection-test/with-personal-and-impersonal-collections [[:block 1]]
metabase.config/build-type-case [[:block 0]]
metabase.dashboards.models.dashboard-test/with-dash-in-collection [[:inner 0]]
metabase.driver-api.core/match [[:inner 0]]
metabase.driver-api.core/match-one [[:inner 0]]
metabase.driver-api.core/replace [[:inner 0]]
metabase.driver-api.core/replace-in [[:inner 0]]
metabase.driver.bigquery-cloud-sdk.query-processor/with-temporal-type [[:default]]
metabase.driver.druid.client/DELETE [[:default]]
metabase.driver.druid.client/GET [[:default]]
metabase.driver.druid.client/POST [[:default]]
metabase.driver.druid.query-processor-test/druid-query [[:block 0]]
metabase.driver.druid.query-processor-test/druid-query-returning-rows [[:block 0]]
metabase.driver.mongo.query-processor/mongo-let [[:block 1]]
metabase.driver.sql-jdbc.actions/with-jdbc-transaction [[:block 1]]
metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection [[:block 1]]
metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info [[:block 1]]
metabase.driver.sql.query-processor/apply-top-level-clause [[:default]]
metabase.driver/with-driver [[:block 1]]
metabase.embedding.api.embed-test/do-response-formats [[:block 1]]
metabase.embedding.api.embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-card-referencing [[:block 3]]
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing [[:block 3]]
metabase.embedding.api.embed-test/with-new-secret-key [[:block 0]]
metabase.embedding.api.embed-test/with-temp-card [[:block 1]]
metabase.embedding.api.embed-test/with-temp-dashcard [[:block 1]]
metabase.embedding.api.preview-embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.embedding.api.preview-embed-test/with-new-secret-key [[:block 0]]
metabase.lib.binning/with-binning [[:default]]
metabase.lib.core/with-binning [[:default]]
metabase.lib.core/with-join-alias [[:default]]
metabase.lib.core/with-join-conditions [[:default]]
metabase.lib.join/with-join-alias [[:default]]
metabase.lib.join/with-join-conditions [[:default]]
metabase.lib.test-util.macros/$ids [[:inner 0]]
metabase.lib.test-util.macros/mbql-5-query [[:inner 0]]
metabase.lib.test-util.macros/mbql-query [[:inner 0]]
metabase.lib.util.match/match [[:inner 0]]
metabase.lib.util.match/match-one [[:inner 0]]
metabase.lib.util.match/match-lite [[:inner 0]]
metabase.lib.util.match/match-lite-recursive [[:inner 0]]
metabase.lib.util.match/replace [[:inner 0]]
metabase.lib.util.match/replace-in [[:inner 0]]
metabase.models.interface/define-batched-hydration-method [[:inner 0]]
metabase.models.interface/define-simple-hydration-method [[:inner 0]]
metabase.parameters.chain-filter-test/with-clean-field-values-for-field [[:block 1]]
metabase.parameters.chain-filter-test/with-fk-field-to-field-remapping [[:block 0]]
metabase.parameters.chain-filter-test/with-human-readable-values-remapping [[:block 0]]
metabase.permissions.core/allow-changing-all-users-group-members [[:block 0]]
metabase.permissions.core/disable-perms-cache [[:block 0]]
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing [[:block 3]]
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing [[:block 3]]
metabase.public-sharing.api-test/with-temp-public-card [[:block 1]]
metabase.public-sharing.api-test/with-temp-public-dashboard [[:block 1]]
metabase.public-sharing.api-test/with-temp-public-dashboard-and-card [[:block 1]]
metabase.pulse.api.alert-test/with-alert-in-collection [[:block 1]]
metabase.pulse.dashboard-subscription-test/with-link-card-fixture-for-dashboard [[:block 2]]
metabase.pulse.models.pulse-test/with-dashboard-subscription-in-collection [[:block 1]]
metabase.pulse.models.pulse-test/with-pulse-in-collection [[:inner 0]]
metabase.queries.api.card-test/with-cards-in-readable-collection [[:block 1]]
metabase.queries.api.card-test/with-cards-in-writeable-collection [[:block 1]]
metabase.queries.api.card-test/with-ordered-items [[:inner 0]]
metabase.queries.api.card-test/with-temp-native-card! [[:block 1]]
metabase.queries.api.card-test/with-temp-native-card-with-params! [[:block 1]]
metabase.query-processor-test.pipeline-queries-test/pmbql-query [[:block 1]]
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query [[:block 1]]
metabase.query-processor.debug/debug> [[:block 0]]
metabase.query-processor.error-type/deferror [[:block 1]]
metabase.query-processor.middleware.cache-backend.interface/with-cached-results [[:block 4]]
metabase.query-processor.middleware.process-userland-query-test/with-query-execution [[:block 1]]
metabase.query-processor.store/cached [[:block 1]]
metabase.query-processor.store/with-metadata-provider [[:inner 0]]
metabase.query-processor.streaming/streaming-response [[:block 1]]
metabase.query-processor.test-util/format-rows-by [[:default]]
metabase.request.core/as-admin [[:block 0]]
metabase.request.core/with-current-request [[:inner 0]]
metabase.request.core/with-current-user [[:inner 0]]
metabase.request.core/with-limit-and-offset [[:inner 0]]
metabase.request.session/as-admin [[:block 0]]
metabase.request.session/with-current-user [[:inner 0]]
metabase.sample-data.impl-test/with-temp-sample-database-db [[:block 1]]
metabase.search.api-test/do-test-users [[:block 1]]
metabase.server.middleware.session/as-admin [[:block 0]]
metabase.server.middleware.session/with-current-user [[:inner 0]]
metabase.server.streaming-response-test/with-streaming-response-thread-pool [[:block 0]]
metabase.server.streaming-response-test/with-test-driver-db! [[:block 0]]
metabase.server.streaming-response/streaming-response [[:block 2]]
metabase.session.api/http-401-on-error [[:block 0]]
metabase.settings.core/define-multi-setting [[:inner 0]]
metabase.settings.core/defsetting [[:block 1]]
metabase.settings.models.setting.multi-setting/define-multi-setting [[:inner 0]]
metabase.settings.models.setting/defsetting [[:block 1]]
metabase.sync.fetch-metadata/log-if-error [[:block 1]]
metabase.sync.sync-metadata.fields-test/with-test-db [[:block 0]]
metabase.sync.util/do-with-start-and-finish-debug-logging [[:default]]
metabase.sync.util/sum-for [[:block 1]]
metabase.sync.util/sync-operation [[:block 3]]
metabase.sync.util/with-db-logging-disabled [[:default]]
metabase.sync.util/with-duplicate-ops-prevented [[:default]]
metabase.sync.util/with-emoji-progress-bar [[:block 1]]
metabase.sync.util/with-error-handling [[:block 1]]
metabase.sync.util/with-start-and-finish-logging [[:default]]
metabase.sync.util/with-start-and-finish-logging* [[:default]]
metabase.sync.util/with-sync-events [[:default]]
metabase.task-history.models.task-history/with-task-history [[:block 1]]
metabase.task.core/rerun-on-error [[:block 1]]
metabase.task.impl/rerun-on-error [[:block 1]]
metabase.test.data.datasets/test-driver [[:inner 0]]
metabase.test.data.datasets/test-drivers [[:inner 0]]
metabase.test.data.one-off-dbs/with-blank-db [[:block 0]]
metabase.test.data.one-off-dbs/with-blueberries-db [[:block 0]]
metabase.test.data.users/with-test-user [[:inner 0]]
metabase.test.data/$ids [[:inner 0]]
metabase.test.data/dataset [[:inner 0]]
metabase.test.data/mbql-query [[:inner 0]]
metabase.test.data/native-query [[:block 0]]
metabase.test.data/query [[:block 1]]
metabase.test.data/run-mbql-query [[:inner 0]]
metabase.test.data/with-empty-h2-app-db! [[:block 0]]
metabase.test.data/with-temp-copy-of-db [[:block 0]]
metabase.test.util.public-settings/with-additional-premium-features [[:block 1]]
metabase.test.util.public-settings/with-premium-features [[:block 1]]
metabase.test.util.thread-local/test-helpers-set-global-values! [[:block 0]]
metabase.test.util/discard-setting-changes [[:block 1]]
metabase.test.util/let-url [[:block 1]]
metabase.test.util/throw-if-called! [[:block 1]]
metabase.test.util/with-temp-scheduler [[:block 0]]
metabase.test.util/with-temp-vals-in-db [[:block 3]]
metabase.test.util/with-user-in-groups [[:inner 0]]
metabase.test/$ids [[:inner 0]]
metabase.test/as-admin [[:inner 0]]
metabase.test/dataset [[:inner 0]]
metabase.test/discard-setting-changes [[:block 1]]
metabase.test/format-rows-by [[:default]]
metabase.test/let-url [[:block 1]]
metabase.test/mbql-query [[:inner 0]]
metabase.test/native-query [[:block 0]]
metabase.test/query [[:block 1]]
metabase.test/run-mbql-query [[:inner 0]]
metabase.test/test-driver [[:inner 0]]
metabase.test/test-drivers [[:inner 0]]
metabase.test/test-helpers-set-global-values! [[:block 0]]
metabase.test/throw-if-called! [[:block 1]]
metabase.test/with-actions [[:block 1]]
metabase.test/with-actions-disabled [[:block 0]]
metabase.test/with-actions-enabled [[:block 0]]
metabase.test/with-actions-test-data [[:inner 0]]
metabase.test/with-actions-test-data-and-actions-enabled [[:block 0]]
metabase.test/with-actions-test-data-tables [[:block 1]]
metabase.test/with-additional-premium-features [[:block 1]]
metabase.test/with-current-user [[:inner 0]]
metabase.test/with-driver [[:block 1]]
metabase.test/with-empty-db [[:inner 0]]
metabase.test/with-empty-h2-app-db! [[:block 0]]
metabase.test/with-metadata-provider [[:inner 0]]
metabase.test/with-premium-features [[:block 1]]
metabase.test/with-temp [[:inner 0]]
metabase.test/with-temp-copy-of-db [[:block 0]]
metabase.test/with-temp-scheduler [[:block 0]]
metabase.test/with-temp-test-data [[:inner 0]]
metabase.test/with-temp-vals-in-db [[:block 3]]
metabase.test/with-test-user [[:inner 0]]
metabase.test/with-user-in-groups [[:inner 0]]
metabase.timeseries-query-processor-test.util/test-timeseries-drivers [[:block 0]]
metabase.timeseries-query-processor-test/run-mbql-query [[:inner 0]]
metabase.upload.impl-test/with-upload-table! [[:inner 0]]
metabase.upload.types/with-currency [[:default]]
metabase.upload.types/with-parens [[:default]]
metabase.util/for-map [[:block 1]]
metabase.util.async/cancelable-thread [[:block 0]]
metabase.util.date-2/with-time-zone-same-instant [[:default]]
metabase.util.encryption-test/with-secret-key [[:block 1]]
metabase.util.honey-sql-2/with-database-type-info [[:default]]
metabase.util.i18n/deferred-trs [[:default]]
metabase.util.i18n/deferred-tru [[:default]]
metabase.util.i18n/trs [[:default]]
metabase.util.i18n/trsn [[:default]]
metabase.util.i18n/tru [[:default]]
metabase.util.i18n/trun [[:default]]
metabase.util.jvm/auto-retry [[:block 1]]
metabase.util.jvm/varargs [[:block 1]]
metabase.util.jvm/with-us-locale [[:block 0]]
metabase.util.malli/disable-enforcement [[:block 0]]
metabase.util.malli/with [[:default]]
metabase.util.malli/with-api-error-message [[:default]]
metabase.util.namespaces/import-fns [[:default]]
metabase.util.queue-test/await-test-while [[:block 1]]
metabase.util/auto-retry [[:block 1]]
metabase.util/case-enum [[:block 1]]
metabase.util/ignore-exceptions [[:block 0]]
metabase.util/or-with [[:block 1]]
metabase.util/profile [[:block 1]]
metabase.util/prog1 [[:inner 0]]
metabase.util/varargs [[:inner 0]]
metabase.util/with-us-locale [[:block 0]]
metabuild-common.core/exit-when-finished-nonzero-on-exception [[:block 0]]
metabuild-common.core/step [[:block 1]]
metabuild-common.entrypoint/exit-when-finished-nonzero-on-exception [[:block 0]]
metabuild-common.steps/step [[:block 1]]
methodical.core/defmethod [[:inner 0]]
methodical.core/defmulti [[:inner 0]]
methodical.core/with-dispatcher [[:default]]
methodical.core/with-method-table [[:default]]
methodical.core/with-prefers [[:default]]
methodical.interface/with-dispatcher [[:default]]
methodical.interface/with-method-table [[:default]]
methodical.interface/with-prefers [[:default]]
methodical.macros/defmethod [[:inner 0]]
methodical.macros/defmulti [[:inner 0]]
saml20-clj.sp.response/validate-confirmation-datas [[:block 1]]
toucan2.connection/with-transaction [[:block 1]]
toucan2.core/build [[:block 0]]
toucan2.core/compile [[:block 0]]
toucan2.core/debug [[:block 0]]
toucan2.core/define-after-insert [[:inner 0]]
toucan2.core/define-after-select [[:inner 0]]
toucan2.core/define-after-update [[:inner 0]]
toucan2.core/define-before-delete [[:inner 0]]
toucan2.core/define-before-insert [[:inner 0]]
toucan2.core/define-before-select [[:inner 0]]
toucan2.core/define-default-fields [[:inner 0]]
toucan2.core/define-named-query [[:block 1]]
toucan2.core/deftransforms [[:block 1]]
toucan2.core/with-transaction [[:block 1]]
toucan2.tools.after-insert/define-after-insert [[:inner 0]]
toucan2.tools.after-select/define-after-select [[:inner 0]]
toucan2.tools.after-update/define-after-update [[:inner 0]]
toucan2.tools.after/define-after-impl [[:default]]
toucan2.tools.before-delete/define-before-delete [[:inner 0]]
toucan2.tools.before-insert/define-before-insert [[:inner 0]]
toucan2.tools.before-select/define-before-select [[:inner 0]]
toucan2.tools.compile/build [[:block 0]]
toucan2.tools.compile/compile [[:block 0]]
toucan2.tools.compile/resolved [[:block 0]]
toucan2.tools.debug/debug [[:block 0]]
toucan2.tools.default-fields/define-default-fields [[:inner 0]]
toucan2.tools.named-query/define-named-query [[:block 1]]
toucan2.tools.simple-out-transform/define-out-transform [[:inner 0]]
toucan2.tools.transformed/deftransforms [[:block 1]]
toucan2.tools.with-temp/with-temp [[:inner 0]]
toucan2.util/try-with-error-context [[:inner 0]]}}
;; https://github.com/weavejester/cljfmt/blob/master/cljfmt/resources/cljfmt/indents/clojure.clj
{:extra-indents
{auto-parse [[:inner 0]]
cond-> [[:inner 0]]
check [[:inner 0]]
deferred-tru [[:inner 0]]
defprotocol+ [[:block 1] [:inner 1]]
potemkin/defprotocol+ [[:block 1] [:inner 1]]
let-404 [[:block 1]]
metabase.mbql.util/match-one [[:inner 0]]
merge-with [[:inner 0]]
metabase.test/dataset [[:inner 0]]
metabase.test/formatted-rows [[:inner 0]]
metabase.test/mbql-query [[:inner 0]]
metabase.test/test-drivers [[:inner 0]]
metabase.test/with-temporary-setting-values [[:block 1]]
;; If we just treat these as functions we can ignore this, despite some files not being consistent
;metabase.models.permissions/set-has-full-permissions-for-set? [[:inner 0]]
;metabase.models.permissions/set-has-full-permissions? [[:inner 0]]
;metabase.models.permissions/set-has-partial-permissions? [[:inner 0]]
metabase.models.permissions/set-has-partial-permissions-for-set? [[:inner 0]]
publish-event! [[:block 1]]
metabase.query-processor.streaming/streaming-response [[:inner 0]]
metabase.util/prog1 [[:inner 0]]
metabase.util/select-keys-when [[:inner 0]]
;; This supports a lot of existing formatting, but these are functions so the
;; existing formatting is probably wrong.
;toucan2.core/select [[:inner 0]]
;toucan2.core/update! [[:inner 0]]
}
:sort-ns-references? true}

View File

@@ -1,11 +1,11 @@
FROM mcr.microsoft.com/vscode/devcontainers/java:21
FROM mcr.microsoft.com/vscode/devcontainers/java:11
# Set up nodesource, install node, yarn, fontconfig for static viz, rlwrap for dev ergonomics
RUN ( curl -fsSL https://deb.nodesource.com/setup_22.x | bash ) \
RUN ( curl -fsSL https://deb.nodesource.com/setup_18.x | bash ) \
&& export DEBIAN_FRONTEND=noninteractive \
&& apt-key adv --refresh-keys --keyserver keyserver.ubuntu.com \
&& apt-get update && apt-get -y install --no-install-recommends nodejs yarn rlwrap fontconfig
# install Clojure
RUN curl https://download.clojure.org/install/linux-install-1.12.0.1488.sh | bash
RUN curl https://download.clojure.org/install/linux-install-1.11.1.1262.sh | bash

View File

@@ -20,67 +20,69 @@
(clojure-mode
;; Specify which arg is the docstring for certain macros
;; (Add more as needed)
(eval . (put 'define-premium-feature 'clojure-doc-string-elt 2))
(eval . (put 'api.macros/defendpoint 'clojure-doc-string-elt 3))
(eval . (put 'defsetting 'clojure-doc-string-elt 2))
(eval . (put 'setting/defsetting 'clojure-doc-string-elt 2))
(eval . (put 's/defn 'clojure-doc-string-elt 2))
(eval . (put 'p.types/defprotocol+ 'clojure-doc-string-elt 2))
(eval . (put 'methodical/defmethod 'clojure-doc-string-elt 3))
(eval . (put 'methodical/defmulti 'clojure-doc-string-elt 2))
(eval . (put 'mi/define-simple-hydration-method 'clojure-doc-string-elt 3))
(eval . (put 'defendpoint-schema 'clojure-doc-string-elt 3))
(eval . (put 'defendpoint 'clojure-doc-string-elt 3))
(eval . (put 'defendpoint-async 'clojure-doc-string-elt 3))
(eval . (put 'define-premium-feature 'clojure-doc-string-elt 2))
(eval . (put 'api/defendpoint-schema 'clojure-doc-string-elt 3))
(eval . (put 'api/defendpoint 'clojure-doc-string-elt 3))
(eval . (put 'api/defendpoint-async 'clojure-doc-string-elt 3))
(eval . (put 'defsetting 'clojure-doc-string-elt 2))
(eval . (put 'setting/defsetting 'clojure-doc-string-elt 2))
(eval . (put 's/defn 'clojure-doc-string-elt 2))
(eval . (put 'p.types/defprotocol+ 'clojure-doc-string-elt 2))
(eval . (put 'methodical/defmethod 'clojure-doc-string-elt 3))
(eval . (put 'methodical/defmulti 'clojure-doc-string-elt 2))
(eval . (put 'mi/define-simple-hydration-method 'clojure-doc-string-elt 3))
(eval . (put 'mi/define-batched-hydration-method 'clojure-doc-string-elt 3))
(eval . (put 'mr/def 'clojure-doc-string-elt 2))
(eval . (put 'mu/defn 'clojure-doc-string-elt 2))
(eval . (put 'mu/defn 'clojure-doc-string-elt 2))
;; Define custom indentation for functions inside metabase.
;; This list isn't complete; add more forms as we come across them.
;;
;; `put-clojure-indent' is a safe-local-eval-function, so use a bunch of calls to that
;; instead of one call to `define-clojure-indent'
(eval . (put-clojure-indent 'api/let-404 1))
(eval . (put-clojure-indent 'c/step 1))
(eval . (put-clojure-indent 'impl/test-migrations 2))
(eval . (put-clojure-indent 'let-404 0))
(eval . (put-clojure-indent 'lib.schema.match/match '(:defn)))
(eval . (put-clojure-indent 'lib.schema.match/match-one '(:defn)))
(eval . (put-clojure-indent 'lib.schema.match/replace '(:defn)))
(eval . (put-clojure-indent 'lib.schema.match/replace-in '(:defn)))
(eval . (put-clojure-indent 'lib.util.match/replace '(:defn)))
(eval . (put-clojure-indent 'macros/case 0))
(eval . (put-clojure-indent 'match 1))
(eval . (put-clojure-indent 'mt/dataset 1))
(eval . (put-clojure-indent 'mt/format-rows-by '(:form)))
(eval . (put-clojure-indent 'mt/query 1))
(eval . (put-clojure-indent 'mt/test-driver 1))
(eval . (put-clojure-indent 'mt/test-drivers 1))
(eval . (put-clojure-indent 'mt/with-anaphora 1))
(eval . (put-clojure-indent 'prop/for-all 1))
(eval . (put-clojure-indent 'api/let-404 1))
(eval . (put-clojure-indent 'c/step 1))
(eval . (put-clojure-indent 'impl/test-migrations 2))
(eval . (put-clojure-indent 'let-404 0))
(eval . (put-clojure-indent 'macros/case 0))
(eval . (put-clojure-indent 'match 1))
(eval . (put-clojure-indent 'mbql.match/match 1))
(eval . (put-clojure-indent 'mbql.match/match-one 1))
(eval . (put-clojure-indent 'mbql.match/replace 1))
(eval . (put-clojure-indent 'mbql.match/replace-in 2))
(eval . (put-clojure-indent 'mbql.u/replace 1))
(eval . (put-clojure-indent 'mi/define-methods '(:form)))
(eval . (put-clojure-indent 'mt/dataset 1))
(eval . (put-clojure-indent 'mt/format-rows-by 1))
(eval . (put-clojure-indent 'mt/query 1))
(eval . (put-clojure-indent 'mt/test-drivers 1))
(eval . (put-clojure-indent 'mt/test-driver 1))
(eval . (put-clojure-indent 'prop/for-all 1))
(eval . (put-clojure-indent 'qp.streaming/streaming-response 1))
(eval . (put-clojure-indent 'tc/quick-check 1))
(eval . (put-clojure-indent 'u/profile 1))
(eval . (put-clojure-indent 'u/prog1 1))
(eval . (put-clojure-indent 'u/select-keys-when '(:form)))
(eval . (put-clojure-indent 'with-meta '(:form)))
(eval . (put-clojure-indent 'u/prog1 1))
(eval . (put-clojure-indent 'u/select-keys-when 1))
(eval . (put-clojure-indent 'with-meta '(:form)))
(eval . (put-clojure-indent 'tc/quick-check 1))
;; these ones have to be done with `define-clojure-indent' for now because of upstream bug
;; https://github.com/clojure-emacs/clojure-mode/issues/600 once that's resolved we should use `put-clojure-indent'
;; instead. Please don't add new entries unless they don't work with `put-clojure-indent'
(eval . (define-clojure-indent
(l/matcha '(1 (:defn)))
(l/matche '(1 (:defn)))
(l/matcha '(1 (:defn)))
(l/matche '(1 (:defn)))
(p.types/def-abstract-type '(1 (:defn)))
(p.types/defprotocol+ '(1 (:defn)))
(p.types/defrecord+ '(2 nil nil (:defn)))
(p.types/deftype+ '(2 nil nil (:defn)))
(p/def-map-type '(2 nil nil (:defn)))
(p/defprotocol+ '(1 (:defn)))
(p/defrecord+ '(2 nil nil (:defn)))
(p/deftype+ '(2 nil nil (:defn)))
(tools.macro/macrolet '(1 ((:defn)) :form))))
(p.types/defprotocol+ '(1 (:defn)))
(p.types/defrecord+ '(2 nil nil (:defn)))
(p.types/deftype+ '(2 nil nil (:defn)))
(p/def-map-type '(2 nil nil (:defn)))
(p/defprotocol+ '(1 (:defn)))
(p/defrecord+ '(2 nil nil (:defn)))
(p/deftype+ '(2 nil nil (:defn)))
(tools.macro/macrolet '(1 ((:defn)) :form))))
(cider-clojure-cli-aliases . "dev:drivers:drivers-dev:ee:ee-dev:user")
(clojure-indent-style . always-align)
(cljr-favor-prefix-notation . nil)
(cljr-insert-newline-after-require . t)
(cljr-print-right-margin . 118)
(clojure-docstring-fill-column . 118)
(cider-preferred-build-tool . clojure-cli)
(cider-default-cljs-repl . shadow-select)

View File

@@ -1,4 +1,2 @@
frontend/src/cljs
frontend/src/cljs_release
e2e/support/cypress_sample_database.js
e2e/support/cypress_sample_instance_data.js

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