Compare commits
16 Commits
react-colo
...
attach-ren
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff11bd7f79 | ||
|
|
39c81419e8 | ||
|
|
0d95540349 | ||
|
|
f3a26b5135 | ||
|
|
a5c33e1756 | ||
|
|
b89f86c568 | ||
|
|
e108633f36 | ||
|
|
a54ef9bc99 | ||
|
|
9705d8976f | ||
|
|
a222ac98c0 | ||
|
|
520b1f6320 | ||
|
|
4a9d63ebc7 | ||
|
|
e16bfa4a76 | ||
|
|
d0148baa24 | ||
|
|
3e5d3985d8 | ||
|
|
6a61796277 |
@@ -1 +0,0 @@
|
||||
Fix flakey test $ARGUMENTS
|
||||
@@ -1 +0,0 @@
|
||||
Fix issue $ARGUMENTS
|
||||
@@ -1 +0,0 @@
|
||||
Fetch unresolved comments for this branch's PR, then fix them
|
||||
@@ -1 +0,0 @@
|
||||
Repro issue $ARGUMENTS in a failing test
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,147 +0,0 @@
|
||||
{:linters
|
||||
{:metabase/test-helpers-use-non-thread-safe-functions
|
||||
;; TODO -- seems silly to maintain different blacklists and whitelists here than we use for the deftest `^:parallel`
|
||||
;; checker...
|
||||
{:explicitly-safe
|
||||
#{ ;; these toucan methods might actually set global values if it's used outside of a transaction,
|
||||
;; but since mt/with-temp runs in a transaction, so we'll ignore them in this case.
|
||||
toucan2.core/delete!
|
||||
toucan2.core/update!
|
||||
toucan2.core/insert!
|
||||
toucan2.core/insert-returning-instance!
|
||||
toucan2.core/insert-returning-instances!
|
||||
toucan2.core/insert-returning-pk!
|
||||
toucan2.core/insert-returning-pks!
|
||||
clojure.core.async/<!!
|
||||
clojure.core.async/<!
|
||||
clojure.core.async/>!!
|
||||
clojure.core.async/>!
|
||||
clojure.core.async/alts!
|
||||
clojure.core.async/alts!!
|
||||
clojure.core.async/close!
|
||||
clojure.core.async/poll!
|
||||
clojure.core.memoize/memo-clear!
|
||||
clojure.core/conj!
|
||||
clojure.core/persistent!
|
||||
clojure.core/reset!
|
||||
clojure.core/swap!
|
||||
clojure.core/volatile!
|
||||
clojure.core/vreset!
|
||||
clojure.core/vswap!
|
||||
clojure.java.jdbc/execute!
|
||||
methodical.core/add-aux-method-with-unique-key!
|
||||
methodical.core/remove-aux-method-with-unique-key!
|
||||
next.jdbc/execute!
|
||||
|
||||
;; Definitely thread safe
|
||||
metabase.test.util.dynamic-redefs/patch-vars!
|
||||
|
||||
;; TODO: most of these symbols shouldn't be here, we should go through them and
|
||||
;; find the functions/macros that use them and make sure their names end with !
|
||||
;; best way to do this is try remove each of these and rely on kondo output to find places where it's used
|
||||
clojure.test/grant-collection-perms!
|
||||
clojure.test/grant-collection-perms-fn!
|
||||
clojure.test/grant-perms-fn!
|
||||
clojure.test/purge-old-entries!
|
||||
clojure.test/revoke-collection-perms!
|
||||
clojure.test/save-results!
|
||||
metabase-enterprise.advanced-permissions.models.permissions/update-db-download-permissions!
|
||||
metabase-enterprise.internal-user/install-internal-user!
|
||||
metabase-enterprise.sso.integrations.saml-test/call-with-login-attributes-cleared!
|
||||
metabase.actions.actions/perform-action!
|
||||
metabase.actions.models/insert!
|
||||
metabase.analytics.snowplow-test/fake-track-event-impl!
|
||||
metabase.analytics.snowplow/track-event-impl!
|
||||
metabase.app-db.core/finish-db-setup!
|
||||
metabase.app-db.core/migrate!
|
||||
metabase.app-db.core/setup-db!
|
||||
metabase.app-db.schema-migrations-test.impl/run-migrations-in-range!
|
||||
metabase.app-db.setup/migrate!
|
||||
metabase.app-db.setup/setup-db!
|
||||
metabase.channel.email-test/reset-inbox!
|
||||
metabase.channel.email/send-email!
|
||||
metabase.cmd.dump-to-h2/dump-to-h2!
|
||||
metabase.cmd.load-from-h2/load-from-h2!
|
||||
metabase.content-verification.models.moderation-review/create-review!
|
||||
metabase.core.core/ensure-audit-db-installed!
|
||||
metabase.driver.mongo-test/create-database-from-row-maps!
|
||||
metabase.driver.postgres-test/create-enums-db!
|
||||
metabase.driver.postgres-test/drop-if-exists-and-create-db!
|
||||
metabase.driver.sql-jdbc.execute/execute-statement!
|
||||
metabase.indexed-entities.models.model-index/add-values!
|
||||
metabase.driver.sql-jdbc.connection.ssh-tunnel-test/start-mock-servers!
|
||||
metabase.driver.sql-jdbc.connection.ssh-tunnel-test/stop-mock-servers!
|
||||
metabase.indexed-entities.task.index-values/job-init!
|
||||
metabase.model-persistence.models.persisted-info/ready-database!
|
||||
metabase.model-persistence.task.persist-refresh/job-init!
|
||||
metabase.model-persistence.task.persist-refresh/refresh-tables!
|
||||
metabase.model-persistence.task.persist-refresh/schedule-persistence-for-database!
|
||||
metabase.models.on-demand-test/add-dashcard-with-parameter-mapping!
|
||||
metabase.permissions.core/grant-application-permissions!
|
||||
metabase.permissions.core/grant-collection-read-permissions!
|
||||
metabase.permissions.core/grant-collection-readwrite-permissions!
|
||||
metabase.permissions.core/grant-full-data-permissions!
|
||||
metabase.permissions.core/grant-native-readwrite-permissions!
|
||||
metabase.permissions.core/grant-permissions!
|
||||
metabase.permissions.core/revoke-application-permissions!
|
||||
metabase.permissions.core/revoke-data-perms!
|
||||
metabase.permissions.core/update-data-perms-graph!
|
||||
metabase.permissions.core/update-group-permissions!
|
||||
metabase.permissions.models.collection.graph-test/clear-graph-revisions!
|
||||
metabase.permissions.models.collection.graph-test/do-with-n-temp-users-with-personal-collections!
|
||||
metabase.permissions.models.permissions/grant-application-permissions!
|
||||
metabase.permissions.models.permissions/grant-collection-read-permissions!
|
||||
metabase.permissions.models.permissions/grant-collection-readwrite-permissions!
|
||||
metabase.permissions.models.permissions/grant-full-data-permissions!
|
||||
metabase.permissions.models.permissions/grant-native-readwrite-permissions!
|
||||
metabase.permissions.models.permissions/grant-permissions!
|
||||
metabase.permissions.models.permissions/revoke-application-permissions!
|
||||
metabase.permissions.models.permissions/revoke-data-perms!
|
||||
metabase.permissions.models.permissions/update-data-perms-graph!
|
||||
metabase.permissions.models.permissions/update-group-permissions!
|
||||
metabase.permissions.test-util/with-restored-perms!
|
||||
metabase.public-sharing.api-test/add-card-to-dashboard!
|
||||
metabase.pulse.send/send-notifications!
|
||||
metabase.pulse.send/send-pulse!
|
||||
metabase.query-processor.streaming.interface/begin!
|
||||
metabase.query-processor.streaming.interface/finish!
|
||||
metabase.query-processor.streaming.interface/write-row!
|
||||
metabase.sample-data.impl/try-to-extract-sample-database!
|
||||
metabase.settings.models.setting-test/test-user-local-allowed-setting!
|
||||
metabase.settings.models.setting-test/test-user-local-only-setting!
|
||||
metabase.settings.models.setting.cache/restore-cache!
|
||||
metabase.settings.models.setting/set!
|
||||
metabase.settings.models.setting/validate-settings-formatting!
|
||||
metabase.setup.core/create-token!
|
||||
metabase.sso.ldap-test-util/start-ldap-server!
|
||||
metabase.sync.core/sync-database!
|
||||
metabase.sync.sync-metadata.fields.sync-metadata/update-field-metadata-if-needed!
|
||||
metabase.sync.sync-metadata/sync-db-metadata!
|
||||
metabase.sync.util-test/sync-database!
|
||||
metabase.sync.util/store-sync-summary!
|
||||
metabase.task.core/delete-task!
|
||||
metabase.task.impl/delete-task!
|
||||
metabase.test.data.bigquery-cloud-sdk/execute!
|
||||
metabase.test.data.impl.get-or-create/set-test-db-permissions!
|
||||
metabase.test.data.impl/copy-db-tables-and-fields!
|
||||
metabase.test.data.impl/get-or-create-database!
|
||||
metabase.test.data.impl/get-or-create-default-dataset!
|
||||
metabase.test.data.interface/create-db!
|
||||
metabase.test.data.interface/destroy-db!
|
||||
metabase.test.data.oracle/create-user!
|
||||
metabase.test.data.oracle/drop-user!
|
||||
metabase.test.data.sql-jdbc.load-data/make-insert!
|
||||
metabase.test.data.users/clear-cached-session-tokens!
|
||||
metabase.test.initialize/do-initialization!
|
||||
metabase.test.initialize/initialize-if-needed!
|
||||
metabase.test.util.log/ensure-unique-logger!
|
||||
metabase.test.util.log/set-ns-log-level!
|
||||
metabase.test.util/do-with-temp-env-var-value!
|
||||
metabase.test.util/restore-raw-setting!
|
||||
metabase.test.util/upsert-raw-setting!
|
||||
metabase.test/initialize-if-needed!
|
||||
metabase.test/test-helpers-set-global-values!
|
||||
metabase.test/with-temp-env-var-value!
|
||||
metabase.upload.impl-test/set-local-infile!
|
||||
metabase.util.files/create-dir-if-not-exists!
|
||||
metabase.warehouse-schema.models.field-values/create-or-update-full-field-values!}}}}
|
||||
5
.clj-kondo/macros/common.clj
Normal file
5
.clj-kondo/macros/common.clj
Normal file
@@ -0,0 +1,5 @@
|
||||
(ns macros.common
|
||||
"Helpers for common Metabase macro shapes so we don't have to define the same macroexpansions over and over.")
|
||||
|
||||
(defn ignore-unused [symb]
|
||||
(vary-meta symb assoc :clj-kondo/ignore [:unused-binding]))
|
||||
12
.clj-kondo/macros/metabase/api/card_test.clj
Normal file
12
.clj-kondo/macros/metabase/api/card_test.clj
Normal file
@@ -0,0 +1,12 @@
|
||||
(ns macros.metabase.api.card-test
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-ordered-items
|
||||
[collection model-and-name-syms & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[model binding]]
|
||||
[(macros.common/ignore-unused binding) model])))
|
||||
model-and-name-syms)
|
||||
~collection
|
||||
~@body))
|
||||
17
.clj-kondo/macros/metabase/api/collection_test.clj
Normal file
17
.clj-kondo/macros/metabase/api/collection_test.clj
Normal file
@@ -0,0 +1,17 @@
|
||||
(ns macros.metabase.api.collection-test
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-collection-hierarchy [bindings & body]
|
||||
`(let [~(macros.common/ignore-unused 'a) nil
|
||||
~(macros.common/ignore-unused 'b) nil
|
||||
~(macros.common/ignore-unused 'c) nil
|
||||
~(macros.common/ignore-unused 'd) nil
|
||||
~(macros.common/ignore-unused 'e) nil
|
||||
~(macros.common/ignore-unused 'f) nil
|
||||
~(macros.common/ignore-unused 'g) nil]
|
||||
~bindings
|
||||
~@body))
|
||||
|
||||
(defmacro with-some-children-of-collection [collection-or-id-or-nil & body]
|
||||
`(let [~(macros.common/ignore-unused '&ids) ~collection-or-id-or-nil]
|
||||
~@body))
|
||||
7
.clj-kondo/macros/metabase/api/common.clj
Normal file
7
.clj-kondo/macros/metabase/api/common.clj
Normal 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))
|
||||
13
.clj-kondo/macros/metabase/api/embed_test.clj
Normal file
13
.clj-kondo/macros/metabase/api/embed_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.api.embed-test)
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-card-referencing
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-dashcard-referencing
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
13
.clj-kondo/macros/metabase/api/public_test.clj
Normal file
13
.clj-kondo/macros/metabase/api/public_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.api.public-test)
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-card-referencing
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-dashcard-referencing
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
@@ -1,24 +0,0 @@
|
||||
(ns macros.metabase.collections.api-test)
|
||||
|
||||
(defmacro with-collection-hierarchy! [bindings & body]
|
||||
`(let [~'a nil
|
||||
~'b nil
|
||||
~'c nil
|
||||
~'d nil
|
||||
~'e nil
|
||||
~'f nil
|
||||
~'g nil]
|
||||
~'a
|
||||
~'b
|
||||
~'c
|
||||
~'d
|
||||
~'e
|
||||
~'f
|
||||
~'g
|
||||
~bindings
|
||||
~@body))
|
||||
|
||||
(defmacro with-some-children-of-collection! [collection-or-id-or-nil & body]
|
||||
`(let [~'&ids ~collection-or-id-or-nil]
|
||||
~'&ids
|
||||
~@body))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.embedding.api.embed-test)
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-card-referencing!
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-embedding-enabled-and-temp-dashcard-referencing!
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
@@ -0,0 +1,4 @@
|
||||
(ns macros.metabase.models.params.chain-filter-test)
|
||||
|
||||
(defmacro chain-filter [& _]
|
||||
`(metabase.models.params.chain-filter/chain-filter nil nil))
|
||||
9
.clj-kondo/macros/metabase/models/query_field_test.clj
Normal file
9
.clj-kondo/macros/metabase/models/query_field_test.clj
Normal 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))
|
||||
9
.clj-kondo/macros/metabase/models/user_test.clj
Normal file
9
.clj-kondo/macros/metabase/models/user_test.clj
Normal file
@@ -0,0 +1,9 @@
|
||||
(ns macros.metabase.models.user-test)
|
||||
|
||||
(defmacro with-groups [bindings & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 3)
|
||||
(mapcat (fn [[group-binding properties members]]
|
||||
[group-binding [properties members]])))
|
||||
bindings)
|
||||
~@body))
|
||||
@@ -1,4 +0,0 @@
|
||||
(ns macros.metabase.parameters.chain-filter-test)
|
||||
|
||||
(defmacro chain-filter [& _]
|
||||
`(metabase.parameters.chain-filter/chain-filter nil nil))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.public-sharing.api-test)
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-card-referencing!
|
||||
[table-kw field-kw [card-binding] & body]
|
||||
`(let [~card-binding [~table-kw ~field-kw]]
|
||||
~@body))
|
||||
|
||||
(defmacro with-sharing-enabled-and-temp-dashcard-referencing!
|
||||
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
|
||||
`(let [~dash-binding [~table-kw ~field-kw]
|
||||
~(or card-binding '_) nil
|
||||
~(or dashcard-binding '_) nil]
|
||||
~@body))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns macros.metabase.queries.api.card-test)
|
||||
|
||||
(defmacro with-ordered-items
|
||||
[collection model-and-name-syms & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 2)
|
||||
(mapcat (fn [[model binding]]
|
||||
[binding model])))
|
||||
model-and-name-syms)
|
||||
~collection
|
||||
~@(->> (partition-all 2 model-and-name-syms)
|
||||
(map second))
|
||||
~@body))
|
||||
@@ -1,12 +0,0 @@
|
||||
(ns macros.metabase.query-analysis.models.query-analysis-test)
|
||||
|
||||
(defmacro with-test-setup [& body]
|
||||
`(let [~'card-id 1
|
||||
~'table-id 2
|
||||
~'tax-id 3
|
||||
~'total-id 4]
|
||||
~'card-id
|
||||
~'table-id
|
||||
~'tax-id
|
||||
~'total-id
|
||||
~@body))
|
||||
@@ -1,18 +0,0 @@
|
||||
(ns macros.metabase.query-analysis.task.test-setup)
|
||||
|
||||
(defmacro with-test-setup!
|
||||
"Set up the data required to test the Query Analysis related tasks"
|
||||
[& body]
|
||||
`(let [~'c1 1
|
||||
~'c2 2
|
||||
~'c3 3
|
||||
~'c4 4
|
||||
~'archived 5
|
||||
~'invalid 6]
|
||||
~'c1
|
||||
~'c2
|
||||
~'c3
|
||||
~'c4
|
||||
~'archived
|
||||
~'invalid
|
||||
~@body))
|
||||
13
.clj-kondo/macros/metabase/related_test.clj
Normal file
13
.clj-kondo/macros/metabase/related_test.clj
Normal file
@@ -0,0 +1,13 @@
|
||||
(ns macros.metabase.related-test
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-world [& body]
|
||||
`(let [~(macros.common/ignore-unused 'collection-id) nil
|
||||
~(macros.common/ignore-unused 'metric-id-a) nil
|
||||
~(macros.common/ignore-unused 'metric-id-b) nil
|
||||
~(macros.common/ignore-unused 'segment-id-a) nil
|
||||
~(macros.common/ignore-unused 'segment-id-b) nil
|
||||
~(macros.common/ignore-unused 'card-id-a) nil
|
||||
~(macros.common/ignore-unused 'card-id-b) nil
|
||||
~(macros.common/ignore-unused 'card-id-c) nil]
|
||||
~@body))
|
||||
@@ -1,9 +0,0 @@
|
||||
(ns macros.metabase.server.statistics-handler-test)
|
||||
|
||||
(defmacro with-server
|
||||
[_ _ [connector chan-start-handle chan-finish-handle chan-done-request] & body]
|
||||
`(let [~connector nil
|
||||
~chan-start-handle nil
|
||||
~chan-finish-handle nil
|
||||
~chan-done-request nil]
|
||||
~@body))
|
||||
8
.clj-kondo/macros/metabase/shared/util/namespaces.clj
Normal file
8
.clj-kondo/macros/metabase/shared/util/namespaces.clj
Normal file
@@ -0,0 +1,8 @@
|
||||
(ns macros.metabase.shared.util.namespaces)
|
||||
|
||||
(defmacro import-fn
|
||||
"Kondo macro replacement for [[metabase.shared.util.namespaces/import-fn]]."
|
||||
([sym]
|
||||
`(def ~(-> sym name symbol) "docstring" ~sym))
|
||||
([sym name]
|
||||
`(def ~name "docstring" ~sym)))
|
||||
5
.clj-kondo/macros/metabase/test/persistence.clj
Normal file
5
.clj-kondo/macros/metabase/test/persistence.clj
Normal file
@@ -0,0 +1,5 @@
|
||||
(ns macros.metabase.test.persistence)
|
||||
|
||||
(defmacro with-persistence-enabled [[persist-fn-sym] & body]
|
||||
`(clojure.core/let [~persist-fn-sym (fn [])]
|
||||
~@body))
|
||||
@@ -1,9 +0,0 @@
|
||||
(ns macros.metabase.users.models.user-test)
|
||||
|
||||
(defmacro with-groups! [bindings & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 3)
|
||||
(mapcat (fn [[group-binding properties members]]
|
||||
[group-binding [properties members]])))
|
||||
bindings)
|
||||
~@body))
|
||||
@@ -1,8 +0,0 @@
|
||||
(ns macros.metabase.util.namespaces)
|
||||
|
||||
(defmacro import-fn
|
||||
"Kondo macro replacement for [[metabase.util.namespaces/import-fn]]."
|
||||
([sym]
|
||||
`(def ~(-> sym name symbol) "docstring" ~sym))
|
||||
([sym name]
|
||||
`(def ~name "docstring" ~sym)))
|
||||
16
.clj-kondo/macros/metabase/xrays/domain_entities/malli.clj
Normal file
16
.clj-kondo/macros/metabase/xrays/domain_entities/malli.clj
Normal file
@@ -0,0 +1,16 @@
|
||||
(ns macros.metabase.xrays.domain-entities.malli)
|
||||
|
||||
(defmacro define-getters-and-setters [schema & defs]
|
||||
`(do
|
||||
(comment ~schema) ;; Reference the schema
|
||||
~@(for [[sym _path] (partition 2 defs)]
|
||||
`(do
|
||||
(defn ~(vary-meta sym
|
||||
assoc :export true)
|
||||
"docs" [_x#] nil)
|
||||
(defn ~(vary-meta (symbol (str "with-" (name sym)))
|
||||
assoc :export true)
|
||||
"docs" [x# _new#] x#)
|
||||
(defn ~(vary-meta (symbol (str (name sym) "-js"))
|
||||
assoc :export true)
|
||||
"docs" [x#] x#)))))
|
||||
@@ -1,20 +0,0 @@
|
||||
(ns macros.metabase.xrays.related-test)
|
||||
|
||||
(defmacro with-world [& body]
|
||||
`(let [~'collection-id nil
|
||||
~'metric-id-a nil
|
||||
~'metric-id-b nil
|
||||
~'segment-id-a nil
|
||||
~'segment-id-b nil
|
||||
~'card-id-a nil
|
||||
~'card-id-b nil
|
||||
~'card-id-c nil]
|
||||
~'collection-id
|
||||
~'metric-id-a
|
||||
~'metric-id-b
|
||||
~'segment-id-a
|
||||
~'segment-id-b
|
||||
~'card-id-a
|
||||
~'card-id-b
|
||||
~'card-id-c
|
||||
~@body))
|
||||
@@ -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))
|
||||
@@ -1,5 +0,0 @@
|
||||
(ns macros.metabase-enterprise.database-routing.e2e-test)
|
||||
|
||||
(defmacro with-temp-dbs! [bindings & body]
|
||||
`(let [~@(interleave bindings (repeat nil))]
|
||||
~@body))
|
||||
@@ -1,6 +0,0 @@
|
||||
(ns macros.metabase-enterprise.impersonation.util-test)
|
||||
|
||||
(defmacro with-impersonations! [impersonations-and-attributes-map & body]
|
||||
`(let [~'&group ~impersonations-and-attributes-map]
|
||||
~'&group
|
||||
~@body))
|
||||
@@ -1,18 +0,0 @@
|
||||
(ns macros.metabase-enterprise.query-reference-validation.api-test)
|
||||
|
||||
(defmacro with-test-setup! [& body]
|
||||
`(let [~'card-1 1
|
||||
~'card-2 2
|
||||
~'card-3 3
|
||||
~'card-4 4
|
||||
~'card-5 5
|
||||
~'coll-2 100
|
||||
~'coll-3 200]
|
||||
~'card-1
|
||||
~'card-2
|
||||
~'card-3
|
||||
~'card-4
|
||||
~'card-5
|
||||
~'coll-2
|
||||
~'coll-3
|
||||
~@body))
|
||||
@@ -1,6 +1,6 @@
|
||||
(ns macros.metabase-enterprise.sandbox.test-util)
|
||||
(ns macros.metabase-enterprise.sandbox.test-util
|
||||
(:require [macros.common]))
|
||||
|
||||
(defmacro with-gtaps! [gtaps-definition & body]
|
||||
`(let [~'&group ~gtaps-definition]
|
||||
~'&group
|
||||
`(let [~(macros.common/ignore-unused '&group) ~gtaps-definition]
|
||||
~@body))
|
||||
|
||||
@@ -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))
|
||||
|
||||
329
.clj-kondo/src/hooks/clojure/core.clj
Normal file
329
.clj-kondo/src/hooks/clojure/core.clj
Normal 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)
|
||||
@@ -1,52 +0,0 @@
|
||||
(ns hooks.clojure.core.defmulti
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]))
|
||||
|
||||
(defn- check-arglists [report-node arglists]
|
||||
(letfn [(reg-bad-arglists! []
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta report-node)
|
||||
:message ":arglists should be a quoted list of vectors [:metabase/check-defmulti-arglists]"
|
||||
:type :metabase/check-defmulti-arglists)))
|
||||
(reg-bad-arg! []
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta report-node)
|
||||
:message ":arglists should contain actual arg names, not underscore (unused) symbols [:metabase/check-defmulti-arglists]"
|
||||
:type :metabase/check-defmulti-arglists)))
|
||||
(underscore-arg? [arg]
|
||||
(and (symbol? arg)
|
||||
(str/starts-with? arg "_")))
|
||||
(check-arglist [arglist]
|
||||
(cond
|
||||
(not (vector? arglist)) (reg-bad-arglists!)
|
||||
(some underscore-arg? arglist) (reg-bad-arg!)))]
|
||||
(if-not (and (seq? arglists)
|
||||
(= (first arglists) 'quote)
|
||||
(seq (second arglists)))
|
||||
(reg-bad-arglists!)
|
||||
(let [[_quote arglists] arglists]
|
||||
(doseq [arglist arglists]
|
||||
(check-arglist arglist))))))
|
||||
|
||||
(defn- defmulti-check-for-arglists-metadata
|
||||
"Make sure a [[defmulti]] has an attribute map with `:arglists` metadata."
|
||||
[node]
|
||||
(let [[_defmulti _symb & args] (:children node)
|
||||
[_docstring & args] (if (hooks/string-node? (first args))
|
||||
args
|
||||
(cons nil args))
|
||||
attr-map (when (hooks/map-node? (first args))
|
||||
(first args))
|
||||
arglists (some-> attr-map hooks/sexpr :arglists seq)]
|
||||
(if-not (seq? arglists) ; should be a list or at least list-like
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta node)
|
||||
:message "All defmultis should have an attribute map with :arglists metadata. [:metabase/check-defmulti-arglists]"
|
||||
:type :metabase/check-defmulti-arglists))
|
||||
(check-arglists attr-map arglists))))
|
||||
|
||||
(defn lint-defmulti [x]
|
||||
(defmulti-check-for-arglists-metadata (:node x))
|
||||
x)
|
||||
@@ -1,76 +0,0 @@
|
||||
(ns hooks.clojure.core.defn
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]))
|
||||
|
||||
(defn- end-with-exclamation?
|
||||
[s]
|
||||
(str/ends-with? s "!"))
|
||||
|
||||
(defn- explicitly-safe? [config qualified-symbol]
|
||||
(contains? (get-in config [:linters :metabase/test-helpers-use-non-thread-safe-functions :explicitly-safe])
|
||||
qualified-symbol))
|
||||
|
||||
(defn- explicitly-unsafe? [config qualified-symbol]
|
||||
(contains? (get-in config [:linters :metabase/validate-deftest :parallel/unsafe]) qualified-symbol))
|
||||
|
||||
(defn- unsafe? [config qualified-symbol]
|
||||
(and (or (end-with-exclamation? qualified-symbol)
|
||||
(explicitly-unsafe? config qualified-symbol))
|
||||
(not (explicitly-safe? config qualified-symbol))))
|
||||
|
||||
(defn- non-thread-safe-form-should-end-with-exclamation*
|
||||
[{[defn-or-defmacro form-name] :children, :as node} config]
|
||||
(when-not (and (:string-value form-name)
|
||||
(end-with-exclamation? (:string-value form-name)))
|
||||
(letfn [(walk [f form]
|
||||
(f form)
|
||||
(doseq [child (:children form)]
|
||||
(walk f child)))
|
||||
(check-node [form]
|
||||
(when-let [qualified-symbol (hooks.common/node->qualified-symbol form)]
|
||||
(when (unsafe? config qualified-symbol)
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta form-name)
|
||||
:message (format "The name of this %s should end with `!` because it contains calls to non thread safe form `%s`. [:metabase/test-helpers-use-non-thread-safe-functions]"
|
||||
(:string-value defn-or-defmacro) qualified-symbol)
|
||||
:type :metabase/test-helpers-use-non-thread-safe-functions)))))]
|
||||
(walk check-node node))
|
||||
node))
|
||||
|
||||
(defn non-thread-safe-form-should-end-with-exclamation
|
||||
"Used to ensure defn and defmacro in test namespace to have name ending with `!` if it's non-thread-safe.
|
||||
A function or a macro can be defined as 'not thread safe' when their funciton name ends with a `!`.
|
||||
|
||||
Only used in tests to identify thread-safe/non-thread-safe test helpers. See #37126"
|
||||
[{:keys [node cljc lang config]}]
|
||||
(when (or (not cljc)
|
||||
(= lang :clj))
|
||||
(non-thread-safe-form-should-end-with-exclamation* node config))
|
||||
{:node node})
|
||||
|
||||
(comment
|
||||
(require '[clj-kondo.core :as clj-kondo])
|
||||
(def form (str '(defmacro a
|
||||
[x]
|
||||
`(fun-call x))))
|
||||
|
||||
(def form "(defmacro a
|
||||
[x]
|
||||
`(some! ~x))")
|
||||
|
||||
(def form "(defun f
|
||||
[x]
|
||||
(let [g! (fn [] 1)]
|
||||
(g!)))")
|
||||
|
||||
(str (hooks/parse-string form))
|
||||
(hooks/sexpr (hooks/parse-string form))
|
||||
|
||||
(binding [hooks/*reload* true]
|
||||
(-> form
|
||||
(with-in-str (clj-kondo/run! {:lint ["-"]}))
|
||||
:findings))
|
||||
|
||||
(do (non-thread-safe-form-should-end-with-exclamation* (hooks/parse-string form)) nil))
|
||||
@@ -1,119 +0,0 @@
|
||||
(ns hooks.clojure.core.ns
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]
|
||||
[hooks.common.modules :as modules]))
|
||||
|
||||
(defn- ns-form-node->require-node [ns-form-node]
|
||||
(some (fn [node]
|
||||
(when (and (hooks/list-node? node)
|
||||
(let [first-child (first (:children node))]
|
||||
(and (hooks/keyword-node? first-child)
|
||||
(= (hooks/sexpr first-child) :require))))
|
||||
node))
|
||||
(:children ns-form-node)))
|
||||
|
||||
(defn- lint-require-shapes [ns-form-node]
|
||||
(doseq [node (-> ns-form-node
|
||||
ns-form-node->require-node
|
||||
:children
|
||||
rest)]
|
||||
(cond
|
||||
(not (hooks/vector-node? node))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "All :required namespaces should be wrapped in vectors [:metabase/require-shape-checker]"
|
||||
:type :metabase/require-shape-checker))
|
||||
|
||||
(hooks/vector-node? (second (:children node)))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "Don't use prefix forms inside :require [:metabase/require-shape-checker]"
|
||||
:type :metabase/require-shape-checker)))))
|
||||
|
||||
(defn- lint-requires-on-new-lines [ns-form-node]
|
||||
(let [[require-keyword first-require] (-> ns-form-node
|
||||
ns-form-node->require-node
|
||||
:children)]
|
||||
(when-let [require-keyword-line (:row (meta require-keyword))]
|
||||
(when-let [first-require-line (:row (meta first-require))]
|
||||
(when (= require-keyword-line first-require-line)
|
||||
(hooks/reg-finding! (assoc (meta first-require)
|
||||
:message "Put your requires on a newline from the :require keyword [:metabase/require-shape-checker]"
|
||||
:type :metabase/require-shape-checker)))))))
|
||||
|
||||
(defn- require-node->namespace-symb-nodes [require-node]
|
||||
(let [[_ns & args] (:children require-node)]
|
||||
(into []
|
||||
;; prefixed namespace forms are NOT SUPPORTED!!!!!!!!1
|
||||
(keep (fn [node]
|
||||
(cond
|
||||
(hooks/vector-node? node)
|
||||
;; propagate the metadata attached to this vector in case there's a `:clj-kondo/ignore` form.
|
||||
(let [symbol-node (first (:children node))]
|
||||
(hooks.common/merge-ignored-linters symbol-node require-node node))
|
||||
|
||||
;; this should also be dead code since we require requires to be vectors
|
||||
(hooks/token-node? node)
|
||||
(hooks.common/merge-ignored-linters node require-node)
|
||||
|
||||
:else
|
||||
(printf "Don't know how to figure out what namespace is being required in %s\n" (pr-str node)))))
|
||||
args)))
|
||||
|
||||
(defn- ns-form-node->ns-symb-node [ns-form-node]
|
||||
(some (fn [node]
|
||||
(when (and (hooks/token-node? node)
|
||||
(not= (hooks/sexpr node) 'ns))
|
||||
node))
|
||||
(:children ns-form-node)))
|
||||
|
||||
(defn- ns-form-node->ns-symb [ns-form-node]
|
||||
(some-> (ns-form-node->ns-symb-node ns-form-node)
|
||||
hooks/sexpr))
|
||||
|
||||
(defn- lint-modules [ns-form-node config]
|
||||
(let [ns-symb (ns-form-node->ns-symb ns-form-node)]
|
||||
(when-not (modules/ignored-namespace? config ns-symb)
|
||||
(when-let [current-module (modules/module ns-symb)]
|
||||
(let [required-namespace-symb-nodes (-> ns-form-node
|
||||
ns-form-node->require-node
|
||||
require-node->namespace-symb-nodes)]
|
||||
(doseq [node required-namespace-symb-nodes
|
||||
:when (not (contains? (hooks.common/ignored-linters node) :metabase/modules))
|
||||
:let [required-namespace (hooks/sexpr node)
|
||||
error (modules/usage-error config current-module required-namespace)]
|
||||
:when error]
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message error
|
||||
:type :metabase/modules))))))))
|
||||
|
||||
(defn- lint-namespace-name [ns-form-node]
|
||||
(when-let [ns-symb-node (ns-form-node->ns-symb-node ns-form-node)]
|
||||
(when-not (contains? (hooks.common/ignored-linters ns-symb-node) :metabase/namespace-name)
|
||||
(let [parts (str/split (str (hooks/sexpr ns-symb-node)) #"\.")]
|
||||
(when (and (<= (count parts) 2)
|
||||
(#{"metabase" "metabase-enterprise"} (first parts))
|
||||
;; exclude test namespaces for now
|
||||
(not (str/ends-with? (last parts) "test")))
|
||||
(hooks/reg-finding! (assoc (meta ns-symb-node)
|
||||
:message "Metabase namespaces should have the form metabase[-enterprise].<module>.* [:metabase/namespace-name]"
|
||||
:type :metabase/namespace-name)))))))
|
||||
|
||||
(defn lint-ns [x]
|
||||
(doto (:node x)
|
||||
lint-require-shapes
|
||||
lint-requires-on-new-lines
|
||||
(lint-modules (modules/config x))
|
||||
lint-namespace-name)
|
||||
x)
|
||||
|
||||
(comment
|
||||
(lint-ns
|
||||
{:node (hooks/parse-string
|
||||
(pr-str
|
||||
'(ns hooks.clojure.core.ns
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]
|
||||
[hooks.common.modules :as modules]))))}))
|
||||
@@ -1,75 +0,0 @@
|
||||
(ns hooks.clojure.core.require
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common.modules :as modules]))
|
||||
|
||||
(defn- lint* [node current-ns config required-namespaces]
|
||||
{:pre [(map? node)
|
||||
(simple-symbol? current-ns)
|
||||
(map? config)
|
||||
(sequential? required-namespaces)
|
||||
(every? simple-symbol? required-namespaces)]}
|
||||
(when-not (modules/ignored-namespace? config current-ns)
|
||||
;; ignore namespaces outside of the module system
|
||||
(when-let [current-module (modules/module current-ns)]
|
||||
(doseq [required-namespace required-namespaces]
|
||||
;; ignore namespaces outside of the module system.
|
||||
(when (modules/module required-namespace)
|
||||
(when-let [error (modules/usage-error config current-module required-namespace)]
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message error
|
||||
:type :metabase/modules))))))))
|
||||
|
||||
(defn- unwrap-require [node]
|
||||
(case (hooks/tag node)
|
||||
;; some.namespace of some.namespace/some-var
|
||||
:token
|
||||
(let [sexpr (hooks/sexpr node)]
|
||||
(if (qualified-symbol? sexpr)
|
||||
(symbol (namespace sexpr))
|
||||
sexpr))
|
||||
|
||||
;; (quote some.namespace)
|
||||
:list
|
||||
(let [[x y] (:children node)]
|
||||
(when (and (hooks/token-node? x)
|
||||
(= (hooks/sexpr x) 'quote))
|
||||
(recur y)))
|
||||
|
||||
;; 'some.namespace
|
||||
:quote
|
||||
(recur (first (:children node)))
|
||||
|
||||
;; something like [some.namespace :as whatever]
|
||||
:vector
|
||||
(recur (first (:children node)))))
|
||||
|
||||
(comment
|
||||
(unwrap-require (-> "(requiring-resolve 'metabase.notification.payload.execute/execute-dashboard)"
|
||||
hooks/parse-string
|
||||
:children
|
||||
second)))
|
||||
|
||||
(defn- lint-require* [node current-ns config]
|
||||
(let [[_require & args] (:children node)
|
||||
required-namespaces (->> (keep unwrap-require args)
|
||||
;; `require` runs through `cc/load-libs` which allows
|
||||
;; #{:as :reload :reload-all :require :use :verbose :refer :as-alias}
|
||||
;; as args. If you use a REPL tool that applies clj-kondo linting (e.g., Clojure MCP)
|
||||
;; you may wish to `:reload` or `:reload-all` a namespace. So we shouldn't assume all
|
||||
;; args are keywords:
|
||||
(remove keyword?))]
|
||||
(lint* node current-ns config required-namespaces)))
|
||||
|
||||
(defn- lint-requiring-resolve* [node current-ns config]
|
||||
(let [[_requiring-resolve symb-node] (:children node)
|
||||
required-namespace (unwrap-require symb-node)]
|
||||
(lint* node current-ns config [required-namespace])))
|
||||
|
||||
(defn lint-require [x]
|
||||
(lint-require* (:node x) (:ns x) (modules/config x))
|
||||
x)
|
||||
|
||||
(defn lint-requiring-resolve [x]
|
||||
(lint-requiring-resolve* (:node x) (:ns x) (modules/config x))
|
||||
x)
|
||||
@@ -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})
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
(ns hooks.common.modules)
|
||||
|
||||
(defn ignored-namespace? [config ns-symb]
|
||||
(some
|
||||
(fn [pattern-str]
|
||||
(re-find (re-pattern pattern-str) (str ns-symb)))
|
||||
(:ignored-namespace-patterns config)))
|
||||
|
||||
(defn config [{:keys [config], :as _hook-input}]
|
||||
(merge (get-in config [:linters :metabase/modules])
|
||||
(select-keys config [:metabase/modules])))
|
||||
|
||||
(defn module
|
||||
"E.g.
|
||||
|
||||
(module 'metabase.qp.middleware.wow) => 'qp
|
||||
(module 'metabase-enterprise.whatever.core) => enterprise/whatever"
|
||||
[ns-symb]
|
||||
{:pre [(simple-symbol? ns-symb)]}
|
||||
(or (some->> (re-find #"^metabase-enterprise\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
(symbol "enterprise"))
|
||||
(some-> (re-find #"^metabase\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
symbol)))
|
||||
|
||||
(defn module-api-namespaces
|
||||
"Set of API namespace symbols for a given module. `:any` means you can use anything, there are no API namespaces for
|
||||
this module (yet). If unspecified, the default is just the `<module>.core` namespace."
|
||||
[config module]
|
||||
(let [module-config (get-in config [:metabase/modules module :api])]
|
||||
(cond
|
||||
(= module-config :any)
|
||||
nil
|
||||
|
||||
(set? module-config)
|
||||
module-config
|
||||
|
||||
:else
|
||||
(let [ns-prefix (if (= (namespace module) "enterprise")
|
||||
(str "metabase-enterprise." (name module))
|
||||
(name module))]
|
||||
#{(symbol (str ns-prefix ".api"))
|
||||
(symbol (str ns-prefix ".core"))
|
||||
(symbol (str ns-prefix ".init"))}))))
|
||||
|
||||
(defn allowed-modules
|
||||
"Set of namespace symbols that `module` is allowed to use. `:any` means it's allowed to use anything."
|
||||
[config module]
|
||||
(get-in config [:metabase/modules module :uses]))
|
||||
|
||||
(defn allowed-module? [config module required-module]
|
||||
(let [allowed-modules (allowed-modules config module)]
|
||||
(or (= allowed-modules :any)
|
||||
(contains? (set allowed-modules) required-module))))
|
||||
|
||||
(defn allowed-module-namespace? [config ns-symb]
|
||||
(let [module (module ns-symb)
|
||||
module-api-namespaces (module-api-namespaces config module)]
|
||||
(or (empty? module-api-namespaces)
|
||||
(contains? module-api-namespaces ns-symb))))
|
||||
|
||||
(defn usage-error
|
||||
"Find usage errors when a `required-namespace` is required in the `current-module`. Returns a string describing the
|
||||
error type if there is one, otherwise `nil` if there are no errors."
|
||||
[config current-module required-namespace]
|
||||
;; ignore stuff not in a module i.e. non-Metabase stuff.
|
||||
(when-let [required-module (module required-namespace)]
|
||||
(when-not (= current-module required-module)
|
||||
(cond
|
||||
(not (allowed-module? config current-module required-module))
|
||||
(format "Module %s should not be used in the %s module. [:metabase/modules %s :uses]"
|
||||
required-module
|
||||
current-module
|
||||
current-module)
|
||||
|
||||
(not (allowed-module-namespace? config required-namespace))
|
||||
(format "Namespace %s is not an allowed external API namespace for the %s module. [:metabase/modules %s :api]"
|
||||
required-namespace
|
||||
required-module
|
||||
required-module)))))
|
||||
@@ -1,26 +0,0 @@
|
||||
(ns hooks.malli.error
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common]))
|
||||
|
||||
(defn humanize [x]
|
||||
(letfn [(check-node [node]
|
||||
(when (hooks/list-node? node)
|
||||
(let [[_ child] (:children node)]
|
||||
(when (hooks/list-node? child)
|
||||
(let [[symb] (:children child)]
|
||||
(when-let [qualified-symb (hooks.common/node->qualified-symbol symb)]
|
||||
(when ('#{malli.core/validate metabase.util.malli.registry/validate}
|
||||
qualified-symb)
|
||||
(hooks/reg-finding! (assoc (meta symb)
|
||||
:message "Use malli.error/humanize with explain, NOT with validate. [:metabase/check-me-humanize]"
|
||||
:type :metabase/check-me-humanize)))))))))]
|
||||
(check-node (:node x))
|
||||
x))
|
||||
|
||||
(comment
|
||||
(humanize {:node (hooks/parse-string
|
||||
(pr-str '(malli.error/humanize
|
||||
(malli.core/validate
|
||||
metabase.sync.sync-metadata.dbms-version/DBMSVersion
|
||||
metabase.driver.druid.sync-test/dbms-version))))}))
|
||||
@@ -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*}))
|
||||
45
.clj-kondo/src/hooks/metabase/api/common.clj
Normal file
45
.clj-kondo/src/hooks/metabase/api/common.clj
Normal 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))
|
||||
@@ -1,145 +0,0 @@
|
||||
(ns hooks.metabase.api.macros
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn- validate-route!
|
||||
"Make sure the route uses kebab-case rather than snake_case."
|
||||
[route]
|
||||
(letfn [(validate-route-string! [route-string]
|
||||
(when (and (api/string-node? route-string)
|
||||
(str/includes? (api/sexpr route-string) "_"))
|
||||
(api/reg-finding! (assoc (meta route-string)
|
||||
:message "REST API routes and query params should use kebab-case, not snake_case"
|
||||
:type :metabase/validate-defendpoint-route-uses-kebab-case))))]
|
||||
(cond
|
||||
(api/string-node? route)
|
||||
(validate-route-string! route)
|
||||
;; otherwise route is a vector like ["/abc/:d/" :d <regex>]
|
||||
(api/vector-node? route)
|
||||
(validate-route-string! (first (:children route))))))
|
||||
|
||||
(defn- validate-query-params!
|
||||
"Make sure query params use kebab-case rather than snake_case.
|
||||
|
||||
This only works on inline schemas for now. If you define the schema elsewhere then we'd have to implement this
|
||||
validation in Clojure-land since we can't resolve the schemas in Kondo. It should however still work on complicated
|
||||
schemas like
|
||||
|
||||
[:merge <map-1> <map-2>]"
|
||||
[query-param-schema-node]
|
||||
(letfn [(map-schema? [node]
|
||||
(and (api/vector-node? node)
|
||||
(when-let [first-child (first (:children node))]
|
||||
(and (api/keyword-node? first-child)
|
||||
(= (api/sexpr first-child) :map)))))
|
||||
(map-schema-keys [{:keys [children], :as _node}]
|
||||
(let [child-schemas (if (api/map-node? (second children))
|
||||
(drop 2 children)
|
||||
(drop 1 children))]
|
||||
(keep (fn [child-schema]
|
||||
(when (api/vector-node? child-schema)
|
||||
(when-let [k (first (:children child-schema))]
|
||||
(when (api/keyword-node? k)
|
||||
k))))
|
||||
child-schemas)))
|
||||
(validate-map-schema! [form]
|
||||
;; only warn once per endpoint -- just once on the first bad node
|
||||
(when-let [bad-node (some (fn [key-node]
|
||||
(when (str/includes? (str (api/sexpr key-node)) "_")
|
||||
key-node))
|
||||
(map-schema-keys form))]
|
||||
(api/reg-finding! (assoc (meta bad-node)
|
||||
:message "REST API routes and query params should use kebab-case, not snake_case"
|
||||
:type :metabase/validate-defendpoint-query-params-use-kebab-case))))
|
||||
(validate! [node]
|
||||
(cond
|
||||
(and (api/vector-node? node)
|
||||
(map-schema? node))
|
||||
(validate-map-schema! node)
|
||||
|
||||
(api/vector-node? node)
|
||||
(doseq [child (:children node)]
|
||||
(validate! child))))]
|
||||
(validate! query-param-schema-node)))
|
||||
|
||||
(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 z & more] (:children params)]
|
||||
(cond
|
||||
(not x)
|
||||
[bindings schemas]
|
||||
|
||||
(and (api/keyword-node? y)
|
||||
(= (api/sexpr y) :-))
|
||||
(recur (conj bindings x) (conj schemas z) more)
|
||||
|
||||
:else
|
||||
(recur (conj bindings x) (conj schemas nil) (list* y z more)))))]
|
||||
(validate-route! route)
|
||||
(let [[_route-param-schema query-param-schema _body-param-schema] schemas]
|
||||
(when query-param-schema
|
||||
(validate-query-params! query-param-schema)))
|
||||
(-> (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)))
|
||||
|
||||
(comment
|
||||
(defn -defendpoint [form]
|
||||
(-> {:node (-> form pr-str api/parse-string)}
|
||||
defendpoint
|
||||
:node
|
||||
api/sexpr))
|
||||
|
||||
(defn x []
|
||||
(-defendpoint '(api.macros/defendpoint :get "/:id/syncable_schemas" ; snake_case route should trigger a warning
|
||||
"Returns a list of all syncable schemas found for the database `id`."
|
||||
[{:keys [id]} :- [:map
|
||||
[:id ms/PositiveInt]]
|
||||
;; snake_case query parameter, should trigger a warning
|
||||
{:keys [filter_by]} :- [:map
|
||||
[:filter_by {:optional true} :string]]]
|
||||
(let [db (get-database id)]
|
||||
(api/check-403 (or (:is_attached_dwh db)
|
||||
(and (mi/can-write? db)
|
||||
(mi/can-read? db))))
|
||||
(->> db
|
||||
(driver/syncable-schemas (:engine db))
|
||||
(vec)
|
||||
(sort)))))))
|
||||
17
.clj-kondo/src/hooks/metabase/api/search_test.clj
Normal file
17
.clj-kondo/src/hooks/metabase/api/search_test.clj
Normal file
@@ -0,0 +1,17 @@
|
||||
(ns hooks.metabase.api.search-test
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn with-search-items-in-collection [{{[_ & args] :children, :as node} :node}]
|
||||
;; (println \newline)
|
||||
;; (clojure.pprint/pprint (hooks/sexpr node))
|
||||
(let [[created-items-sym search-string & body] args
|
||||
node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node 'let)
|
||||
(hooks/vector-node
|
||||
[created-items-sym (hooks/token-node 'nil)])
|
||||
search-string
|
||||
body))]
|
||||
;; (println '=>)
|
||||
;; (clojure.pprint/pprint (hooks/sexpr node*))
|
||||
{:node node*}))
|
||||
@@ -1,14 +0,0 @@
|
||||
(ns hooks.metabase.app-db.schema-migrations-test.impl
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn test-migrations
|
||||
[{{[_ migration-range {[binding] :children} & body] :children} :node}]
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node `let)
|
||||
(hooks/vector-node
|
||||
[binding (hooks/list-node (list (hooks/token-node `fn)
|
||||
(hooks/vector-node [(hooks/token-node '&) (hooks/token-node '_args)])))])
|
||||
migration-range
|
||||
body))]
|
||||
{:node node*}))
|
||||
@@ -1,25 +0,0 @@
|
||||
(ns hooks.metabase.channel.template.handlebars-helper
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common]))
|
||||
|
||||
(defn defhelper
|
||||
"defhelper hook"
|
||||
[{:keys [node]}]
|
||||
(let [[_ helper-name description argvec & body] (:children node)
|
||||
helper-fn-name (symbol (str (hooks/sexpr helper-name) "*"))]
|
||||
{:node (-> (hooks/list-node
|
||||
(list (hooks/token-node 'do)
|
||||
(hooks/list-node
|
||||
(list (hooks/token-node 'defn)
|
||||
(hooks/token-node helper-fn-name)
|
||||
description
|
||||
argvec
|
||||
(hooks/list-node body)))
|
||||
(hooks/list-node
|
||||
(list (hooks/token-node 'defn)
|
||||
helper-name
|
||||
description
|
||||
argvec
|
||||
(hooks/list-node body)))))
|
||||
(with-meta (meta node)))}))
|
||||
@@ -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*}))
|
||||
@@ -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)}))
|
||||
@@ -33,36 +33,18 @@
|
||||
(defclause [ag:var var] field-or-expression FieldOrExpressionDef)
|
||||
=>
|
||||
(def ag:var [FieldOrExpressionDef])"
|
||||
[{:keys [node], :as x}]
|
||||
(let [[_defclause clause-name & arg-specs] (:children node)]
|
||||
(assoc x :node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'do)
|
||||
(-> (hooks/keyword-node (keyword "metabase.legacy-mbql.schema" (name (hooks/sexpr clause-name))))
|
||||
(with-meta (meta clause-name))
|
||||
(hooks/reg-keyword! 'metabase.legacy-mbql.schemas.macros/defclause))
|
||||
(collect-defclause-body-schemas arg-specs)))
|
||||
(with-meta (meta node))))))
|
||||
|
||||
(defn defclause*
|
||||
"e.g.
|
||||
|
||||
(defclause [ag:var var] field-or-expression FieldOrExpressionDef)
|
||||
=>
|
||||
(def ag:var [FieldOrExpressionDef])"
|
||||
[{:keys [node], :as x}]
|
||||
(let [[_defclause clause-name schema] (:children node)]
|
||||
(assoc x :node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'do)
|
||||
(-> (hooks/keyword-node (keyword "metabase.legacy-mbql.schema" (name (hooks/sexpr clause-name))))
|
||||
(with-meta (meta clause-name))
|
||||
(hooks/reg-keyword! 'metabase.legacy-mbql.schemas.macros/defclause*))
|
||||
schema))
|
||||
(with-meta (meta node))))))
|
||||
[{:keys [node]}]
|
||||
(let [[_defclause clause-name-form & arg-specs] (:children node)]
|
||||
{:node (-> (hooks/list-node
|
||||
(list
|
||||
(hooks/token-node 'def)
|
||||
(unwrap-defclause-clause-name clause-name-form)
|
||||
(hooks/string-node "Docstring.")
|
||||
(collect-defclause-body-schemas arg-specs)))
|
||||
(with-meta (meta node)))}))
|
||||
|
||||
(comment
|
||||
(defn- expand-defclause [form]
|
||||
(defn- defclause* [form]
|
||||
(hooks/sexpr
|
||||
(:node
|
||||
(defclause
|
||||
@@ -73,35 +55,15 @@
|
||||
form)))}))))
|
||||
|
||||
(defn- x []
|
||||
(expand-defclause '(defclause var
|
||||
field-or-expression FieldOrExpressionDef)))
|
||||
(defclause* '(defclause [ag:var var]
|
||||
field-or-expression FieldOrExpressionDef)))
|
||||
;; =>
|
||||
(do :metabase.legacy-mbql.schema/var [FieldOrExpressionDef])
|
||||
(def ag:var "Docstring." [FieldOrExpressionDef])
|
||||
|
||||
(defn- y []
|
||||
(expand-defclause '(defclause var
|
||||
x (some-list-schema)
|
||||
y (optional FieldOrExpressionDef)
|
||||
z (rest (some-other-schema)))))
|
||||
(defclause* '(defclause var
|
||||
x (some-list-schema)
|
||||
y (optional FieldOrExpressionDef)
|
||||
z (rest (some-other-schema)))))
|
||||
;; =>
|
||||
(do :metabase.legacy-mbql.schema/var [(some-list-schema) FieldOrExpressionDef (some-other-schema)]))
|
||||
|
||||
(defn one-of [{:keys [node], :as x}]
|
||||
(let [[_one-of & schema-nodes] (:children node)
|
||||
node' (-> (hooks/vector-node
|
||||
(mapv (fn [schema-node]
|
||||
(if (and (hooks/token-node? schema-node)
|
||||
(simple-symbol? (hooks/sexpr schema-node)))
|
||||
(-> (hooks/keyword-node (keyword "mteabase.legacy-mbql.schema" (name (hooks/sexpr schema-node))))
|
||||
(with-meta (meta schema-node)))
|
||||
schema-node))
|
||||
schema-nodes))
|
||||
(with-meta (meta node)))]
|
||||
(assoc x :node node')))
|
||||
|
||||
(comment
|
||||
(defn- expand-one-of [form]
|
||||
(hooks/sexpr (:node (one-of {:node (hooks/parse-string (with-out-str (clojure.pprint/pprint form)))}))))
|
||||
|
||||
(defn- z []
|
||||
(expand-one-of '(one-of absolute-datetime relative-datetime time))))
|
||||
(def var "Docstring." [(some-list-schema) FieldOrExpressionDef (some-other-schema)]))
|
||||
|
||||
@@ -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*}))
|
||||
|
||||
@@ -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)))
|
||||
@@ -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}))
|
||||
|
||||
264
.clj-kondo/src/hooks/metabase/models/setting.clj
Normal file
264
.clj-kondo/src/hooks/metabase/models/setting.clj
Normal 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)))))))
|
||||
@@ -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)))))
|
||||
@@ -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)))))
|
||||
@@ -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*}))
|
||||
@@ -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)))))))
|
||||
@@ -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`."
|
||||
|
||||
@@ -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))
|
||||
@@ -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*)))
|
||||
@@ -1,6 +1,6 @@
|
||||
(ns hooks.metabase.test.util
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]))
|
||||
(:require [clj-kondo.hooks-api :as hooks]
|
||||
[hooks.common :as common]))
|
||||
|
||||
(defn- namespaced-symbol-node? [node]
|
||||
(when (hooks/token-node? node)
|
||||
@@ -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*
|
||||
|
||||
@@ -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*)))
|
||||
@@ -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)))
|
||||
|
||||
@@ -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)
|
||||
@@ -109,6 +109,8 @@
|
||||
actual-arg-count)
|
||||
:type :metabase/validate-logging)))))
|
||||
|
||||
|
||||
|
||||
(defn infof
|
||||
"Valid logf, debugf, etc."
|
||||
[{:keys [node], :as x}]
|
||||
|
||||
@@ -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)))
|
||||
@@ -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)))))))
|
||||
@@ -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*}))
|
||||
@@ -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))))))))
|
||||
@@ -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*)))))
|
||||
@@ -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))))))
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
(ns hooks.metabase.api.macros-test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[clj-kondo.impl.utils]
|
||||
[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))))))))
|
||||
|
||||
(defn- defendpoint [form]
|
||||
(hooks.metabase.api.macros/defendpoint
|
||||
{:node (-> form pr-str api/parse-string)}))
|
||||
|
||||
(defn- defendpoint-warnings
|
||||
[form]
|
||||
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/validate-defendpoint-route-uses-kebab-case {:level :warning}
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case {:level :warning}}}
|
||||
:ignores (atom nil)
|
||||
:findings (atom [])
|
||||
:namespaces (atom {})}]
|
||||
(defendpoint form)
|
||||
@(:findings clj-kondo.impl.utils/*ctx*)))
|
||||
|
||||
(deftest ^:parallel validate-route-kebab-case-test
|
||||
(is (=? [{:message "REST API routes and query params should use kebab-case, not snake_case"
|
||||
:type :metabase/validate-defendpoint-route-uses-kebab-case
|
||||
:row pos-int?
|
||||
:col pos-int?}]
|
||||
(defendpoint-warnings
|
||||
'(api.macros/defendpoint :get "/:id/syncable_schemas" ; snake_case route should trigger a warning
|
||||
"Returns a list of all syncable schemas found for the database `id`."
|
||||
[{:keys [id]}]
|
||||
id)))))
|
||||
|
||||
(deftest ^:parallel validate-query-params-kebab-case-test
|
||||
(is (=? [{:message "REST API routes and query params should use kebab-case, not snake_case",
|
||||
:type :metabase/validate-defendpoint-query-params-use-kebab-case
|
||||
:row pos-int?
|
||||
:col pos-int?}]
|
||||
(defendpoint-warnings
|
||||
'(api.macros/defendpoint :get "/:id/syncable-schemas"
|
||||
"Returns a list of all syncable schemas found for the database `id`."
|
||||
[{:keys [id]}
|
||||
{:keys [filter_by]} :- [:map
|
||||
[:filter_by {:optional true} :string]]]
|
||||
id)))))
|
||||
@@ -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 <>)))))
|
||||
@@ -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)))))
|
||||
@@ -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")))
|
||||
@@ -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))))
|
||||
@@ -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]
|
||||
|
||||
389
.cljfmt.edn
389
.cljfmt.edn
@@ -1,358 +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/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/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}
|
||||
|
||||
@@ -1,10 +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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.babel_cache
|
||||
docs/*
|
||||
target/*
|
||||
.circleci
|
||||
.cpcache
|
||||
|
||||
@@ -2,4 +2,3 @@ frontend/src/cljs
|
||||
frontend/src/cljs_release
|
||||
e2e/support/cypress_sample_database.js
|
||||
e2e/support/cypress_sample_instance_data.js
|
||||
e2e/embedding-sdk-host-apps
|
||||
|
||||
150
.eslintrc.js
150
.eslintrc.js
@@ -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",
|
||||
@@ -136,13 +118,13 @@ module.exports = {
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2020: true,
|
||||
es6: true,
|
||||
commonjs: true,
|
||||
jest: true,
|
||||
"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,41 +167,17 @@ module.exports = {
|
||||
"*.unit.spec.*",
|
||||
"frontend/src/metabase/admin/**/*",
|
||||
"frontend/src/metabase/setup/**/*",
|
||||
"enterprise/frontend/src/metabase-enterprise/whitelabel/**/*",
|
||||
"enterprise/frontend/src/metabase-enterprise/embedding/**/*",
|
||||
"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"],
|
||||
@@ -248,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
|
||||
@@ -273,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",
|
||||
@@ -284,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
276
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
16
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/epic.md
vendored
23
.github/ISSUE_TEMPLATE/epic.md
vendored
@@ -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***
|
||||
|
||||
|
||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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). 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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
16
.github/actions/await-port/action.yml
vendored
16
.github/actions/await-port/action.yml
vendored
@@ -1,16 +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: |
|
||||
while ! nc -z localhost ${{ inputs.port }}; do
|
||||
sleep 0.1;
|
||||
done
|
||||
69
.github/actions/build-e2e-matrix/action.yml
vendored
Normal file
69
.github/actions/build-e2e-matrix/action.yml
vendored
Normal 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;
|
||||
36
.github/actions/cleanup-python-runner/action.yml
vendored
36
.github/actions/cleanup-python-runner/action.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: Cleanup Python Runner
|
||||
description: Cleanup python-runner services and upload logs as artifacts
|
||||
|
||||
inputs:
|
||||
upload-logs:
|
||||
description: 'Whether to upload logs (typically on failure)'
|
||||
required: false
|
||||
default: 'false'
|
||||
logs-artifact-suffix:
|
||||
description: 'The unique name of the .zip artifact containing the logs'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Cleanup python-runner services
|
||||
if: always()
|
||||
run: |
|
||||
echo "Stopping python-runner services..."
|
||||
if [ -d "python-runner" ]; then
|
||||
cd python-runner
|
||||
make stop || echo "WARNING: Failed to stop python runner"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Upload python-runner logs
|
||||
if: ${{inputs.upload-logs == 'true' && inputs.logs-artifact-suffix != ''}}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-runner-logs-${{inputs.logs-artifact-suffix}}
|
||||
path: |
|
||||
python-runner.out.log
|
||||
python-runner.err.log
|
||||
retention-days: 1
|
||||
if-no-files-found: warn
|
||||
110
.github/actions/create-backport/action.yml
vendored
110
.github/actions/create-backport/action.yml
vendored
@@ -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.event.pull_request.user.login }}" --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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user