Compare commits

..

16 Commits

Author SHA1 Message Date
adam-james
ff11bd7f79 Merge branch 'master' into attach-render-errors-subscriptions 2024-06-03 08:04:14 -07:00
Adam James
39c81419e8 Add a card query error test 2024-05-28 17:28:44 -07:00
adam-james
0d95540349 Merge branch 'master' into attach-render-errors-subscriptions 2024-05-28 16:48:27 -07:00
Adam James
f3a26b5135 was using the wrong key here, not sure why, maybe it changed recently? 2024-05-28 15:32:30 -07:00
adam-james
a5c33e1756 Merge branch 'master' into attach-render-errors-subscriptions 2024-05-28 13:33:42 -07:00
Adam James
b89f86c568 add back some changes from master 2024-05-28 10:52:44 -07:00
Adam James
e108633f36 Fix lint errors 2024-05-27 16:57:54 -07:00
Adam James
a54ef9bc99 fix some code looking at messages when it should now be attachments 2024-05-27 15:48:52 -07:00
adam-james
9705d8976f Merge branch 'master' into attach-render-errors-subscriptions 2024-05-27 15:13:54 -07:00
Adam James
a222ac98c0 WIP on making the test actually capture the error behaviour
Not sure if this is actually doing what I need it to yet
2024-03-25 08:26:08 -07:00
Adam James
520b1f6320 Merge branch 'attach-render-errors-subscriptions' of https://github.com/metabase/metabase into attach-render-errors-subscriptions 2024-03-22 16:15:36 -07:00
Adam James
4a9d63ebc7 Add test that asserts error attachment exists when error occurs. 2024-03-22 16:13:22 -07:00
adam-james
e16bfa4a76 Merge branch 'master' into attach-render-errors-subscriptions 2024-03-22 13:50:16 -07:00
adam-james
d0148baa24 Merge branch 'master' into attach-render-errors-subscriptions 2024-03-21 08:57:44 -07:00
Adam James
3e5d3985d8 Fix viz-settings in error attachment 2024-03-19 15:31:36 -07:00
Adam James
6a61796277 Attach Static-viz render errors to subscription emails
Fixes: #39814

Our static-viz renders an error message into subscription emails if the render failed, but it provides no further
details as to what caused the failure.

This PR adds a .txt attachment in the case of a card render or card query failure. The contents of the text file are a
short message about the problem with the card's name, the visualization_settings that the card was rendered with, and
the stacktrace.

This should be helpful if a user is reporting a render error as a bug since this can be attached or copy/pasted into
the bug report.

The attachment was used so that the actualy body of the email remains clean/legible.
2024-03-19 15:19:43 -07:00
15958 changed files with 1550754 additions and 2196578 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,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,7 @@
(ns macros.metabase.api.common)
(defmacro define-routes
"Macro for api.common/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

@@ -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.query-field-test
(:require [macros.common]))
(defmacro with-test-setup [& body]
`(let [~(macros.common/ignore-unused 'card-id) 1
~(macros.common/ignore-unused 'table-id) 2
~(macros.common/ignore-unused 'tax-id) 3
~(macros.common/ignore-unused 'total-id) 4]
~@body))

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

@@ -0,0 +1,6 @@
(ns macros.metabase-enterprise.advanced-permissions.api.util-test
(:require [macros.common]))
(defmacro with-impersonations! [impersonations-and-attributes-map & body]
`(let [~(macros.common/ignore-unused '&group) ~impersonations-and-attributes-map]
~@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
`(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

@@ -0,0 +1,329 @@
(ns hooks.clojure.core
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.set :as set]
[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 symbols-allowed-in-fns-not-ending-in-an-exclamation-point
'#{;; 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/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/migrate!
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-default-dataset!
metabase.test.data.impl.get-or-create/set-test-db-permissions!
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? symbols-allowed-in-fns-not-ending-in-an-exclamation-point 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))
(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- 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.
(vary-meta (first (:children node)) (partial merge (meta require-node) (meta node)))
;; this should also be dead code since we require requires to be vectors
(hooks/token-node? node)
(vary-meta node (partial merge (meta 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 [ns-form-node]
(some-> (some (fn [node]
(when (and (hooks/token-node? node)
(not= (hooks/sexpr node) 'ns))
node))
(:children ns-form-node))
hooks/sexpr))
(defn- module
"E.g.
(module 'metabase.qp.middleware.wow) => 'metabase.qp"
[ns-symb]
(some-> (re-find #"^metabase\.[^.]+" (str ns-symb)) symbol))
(defn- ignored-namespace? [ns-symb config]
(some
(fn [pattern-str]
(re-find (re-pattern pattern-str) (str ns-symb)))
(:ignored-namespace-patterns config)))
(defn- module-api-namespaces
"Set API namespaces 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 namespace with the same name as the module e.g.
`metabase.db`."
[module config]
(let [module-config (get-in config [:api-namespaces module])]
(cond
(= module-config :any)
nil
(set? module-config)
module-config
:else
#{module})))
(defn- lint-modules [ns-form-node config]
(let [ns-symb (ns-form-node->ns-symb ns-form-node)]
(when-not (ignored-namespace? ns-symb config)
(when-let [current-module (module ns-symb)]
(let [allowed-modules (get-in config [:allowed-modules current-module])
required-namespace-symb-nodes (-> ns-form-node
ns-form-node->require-node
require-node->namespace-symb-nodes)]
(doseq [node required-namespace-symb-nodes
:let [clj-kondo-ignore (some-> (meta node) :clj-kondo/ignore hooks/sexpr set)]
:when (not (contains? clj-kondo-ignore :metabase/ns-module-checker))
:let [required-namespace (hooks/sexpr node)
required-module (module required-namespace)]
;; ignore stuff not in a module i.e. non-Metabase stuff.
:when required-module
:let [in-current-module? (= required-module current-module)]
:when (not in-current-module?)
:let [allowed-module? (or (= allowed-modules :any)
(contains? (set allowed-modules) required-module))
module-api-namespaces (module-api-namespaces required-module config)
allowed-module-namespace? (or (empty? module-api-namespaces)
(contains? module-api-namespaces required-namespace))]]
(when-let [error (cond
(not allowed-module?)
(format "Module %s should not be used in the %s module. [:metabase/ns-module-checker :allowed-modules %s]"
required-module
current-module
current-module)
(not allowed-module-namespace?)
(format "Namespace %s is not an allowed external API namespace for the %s module. [:metabase/ns-module-checker :api-namespaces %s]"
required-namespace
required-module
required-module))]
(hooks/reg-finding! (assoc (meta node)
:message error
:type :metabase/ns-module-checker)))))))))
(defn lint-ns [x]
(lint-require-shapes (:node x))
(lint-modules (:node x) (get-in x [:config :linters :metabase/ns-module-checker]))
x)

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

@@ -4,7 +4,114 @@
[clojure.string :as str]
[hooks.common]))
(defn- warn-about-disallowed-parallel-forms [form config]
(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.analytics.snowplow-test/with-fake-snowplow-collector
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/with-all-users-permission
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-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-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.pulse/send-pulse!
metabase.query-processor.store/store-database!
next.jdbc/execute!})
(defn- warn-about-disallowed-parallel-forms [form]
(letfn [(error! [form message]
(hooks/reg-finding! (assoc (meta form)
:message message
@@ -12,12 +119,12 @@
(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))
(disallowed-parallel-forms qualified-symbol)
(error! form (format "%s is not allowed inside a ^:parallel test or test fixture" qualified-symbol))
(and (not (contains? (:parallel/safe config) 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 the Kondo config file [:metabase/validate-deftest]"
(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)
@@ -28,7 +135,7 @@
(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]
[{[_ test-name & body] :children, :as _node}]
(let [test-metadata (:meta test-name)
metadata-sexprs (map hooks/sexpr test-metadata)
combined-metadata (transduce
@@ -45,9 +152,9 @@
(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.
;; 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)
@@ -55,61 +162,30 @@
:type :metabase/deftest-not-marked-parallel-or-synchronized)))
(when parallel?
(doseq [form body]
(warn-about-disallowed-parallel-forms form config)))))
(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 {:keys [max-length], :as _config}]
[node]
(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))
(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- 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]}]
(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 (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]))
(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`
@@ -146,11 +222,20 @@
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 config]}]
(warn-about-disallowed-parallel-forms node config)
(defn use-fixtures [{:keys [node]}]
(warn-about-disallowed-parallel-forms node)
{:node node})

View File

@@ -1,8 +1,7 @@
(ns hooks.common
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.pprint]
[clojure.set :as set]))
[clojure.pprint]))
(defn with-macro-meta
"When introducing internal nodes (let, defn, etc) it is important to provide a meta of an existing token
@@ -10,83 +9,10 @@
[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]
@@ -116,14 +42,13 @@
;;;; 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."
"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)
(with-meta (hooks/token-node 'do) {:clj-kondo/ignore [:redundant-do]})
(for [arg args]
(update-ignored-linters arg conj :unused-value))))
(vary-meta arg update :clj-kondo/ignore #(conj (vec %) :unused-value)))))
(with-meta (meta node)))]
{:node node*}))
@@ -363,8 +288,8 @@
(-> (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))
(hooks/vector-node (mapcat (juxt :binding :value) binding-infos))
body))
(with-meta (meta body)))])
(with-meta (meta body)))))
@@ -439,58 +364,14 @@
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))]
(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]
(defn node->qualified-symbol [node]
(try
(when (hooks/token-node? node)
(let [sexpr (hooks/sexpr node)]
@@ -500,8 +381,8 @@
(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.
;; 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.
@@ -526,12 +407,3 @@
;; 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

@@ -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,45 @@
(ns hooks.metabase.api.common
(:require
[clj-kondo.hooks-api :as api]
[clojure.string :as str]))
(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
[arg]
(letfn [(update-defendpoint [node]
(let [[_defendpoint method route & body] (:children node)]
(api/list-node
(list
(api/token-node 'do)
(api/token-node (symbol "compojure.core" (str (api/sexpr method))))
(-> (api/list-node
(list*
(api/token-node 'clojure.core/defn)
(api/token-node (route-fn-name (api/sexpr method) (api/sexpr route)))
body))
(with-meta (meta node)))))))]
(update arg :node update-defendpoint)))
(comment
(-> {:node (-> '(api/defendpoint POST "/:id/copy"
"Copy a `Card`, with the new name 'Copy of _name_'"
[id]
{id [:maybe ms/PositiveInt]}
(let [orig-card (api/read-check Card id)
new-name (str (trs "Copy of ") (:name orig-card))
new-card (assoc orig-card :name new-name)]
(-> (card/create-card! new-card @api/*current-user*)
hydrate-card-details
(assoc :last-edit-info (last-edit/edit-information-for-user @api/*current-user*)))))
pr-str
api/parse-string)}
defendpoint
:node
api/sexpr))

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

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

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

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

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

@@ -4,9 +4,9 @@
(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))]
(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,7 +1,5 @@
(ns hooks.metabase.models.interface
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common]))
(:require [clj-kondo.hooks-api :as hooks]))
(defn define-hydration-method
"This is used for both [[metabase.models.interface/define-simple-hydration-method]] and
@@ -16,6 +14,5 @@
(hooks/token-node 'defn)
fn-name
fn-tail))))
(with-meta (meta node))
hooks.common/add-lsp-ignore-unused-public-var-metadata)]
(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
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?
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
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

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

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

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

@@ -11,10 +11,6 @@
[]
(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:
@@ -25,7 +21,7 @@
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]
[dataset]
(let [sexpr (hooks/sexpr dataset)
global-defs (global-dataset-symbols)]
(cond
@@ -35,45 +31,48 @@
(namespace sexpr)
:qualified
(empty? global-defs)
:unqualified/unknown
(contains? global-defs sexpr)
:unqualified/from-dataset-defs-namespace
(local-def? this-ns sexpr)
:unqualified/local-def
;; either something defined in the current namespace or let-bound in the current scope.
:else
:unqualified/unknown)))
:unqualified/local-def)))
(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))
[{{[_ 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 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))}))
;; 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`."

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

@@ -27,11 +27,11 @@
(hooks/vector-node (into []
(mapcat (fn [[setting-name v]]
(concat
[(with-meta (hooks/token-node '_) (meta setting-name)) v]
[(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]))))
(when (namespaced-symbol-node? setting-name)
[(with-meta (hooks/token-node '_) (meta setting-name)) setting-name]))))
(partition 2 (:children bindings))))
bindings)]
{:node (-> (list*

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

@@ -15,17 +15,3 @@
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

@@ -109,6 +109,8 @@
actual-arg-count)
:type :metabase/validate-logging)))))
(defn infof
"Valid logf, debugf, etc."
[{:keys [node], :as 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,39 +1,10 @@
(ns hooks.common-test
(ns ^:mb/once 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

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,4 +1,4 @@
(ns hooks.metabase.util.log-test
(ns ^:mb/once hooks.metabase.util.log-test
(:require
[clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]

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.legacy-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,62 +20,67 @@
(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 'mr/def '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 'lib.schema.match/match 1))
(eval . (put-clojure-indent 'lib.schema.match/match-one 1))
(eval . (put-clojure-indent 'lib.schema.match/replace 1))
(eval . (put-clojure-indent 'lib.schema.match/replace-in 2))
(eval . (put-clojure-indent 'lib.util.match/replace 1))
(eval . (put-clojure-indent 'macros/case 0))
(eval . (put-clojure-indent 'match 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-driver 1))
(eval . (put-clojure-indent 'mt/test-drivers 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 '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 1))
(eval . (put-clojure-indent 'with-meta '(:form)))
;; 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)

View File

@@ -1,20 +1,13 @@
/* eslint-disable import/no-commonjs */
/* eslint-disable no-undef */
const path = require("path");
// `postcss-modules` lints css modules class names, but it currently crashes
// eslint on vscode. If you use webstorm or want to run the lint for the cli, you
// can use this flag to enable it. This is set to true in CI
const shouldLintCssModules =
process.env.LINT_CSS_MODULES === "true" || process.env.CI;
const plugins = ["react", "no-only-tests", "ttag", "i18next"];
if (shouldLintCssModules) {
plugins.push("postcss-modules");
}
module.exports = {
ignorePatterns: ["!.storybook"],
rules: {
strict: [2, "never"],
"no-undef": 2,
@@ -24,14 +17,11 @@ module.exports = {
{
vars: "all",
args: "none",
varsIgnorePattern: "^_.+$",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"no-empty": [1, { allowEmptyCatch: true }],
// Note: adding this rule to a eslint config file in a subfolder will remove
// *not* carry over the restricted imports from parent folders, you will
// need to copy them over
"no-restricted-imports": [
"error",
{
@@ -49,7 +39,6 @@ module.exports = {
],
curly: [1, "all"],
eqeqeq: [1, "smart"],
"import/no-duplicates": ["warn", { considerQueryString: true }],
"import/no-default-export": 2,
"import/no-named-as-default": 0,
"import/no-commonjs": 1,
@@ -73,13 +62,6 @@ module.exports = {
warnOnUnassignedImports: false,
},
],
"sort-imports": [
"error",
{
// allows this rule to work with import/order
ignoreDeclarationSort: true,
},
],
"no-console": [2, { allow: ["warn", "error", "errorBuffer"] }],
"react/no-is-mounted": 2,
"react/prefer-es6-class": 2,
@@ -96,10 +78,9 @@ module.exports = {
"react/forbid-component-props": [2, { forbid: ["sx"] }],
"react-hooks/exhaustive-deps": [
"warn",
{ additionalHooks: "(useSafeAsyncFunction)" },
{ additionalHooks: "(useSyncedQueryString|useSafeAsyncFunction)" },
],
"prefer-const": [1, { destructuring: "all" }],
"no-restricted-globals": ["error", "close"],
"no-useless-escape": 0,
"no-only-tests/no-only-tests": [
"error",
@@ -119,10 +100,11 @@ module.exports = {
"When",
"Then",
"describeWithSnowplow",
"describeEE",
],
},
],
complexity: ["error", { max: 55 }],
complexity: ["error", { max: 54 }],
...(shouldLintCssModules
? {
"postcss-modules/no-undef-class": "error",
@@ -142,7 +124,7 @@ module.exports = {
"jest/globals": true,
},
parser: "babel-eslint",
plugins,
plugins: ["react", "no-only-tests", "postcss-modules"],
extends: [
"eslint:recommended",
"plugin:react/recommended",
@@ -151,15 +133,11 @@ module.exports = {
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:depend/recommended",
"plugin:storybook/recommended",
"plugin:i18next/recommended",
],
settings: {
"import/internal-regex": "^metabase/|^metabase-lib/",
"import/resolver": {
webpack: {
config: path.resolve(__dirname, "./rspack.main.config.js"),
typescript: true,
},
},
@@ -180,21 +158,8 @@ module.exports = {
{
files: ["*.js", "*.jsx", "*.ts", "*.tsx"],
rules: {
"jtag-missing-key": "error",
"no-unconditional-metabase-links-render": "error",
"no-color-literals": "error",
"no-literal-metabase-strings": "error",
"depend/ban-dependencies": [
"error",
{
allowed: [
"underscore",
"moment",
"lodash.orderby",
"lodash.debounce",
],
},
],
},
},
{
@@ -202,40 +167,17 @@ module.exports = {
"*.unit.spec.*",
"frontend/src/metabase/admin/**/*",
"frontend/src/metabase/setup/**/*",
"enterprise/frontend/src/metabase-enterprise/whitelabel/**/*",
"frontend/lint/**/*",
"*.stories.*",
"**/.storybook/*",
"stories-data.*",
"e2e/**/*",
"**/tests/*",
"release/**/*",
"rspack.config.js",
"rspack.main.config.js",
"rspack.embedding-sdk-package.config.js",
"rspack.embedding-sdk-bundle.config.js",
],
rules: {
"no-color-literals": "off",
"no-unconditional-metabase-links-render": "off",
"no-literal-metabase-strings": "off",
},
},
{
files: [
"*.unit.spec.*",
"frontend/lint/**/*",
"*.stories.*",
"stories-data.*",
"e2e/**/*",
"**/tests/*",
"release/**/*",
"rspack.main.config.js",
],
rules: {
"i18next/no-literal-string": "off",
},
},
{
extends: ["plugin:@typescript-eslint/recommended"],
files: ["*.ts", "*.tsx"],
@@ -247,20 +189,14 @@ module.exports = {
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_.+$",
varsIgnorePattern: "^_.+$",
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
destructuredArrayIgnorePattern: "^_.+$",
destructuredArrayIgnorePattern: "^_",
},
],
// This was introduced in 6.0.0
@@ -272,9 +208,8 @@ module.exports = {
"plugin:jest/recommended",
"plugin:jest-dom/recommended",
"plugin:testing-library/react",
"plugin:jest-formatting/recommended",
],
plugins: ["jest", "jest-dom", "testing-library", "jest-formatting"],
plugins: ["jest", "jest-dom", "testing-library"],
files: [
"*.unit.spec.ts",
"*.unit.spec.tsx",
@@ -283,68 +218,6 @@ module.exports = {
],
rules: {
"jest/valid-title": ["error", { ignoreTypeOfDescribeName: true }],
"jest/expect-expect": [
"error",
{
assertFunctionNames: ["expect*", "assert*"],
additionalTestBlockFunctions: [],
},
],
},
},
{
// Enable jest formatting for cypress tests too, the plugin logic just works
extends: ["plugin:jest-formatting/recommended"],
files: ["*.cy.spec.ts", "*.cy.spec.js"],
},
{
files: ["frontend/src/**/*"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/mb-base-color-/]",
message:
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
},
],
},
},
{
files: ["frontend/src/metabase/**/*"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/mb-base-color-/]",
message:
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
},
],
},
},
{
files: ["frontend/src/metabase/query_builder/**/*"],
rules: {
"import/no-cycle": "error",
},
},
{
files: ["docs/**/snippets/**/*.{ts,tsx,js,jsx}"],
rules: {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off",
"import/no-commonjs": "off",
"import/no-default-export": "off",
"import/order": "off",
"import/no-unresolved": "off",
"no-color-literals": "off",
},
},
{
files: ["frontend/build/**/*.js"],
rules: {
"import/no-commonjs": "off",
},
},
],

276
.github/CODEOWNERS vendored
View File

@@ -1,266 +1,14 @@
# Before changing this file, make sure you have read how we use it:
# https://www.notion.so/metabase/Pull-Request-Code-Review-Guide-374f05df889748b69d67af753f9bc9b8?pvs=4#36550c04ceab40b5a8b8bee6264b090c
enterprise/frontend/src/embedding-sdk-package @metabase/embedding
enterprise/frontend/src/embedding-sdk-bundle @metabase/embedding
enterprise/frontend/src/embedding-sdk-shared @metabase/embedding
frontend/src/metabase/embedding-sdk @metabase/embedding
frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion @metabase/embedding
frontend/src/metabase/public/containers/PublicOrEmbeddedDashboard @metabase/embedding
frontend/src/metabase/ui @metabase/core-frontend-ux-west
# snowplow/* @metabase/data
src/metabase/analytics/prometheus.clj @metabase/cloud-ops
.github @metabase/devexp
.clj-kondo @metabase/devexp
deps.edn @metabase/backend-developers
docs @metabase/tech-writers
docs/developers-guide # exclude from tech-writer ownership
#
# Backend modules <=> teams that own them
#
# modules/drivers @metabase/drivers
src/metabase/driver/sql_jdbc/actions.clj @metabase/core-ux-east
test/metabase/driver/sql_jdbc/actions_test.clj @metabase/core-ux-east
# enterprise/backend/src/metabase_enterprise/advanced_config @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/advanced_config @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/advanced_permissions @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/advanced_permissions @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/airgap @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/airgap @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/api @metabase/devexp
# enterprise/backend/test/metabase_enterprise/api @metabase/devexp
# enterprise/backend/src/metabase_enterprise/audit_app @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/audit_app @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/auth_provider @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/auth_provider @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/billing @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/billing @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/cache @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/cache @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/content_verification @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/content_verification @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/core @metabase/devexp
# enterprise/backend/test/metabase_enterprise/core @metabase/devexp
# enterprise/backend/src/metabase_enterprise/dashboard_subscription_filters @metabase/core-ux-east
# enterprise/backend/test/metabase_enterprise/dashboard_subscription_filters @metabase/core-ux-east
# enterprise/backend/src/metabase_enterprise/enhancements @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/enhancements @metabase/core-backend-admin-webapp
# # enterprise/backend/src/metabase_enterprise/llm
# # enterprise/backend/test/metabase_enterprise/llm
# enterprise/backend/src/metabase_enterprise/query_reference_validation @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/query_reference_validation @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/sandbox @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/sandbox @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/scim @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/scim @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/search ...
# enterprise/backend/test/metabase_enterprise/search ...
# enterprise/backend/src/metabase_enterprise/serialization @metabase/core-ux-east
# enterprise/backend/test/metabase_enterprise/serialization @metabase/core-ux-east
# enterprise/backend/src/metabase_enterprise/snippet_collections @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/snippet_collections @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/sso @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/sso @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/stale @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/stale @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/stats @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/stats @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/task @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/task @metabase/core-backend-admin-webapp
# enterprise/backend/src/metabase_enterprise/upload_management @metabase/core-backend-admin-webapp
# enterprise/backend/test/metabase_enterprise/upload_management @metabase/core-backend-admin-webapp
src/metabase/actions @metabase/core-ux-east
test/metabase/actions @metabase/core-ux-east
# src/metabase/activity_feed @metabase/core-backend-admin-webapp
# test/metabase/activity_feed @metabase/core-backend-admin-webapp
# src/metabase/analytics @metabase/core-backend-admin-webapp
# test/metabase/analytics @metabase/core-backend-admin-webapp
# src/metabase/analyze @metabase/drivers
# test/metabase/analyze @metabase/drivers
# src/metabase/api @metabase/devexp
# test/metabase/api @metabase/devexp
# src/metabase/audit @metabase/core-backend-admin-webapp
# test/metabase/audit @metabase/core-backend-admin-webapp
# src/metabase/auth_provider @metabase/core-backend-admin-webapp
# test/metabase/auth_provider @metabase/core-backend-admin-webapp
# src/metabase/bookmarks @metabase/core-backend-admin-webapp
# test/metabase/bookmarks @metabase/core-backend-admin-webapp
# src/metabase/cmd @metabase/devexp
# test/metabase/cmd @metabase/devexp
# src/metabase/channel @metabase/core-ux-east
# test/metabase/channel @metabase/core-ux-east
# src/metabase/cloud_migration ...
# test/metabase/cloud_migration ...
# src/metabase/cmd @metabase/devexp
# test/metabase/cmd @metabase/devexp
# src/metabase/config @metabase/devexp
# test/metabase/config @metabase/devexp
# src/metabase/core @metabase/devexp
# test/metabase/core @metabase/devexp
# src/metabase/db @metabase/core-backend-admin-webapp
# test/metabase/db @metabase/core-backend-admin-webapp
# src/metabase/driver @metabase/drivers
# test/metabase/driver @metabase/drivers
# src/metabase/eid_translation @metabase/core-backend-admin-webapp
# test/metabase/eid_translation @metabase/core-backend-admin-webapp
# src/metabase/embed @metabase/embedding
# test/metabase/embed @metabase/embedding
# src/metabase/events @metabase/devexp
# test/metabase/events @metabase/devexp
# src/metabase/formatter @metabase/core-backend-querying
# test/metabase/formatter @metabase/core-backend-querying
# src/metabase/indexed_entities @metabase/core-backend-querying
# test/metabase/indexed_entities @metabase/core-backend-querying
# src/metabase/integrations ...
# test/metabase/integrations ...
# src/metabase/legacy_mbql @metabase/core-backend-querying
# test/metabase/legacy_mbql @metabase/core-backend-querying
# src/metabase/lib @metabase/core-backend-querying
# test/metabase/lib @metabase/core-backend-querying
# src/metabase/logger @metabase/devexp
# test/metabase/logger @metabase/devexp
# src/metabase/login_history @metabase/core-backend-admin-webapp
# test/metabase/login_history @metabase/core-backend-admin-webapp
# src/metabase/model_persistence @metabase/core-backend-querying
# test/metabase/model_persistence @metabase/core-backend-querying
# src/metabase/models @metabase/devexp
# test/metabase/models @metabase/devexp
# src/metabase/moderation @metabase/core-backend-admin-webapp
# test/metabase/moderation @metabase/core-backend-admin-webapp
# src/metabase/notification @metabase/core-ux-east
# test/metabase/notification @metabase/core-ux-east
# src/metabase/permissions @metabase/core-backend-admin-webapp
# test/metabase/permissions @metabase/core-backend-admin-webapp
# src/metabase/plugins @metabase/devexp
# test/metabase/plugins @metabase/devexp
# src/metabase/premium_features @metabase/core-backend-admin-webapp
# test/metabase/premium_features @metabase/core-backend-admin-webapp
# src/metabase/public_settings @metabase/core-backend-admin-webapp
# test/metabase/public_settings @metabase/core-backend-admin-webapp
# src/metabase/public_sharing @metabase/core-backend-admin-webapp
# test/metabase/public_sharing @metabase/core-backend-admin-webapp
# src/metabase/pulse @metabase/core-ux-east
# test/metabase/pulse @metabase/core-ux-east
# src/metabase/query_analysis @metabase/core-backend-querying
# test/metabase/query_analysis @metabase/core-backend-querying
# src/metabase/query_processor @metabase/core-backend-querying
# test/metabase/query_processor @metabase/core-backend-querying
# src/metabase/request @metabase/devexp
# test/metabase/request @metabase/devexp
# src/metabase/revisions @metabase/core-backend-admin-webapp
# test/metabase/revisions @metabase/core-backend-admin-webapp
# src/metabase/sample_data @metabase/core-backend-admin-webapp
# test/metabase/sample_data @metabase/core-backend-admin-webapp
# src/metabase/search ...
# test/metabase/search ...
# src/metabase/segments @metabase/core-backend-querying
# test/metabase/segments @metabase/core-backend-querying
# src/metabase/server @metabase/devexp
# test/metabase/server @metabase/devexp
# src/metabase/session @metabase/core-backend-admin-webapp
# test/metabase/session @metabase/core-backend-admin-webapp
# src/metabase/setup @metabase/core-backend-admin-webapp
# test/metabase/setup @metabase/core-backend-admin-webapp
# src/metabase/sso @metabase/core-backend-admin-webapp
# test/metabase/sso @metabase/core-backend-admin-webapp
# src/metabase/sync @metabase/drivers
# test/metabase/sync @metabase/drivers
# src/metabase/task @metabase/devexp
# test/metabase/task @metabase/devexp
# src/metabase/tiles @metabase/core-backend-dashviz
# test/metabase/tiles @metabase/core-backend-dashviz
# src/metabase/timeline @metabase/core-backend-dashviz
# test/metabase/timeline @metabase/core-backend-dashviz
# src/metabase/types @metabase/core-backend-querying
# test/metabase/types @metabase/core-backend-querying
# src/metabase/upload @metabase/core-backend-admin-webapp
# test/metabase/upload @metabase/core-backend-admin-webapp
# src/metabase/user_key_value @metabase/core-backend-admin-webapp
# test/metabase/user_key_value @metabase/core-backend-admin-webapp
# src/metabase/util @metabase/devexp
# test/metabase/util @metabase/devexp
# src/metabase/xrays @metabase/core-backend-querying
# test/metabase/xrays @metabase/core-backend-querying
# TL;DR: if @camsaul hasn't been involved you're probably doing it wrong
src/metabase @camsaul
modules @camsaul
enterprise/backend @camsaul
enterprise/backend/src/metabase_enterprise/advanced_permissions @noahmoss
enterprise/backend/src/metabase_enterprise/audit_app @noahmoss
enterprise/backend/src/metabase_enterprise/sso @noahmoss
enterprise/backend/src/metabase_enterprise/task/truncate_audit_table.clj @noahmoss
enterprise/backend/src/metabase_enterprise/internal_user.clj @noahmoss
src/metabase/**/*permissions* @noahmoss
src/metabase/integrations @noahmoss

View File

@@ -63,17 +63,15 @@ body:
attributes:
label: Information about your Metabase installation
description: |
For versions >= 56, copy and paste the Diagnostic info from `Admin -> Tools -> Help`.
For versions < 56, copy and paste the Diagnostic info from `Admin -> Troubleshooting`.
Copy and paste the JSON from `Admin -> Troubleshooting`.
_This will be automatically formatted into code, so no need for backticks._
placeholder: |
{"browser-info": {
"language": "en-US",
"platform": "MacIntel",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"vendor": "Google Inc."},
"metabase-info": {
"databases": [ "postgres",...
- Your browser and the version: (e.g., Chrome 52.1, Firefox 48.0, Safari 11.1, …)
- Your operating system: (e.g., OS X 10.10, Windows 10.1809, Ubuntu 16.04, …)
- Your databases: (e.g, MySQL, Postgres, MongoDB, …)
- Metabase version: (e.g., 0.19.3)
- Metabase hosting environment: (e.g., Elastic Beanstalk, Docker, Heroku, Jar-file on Ubuntu, …)
- Metabase internal database: (e.g., H2 (default), Postgres or MySQL)
render: JSON
validations:
required: true

View File

@@ -5,19 +5,20 @@ about: This issue is used to track a feature/project that may span several days
and manage work.
title: "[Epic] Title"
labels: ".Epic"
---
### Links
**Links**
- product doc: _link to product doc_
- eng doc: _link to technical design doc, if any_
- feature branch: `branch-name` _this should be the feature branch where this work will be done in. PRs will be delivered against this branch_
- issue links: _related issues if any_
- [product doc](LINK_TO_PRODUCT_DOC)
- [technical design doc](LINK_TO_TECHNICAL_DESIGN_DOC)
- [testing plan](LINK_TO_TESTING_PLAN)
- feature branch: `BRANCH_NAME`
- issue links: _related issues, if any_
**Implementation Plan**
### Implementation Plan
```[tasklist]
#### Milestone 1
- [ ] _Add tasks here_
```
***Milestone 1***
_insert tasklist here_
***Milestone 2***

View File

@@ -1,9 +1,13 @@
> [!IMPORTANT]
> For those employed by Metabase: if you are merging into master, please add either a `backport` or a `no-backport` label to this PR. You will not be able to merge until you do this step. Refer to the section [Do I need to backport this PR?](https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#89f80d6f17714a0198aeb66c0efd1b71) in the Metabase Branching Strategy document for more details. If you're not employed by Metabase, this section does not apply to you, and the label will be taken care of by your reviewer.
> For those employed by Metabase: if you are merging into master, please add either a `backport` or a `no-backport` label
> to this PR. You will not be able to merge until you do this step. Refer to the section [Do I need to backport this
> PR?](https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#89f80d6f17714a0198aeb66c0efd1b71)
> in the Metabase Branching Strategy document for more details. If you're not employed by Metabase, this section does not
> apply to you, and the label will be taken care of by your reviewer.
> **Warning**
>
> If that is your first contribution to Metabase, please sign the [Contributor License Agreement](https://docs.google.com/a/metabase.com/forms/d/1oV38o7b9ONFSwuzwmERRMi9SYrhYeOrkbmNaq9pOJ_E/viewform) (unless it's a tiny documentation change). Also, if you're attempting to fix a translation issue, please submit your changes to our [Crowdin project](https://crowdin.com/project/metabase-i18n) instead of opening a PR.
> If that is your first contribution to Metabase, please sign the [Contributor License Agreement](https://docs.google.com/a/metabase.com/forms/d/1oV38o7b9ONFSwuzwmERRMi9SYrhYeOrkbmNaq9pOJ_E/viewform) (unless it's a tiny documentation change). Also, if you're attempting to fix a translation issue, please submit your changes to our [POEditor project](https://poeditor.com/join/project/ynjQmwSsGh) instead of opening a PR.
Closes https://github.com/metabase/metabase/issues/[issue_number]

View File

@@ -5,7 +5,7 @@ PENDING CI / PENDING REVIEW / READY _(choose one and update accordingly)_
- Reproduces #XXXXX
### How to test this manually?
- `yarn test-cypress`
- `yarn test-cypress-open`
- `relative/path/to/the/file`
> _(For still unfixed bug)_
- Replace `it.skip()` with `it.only()` to run the test in isolation

View File

@@ -1,28 +0,0 @@
name: await-port
description: Wait for localhost port to be ready
inputs:
port:
required: true
runs:
using: composite
steps:
- name: Wait for port ${{ inputs.port }} to be ready
shell: bash
run: |
echo "Waiting for port ${{ inputs.port }} to be ready..."
timeout=300 # 5 minutes timeout
elapsed=0
while ! nc -z localhost ${{ inputs.port }}; do
if [ $elapsed -ge $timeout ]; then
echo "❌ Timeout waiting for port ${{ inputs.port }} after ${timeout} seconds"
exit 1
fi
sleep 1
elapsed=$((elapsed + 1))
if [ $((elapsed % 10)) -eq 0 ]; then
echo " Still waiting for port ${{ inputs.port }}... ($elapsed seconds)"
fi
done
echo "✅ Port ${{ inputs.port }} is ready!"

View File

@@ -0,0 +1,69 @@
name: Build E2E Matrix
description: Build and output a custom matrix for E2E tests.
outputs:
matrix:
description: Parameterized matrix in JSON format
value: ${{ steps.matrix.outputs.result }}
runs:
using: "composite"
steps:
- uses: actions/github-script@v7
id: matrix
with:
script: | # js
const java = 11;
const defaultRunner = "ubuntu-22.04";
const defaultOptions = {
"java-version": java,
runner: defaultRunner,
edition: "ee",
context: "folder"
};
const testSets = [
["actions", {} ],
["admin", {} ],
["admin-2", {} ],
["binning", {} ],
["collections", {} ],
["custom-column", {} ],
["dashboard", {} ],
["dashboard-cards", {} ],
["dashboard-filters", {} ],
["embedding", {} ],
["filters", {} ],
["filters-reproductions", {}],
["joins", {} ],
["metrics", {} ],
["models", {} ],
["native", {} ],
["native-filters", {} ],
["onboarding", {} ],
["organization", {} ],
["permissions", {} ],
["question", {} ],
["question-reproductions", {} ],
["search", {}],
["sharing", {} ],
["visualizations-charts", {} ],
["visualizations-tabular", {} ],
["oss-subset", { edition: 'oss', context: "special" } ],
["slow", { context: "special" } ],
["flaky", { context: "special" } ],
["mongo", { context: "qa-database" } ],
];
const config = testSets.map(([name, specialOptions]) => {
return {
...defaultOptions,
...specialOptions,
name,
}
});
const matrix = { include: config };
return matrix;

View File

@@ -1,110 +0,0 @@
name: Create Backport PR
inputs:
target-version: # major version number like 34 or 86
required: true
backport-commit:
required: true
github-token:
required: true
github-token-2: # need a second token to approve the PR
required: true
runs:
using: "composite"
steps:
- id: create_backport_pull_request
name: Create backport pull request
env:
TARGET_VERSION: ${{ inputs.target-version }}
TARGET_BRANCH: release-x.${{ inputs.target-version }}.x
ORIGINAL_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
ORIGINAL_TITLE: ${{ github.event.pull_request.title }}
GITHUB_TOKEN: ${{ inputs.github-token }}
COMMIT: ${{ inputs.backport-commit }}
shell: bash
run: | # bash
git config --global user.email "metabase-bot@metabase.com"
git config --global user.name "Metabase bot"
BACKPORT_BRANCH="backport-$TARGET_VERSION-$COMMIT"
# Checkout the target branch and create a new backport branch
git checkout ${TARGET_BRANCH}
git fetch --all
git checkout -b ${BACKPORT_BRANCH}
git cherry-pick ${COMMIT} || true
CONFLICT_LIST=$(git ls-files -u)
CONFLICTS=$(git ls-files -u | wc -l)
if [ "$CONFLICTS" -gt 0 ]; then
git cherry-pick --abort
echo "Could not cherry pick because of a conflict"
echo "has-conflicts=true" >> $GITHUB_OUTPUT
# Add a shell script for resolving conflicts
echo "git reset HEAD~1" > ./backport.sh
echo "rm ./backport.sh" >> ./backport.sh
echo "git cherry-pick ${COMMIT}" >> ./backport.sh
echo "echo 'Resolve conflicts and force push this branch.\n\nTo backport translations run: bin/i18n/merge-translations <release-branch>'" >> ./backport.sh
chmod +x ./backport.sh
git add ./backport.sh
git commit -m "Add backport resolution script"
PR_BODY=$(cat <<-END
#${ORIGINAL_PULL_REQUEST_NUMBER}
> [!IMPORTANT]
> Manual conflict resolution is required.
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Check the box below when done.
Conflicts:
\`\`\`shell
${CONFLICT_LIST}
\`\`\`
- [ ] Conflicts resolved
END
)
else
echo "has-conflicts=false" >> $GITHUB_OUTPUT
PR_BODY="#${ORIGINAL_PULL_REQUEST_NUMBER}"
fi
git push -u origin ${BACKPORT_BRANCH}
BACKPORT_PR_URL=$(gh pr create --base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${GITHUB_ACTOR}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
BACKPORT_PR_NUMBER=${BACKPORT_PR_URL##*/}
echo "backport_pr_number=$BACKPORT_PR_NUMBER" >> $GITHUB_OUTPUT
echo "New PR has been created: #${BACKPORT_PR_NUMBER}"
git checkout ${{ github.sha }}
- name: Auto approve backport PR
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'false' }}
uses: juliangruber/approve-pull-request-action@v1
with:
github-token: ${{ inputs.github-token-2 }}
number: ${{ steps.create_backport_pull_request.outputs.backport_pr_number }}
- name: Enable Pull Request Automerge
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'false' }}
uses: peter-evans/enable-pull-request-automerge@v2
with:
token: ${{ inputs.github-token }}
pull-request-number: ${{ steps.create_backport_pull_request.outputs.backport_pr_number }}
merge-method: squash
- uses: ./.github/actions/notify-pull-request
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'true' }}
with:
include-log: false
message: Manual conflict resolution is required on \#${{ steps.create_backport_pull_request.outputs.backport_pr_number }}
- uses: ./.github/actions/notify-pull-request
if: ${{ failure() }}
with:
include-log: true
message: Something went wrong while creating backport

View File

@@ -1,106 +0,0 @@
name: Cross-branch migration test
inputs:
base-ref: ## Base branch PR came from
required: true
head-ref: ## Source branch of the pull request
required: true
runs:
using: "composite"
steps:
## First we deploy the base branch
- name: Check out base branch
uses: actions/checkout@v4
with:
ref: ${{ inputs.base-ref }}
- name: Prepare backend environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: cross-branch-migrations
- name: Run backend on base branch
run: |
clojure -M:run:ee > metabase.log 2>&1 & echo $! > metabase_pid
tail -F metabase.log &
shell: bash
- name: Wait for Metabase to start
run: |
while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}' > /dev/null; do
if ! kill -0 $(cat metabase_pid) 2>/dev/null; then
cat metabase.log
echo "Metabase process has exited."
exit 1
fi
sleep 1
done
shell: bash
- name: Stop Metabase on base branch
run: kill $(cat metabase_pid)
shell: bash
## Then we deploy the PR branch to ensure it migrates up successfully
- name: Check out pull request branch
uses: actions/checkout@v4
with:
ref: ${{ inputs.head-ref }}
- name: Run backend on pull request branch
run: |
clojure -M:run:ee > metabase.log 2>&1 & echo $! > metabase_pid
tail -F metabase.log &
shell: bash
- name: Wait for Metabase to start
run: |
while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}' > /dev/null; do
if ! kill -0 $(cat metabase_pid) 2>/dev/null; then
cat metabase.log
echo "Metabase process has exited. Check that your migration changes work on an instance that has been previously initialized on your PR's target branch."
exit 1
fi
sleep 1
done
shell: bash
- name: Stop Metabase on pull request branch
run: kill $(cat metabase_pid)
shell: bash
## Testing that originally deploying this PR allows you to roll back to a previous release
## Reset the checksums so when we go back to the base branch, it's as if the PR branch was what was originally deployed
- name: Reset the checksums as if the PR branch was what was originally deployed
run: |
clojure -M:dev:ee:migrate reset-checksums
shell: bash
- name: Rollback the last deployment
run: |
clojure -M:dev:ee:migrate rollback last-deployment
shell: bash
- name: Re-check out base branch
uses: actions/checkout@v4
with:
ref: ${{ inputs.base-ref }}
- name: Re-run backend on base branch
run: |
clojure -M:run:ee > metabase.log 2>&1 & echo $! > metabase_pid
tail -F metabase.log &
shell: bash
- name: Wait for Metabase to start
run: |
while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}' > /dev/null; do
if ! kill -0 $(cat metabase_pid) 2>/dev/null; then
cat metabase.log
echo "Metabase process has exited."
exit 1
fi
sleep 1
done
shell: bash

View File

@@ -68,3 +68,10 @@ runs:
- name: Unzip Metabase artifact containing an uberjar
run: unzip mb.zip
shell: bash
- name: Get the version info
run: |
jar xf target/uberjar/metabase.jar version.properties
cat version.properties
mv version.properties resources/
shell: bash

View File

@@ -8,33 +8,26 @@ inputs:
description: DockerHub password/token
required: true
maildev:
description: Maildev containers
description: Maildev container
required: true
default: false
default: 'false'
openldap:
description: OpenLDAP container
required: true
default: false
default: 'false'
postgres:
description: Postgres container
required: true
default: false
default: 'false'
mysql:
description: MySQL container
required: true
default: false
default: 'false'
mongo:
description: Mongo container
required: true
default: false
webhook:
description: Webhook tester container
required: false
default: false
snowplow:
description: Snowplow micro container
required: false
default: false
default: 'false'
runs:
using: "composite"
@@ -46,68 +39,49 @@ runs:
password: ${{ inputs.password }}
- name: Start Containers
shell: bash
run: | # bash
run: |
Y='\033[33m'
G='\033[1;32m'
cd e2e/test/scenarios/
docker compose up \
$([ ${{ inputs.postgres }} = true ] && echo "postgres-sample") \
$([ ${{ inputs.mysql }} = true ] && echo "mysql-sample") \
$([ ${{ inputs.mongo }} = true ] && echo "mongo-sample") \
$([ ${{ inputs.maildev }} = true ] && echo "maildev") \
$([ ${{ inputs.maildev }} = true ] && echo "maildev-ssl") \
$([ ${{ inputs.openldap }} = true ] && echo "ldap") \
$([ ${{ inputs.webhook }} = true ] && echo "webhook-tester") \
$([ ${{ inputs.snowplow }} = true ] && echo "snowplow-micro") \
-d
if ${{ inputs.maildev }}; then
echo -e "${Y}checking maildev container..." &&
echo -e "${Y}Starting maildev container..." &&
docker run -d -p 1080:1080 -p 1025:1025 maildev/maildev:2.0.5 &&
while ! nc -z localhost 1080; do sleep 1; done &&
while ! nc -z localhost 1025; do sleep 1; done &&
echo -e "${G}Maildev is up and running!"
echo -e "${Y}checking maildev-ssl container..." &&
while ! nc -z localhost 1081; do sleep 1; done &&
while ! nc -z localhost 465; do sleep 1; done &&
echo -e "${G}Maildev-ssl is up and running!"
fi
if ${{ inputs.openldap }}; then
echo -e "${Y}checking openldap container..." &&
echo -e "${Y}Starting openldap container..." &&
docker run -d -p 389:389 \
--env LDAP_ADMIN_PASSWORD=adminpass \
--env LDAP_USERS=user01@example.org,user02@example.org \
--env LDAP_PASSWORDS=123456,123465 \
--env LDAP_ROOT=dc=example,dc=org \
--env LDAP_PORT_NUMBER=389 \
bitnami/openldap:2.6.4 &&
while ! nc -z localhost 389; do sleep 1; done &&
echo -e "${G}Openldap is up and running!"
fi
if ${{ inputs.postgres }}; then
echo -e "${Y}checking postgres container..." &&
echo -e "${Y}Starting postgres container..." &&
docker run -d -p 5404:5432 -e TZ='US/Pacific' metabase/qa-databases:postgres-sample-12 &&
while ! nc -z localhost 5404; do sleep 1; done &&
echo -e "${G}Postgres is up and running!"
fi
if ${{ inputs.mysql }}; then
echo -e "${Y}checking mysql container..." &&
echo -e "${Y}Starting mysql container..." &&
docker run -d -p 3304:3306 -e TZ='US/Pacific' metabase/qa-databases:mysql-sample-8 &&
while ! nc -z localhost 3304; do sleep 1; done &&
echo -e "${G}MySQL is up and running!"
fi
if ${{ inputs.mongo }}; then
echo -e "${Y}checking mongo container..." &&
echo -e "${Y}Starting mongo container..." &&
docker run -d -p 27004:27017 metabase/qa-databases:mongo-sample-4.4 &&
while ! nc -z localhost 27004; do sleep 1; done &&
echo -e "${G}Mongo is up and running!"
fi
if ${{ inputs.webhook }}; then
echo -e "${Y}checking webhook test container..." &&
while ! nc -z localhost 9080; do sleep 1; done &&
echo -e "${G}Webhook tester is up and running!"
fi
if ${{ inputs.snowplow }}; then
echo -e "${Y}checking snowplow micro container..." &&
while ! nc -z localhost 9090; do sleep 1; done &&
echo -e "${G}Snowplow micro is up and running!"
fi
shell: bash

View File

@@ -1,105 +0,0 @@
name: Fetch artifact
description: Fetches a specific artifact and downloads it to the current directory
inputs:
name:
description: The name of the artifact to fetch
required: true
output-name:
description: Optional custom name for the output jar file (defaults to metabase.jar)
required: false
default: "metabase.jar"
extract-path:
description: Directory path where the jar should be extracted (defaults to current directory)
required: false
default: "."
runs:
using: "composite"
steps:
- name: find_release_artifact
id: find_release_artifact
uses: actions/github-script@v7
with:
result-encoding: string
script: | # js
const fs = require('fs');
const artifacts = await github.rest.actions.listArtifactsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
name: "${{ inputs.name }}",
per_page: 1,
});
if (!artifacts.data?.artifacts?.[0]?.id) {
throw new Error(`No artifacts found for ${{ inputs.name }}`);
}
const artifact_id = artifacts.data.artifacts[0].id;
const download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact_id,
archive_format: 'zip',
});
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/mb.zip`, Buffer.from(download.data));
- name: Extract and rename JAR
shell: bash
run: | # bash
OUTPUT_NAME="${{ inputs.output-name }}"
EXTRACT_PATH="${{ inputs.extract-path }}"
# Create a temporary directory for extraction to avoid conflicts
TEMP_DIR=$(mktemp -d)
# Extract to temporary directory
unzip mb.zip -d "$TEMP_DIR"
# Create target directory
mkdir -p "$EXTRACT_PATH"
# Find the jar and move all files from its directory
if [ -e "$TEMP_DIR/metabase.jar" ]; then
# Move all files to extract path
mv "$TEMP_DIR"/* "$EXTRACT_PATH/" 2>/dev/null || true
# Rename the jar to custom name if different
if [ "$OUTPUT_NAME" != "metabase.jar" ]; then
mv "$EXTRACT_PATH/metabase.jar" "$EXTRACT_PATH/$OUTPUT_NAME"
fi
elif [ -e "$TEMP_DIR/target/uberjar/metabase.jar" ]; then
# Move all files from uberjar directory to extract path
mv "$TEMP_DIR/target/uberjar"/* "$EXTRACT_PATH/" 2>/dev/null || true
# Rename the jar to custom name if different
if [ "$OUTPUT_NAME" != "metabase.jar" ]; then
mv "$EXTRACT_PATH/metabase.jar" "$EXTRACT_PATH/$OUTPUT_NAME"
fi
else
echo "Could not find metabase.jar in extracted files"
exit 1
fi
# Clean up temporary directory
rm -rf "$TEMP_DIR"
# Set the JAR_PATH environment variable
if [ "$EXTRACT_PATH" == "." ]; then
echo "JAR_PATH=$OUTPUT_NAME" >> $GITHUB_ENV
else
echo "JAR_PATH=$EXTRACT_PATH/$OUTPUT_NAME" >> $GITHUB_ENV
fi
- name: Verify that this is a valid JAR file
shell: bash
run: |
echo "Verifying JAR at: $JAR_PATH"
file --mime-type "$JAR_PATH"
- name: Reveal its version.properties
shell: bash
run: |
echo "Extracting version.properties from: $JAR_PATH"
jar xf "$JAR_PATH" version.properties && cat version.properties

View File

@@ -28,5 +28,3 @@ runs:
env:
PULL_REQUEST_NUMBER: ${{ inputs.pull-request-number }}
BASE_REF: ${{ inputs.base-ref }}
- run: git checkout ${{ github.sha }} # return to original commit
shell: bash

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