From f926e90838e46f22db7b0e2f2e0219516dada5d0 Mon Sep 17 00:00:00 2001 From: Chris Truter <crisptrutski@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:52:36 +0200 Subject: [PATCH] Add linter to rachet down implicitly non-exported defsettings (#37697) * Add kondo for missing :export? on defsetting * And tons of explicit ignores, including some inadvertent test vars, probably * Disable linter in tests * Handle multi settings --- .clj-kondo/config.edn | 5 +- .clj-kondo/hooks/metabase/models/setting.clj | 268 ++++++++++++++++--- src/metabase/driver/util.clj | 3 +- src/metabase/models/setting.clj | 12 +- 4 files changed, 242 insertions(+), 46 deletions(-) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 0364d53c904..0b46ca46f5f 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -18,6 +18,8 @@ :use {:level :warning} :warn-on-reflection {:level :warning} + :metabase/defsetting-must-specify-export {:level :warning} + ;; ;; disabled linters ;; @@ -531,7 +533,7 @@ metabase.models.interface/define-batched-hydration-method hooks.metabase.models.interface/define-hydration-method metabase.models.pulse-test/with-dashboard-subscription-in-collection hooks.common/with-four-bindings metabase.models.pulse-test/with-pulse-in-collection hooks.common/with-four-bindings - metabase.models.setting.multi-setting/define-multi-setting hooks.metabase.models.setting/defsetting + metabase.models.setting.multi-setting/define-multi-setting hooks.metabase.models.setting/define-multi-setting metabase.models.setting/defsetting hooks.metabase.models.setting/defsetting metabase.public-settings.premium-features/defenterprise hooks.metabase.public-settings.premium-features/defenterprise metabase.pulse.test-util/checkins-query-card hooks.metabase.test.data/$ids @@ -637,6 +639,7 @@ {:inline-def {:level :off} :missing-docstring {:level :off} :private-call {:level :off} + :metabase/defsetting-must-specify-export {:level :off} :hooks.metabase.test.data/mbql-query-first-arg {:level :error}}} source-namespaces diff --git a/.clj-kondo/hooks/metabase/models/setting.clj b/.clj-kondo/hooks/metabase/models/setting.clj index 600d7c515af..4ec17037ea6 100644 --- a/.clj-kondo/hooks/metabase/models/setting.clj +++ b/.clj-kondo/hooks/metabase/models/setting.clj @@ -3,6 +3,205 @@ [clj-kondo.hooks-api :as hooks] [hooks.common :as common])) +(def ^:private ignored-implicit-export? + '#{active-users-count + admin-email + analytics-uuid + anon-tracking-enabled + api-key + attachment-table-row-limit + audit-max-retention-days + bcc-enabled? + breakout-bin-width + check-for-updates + cloud-gateway-ips + config-from-file-sync-databases + custom-homepage + custom-homepage-dashboard + database-enable-actions + deprecation-notice-version + dismissed-custom-dashboard-toast + email-configured? + email-from-address + email-from-name + email-reply-to + email-smtp-host + email-smtp-password + email-smtp-port + email-smtp-security + email-smtp-username + embedding-app-origin + embedding-secret-key + enable-password-login + enable-public-sharing + enable-query-caching + engines + enum-cardinality-threshold + follow-up-email-sent + ga-code + ga-enabled + google-auth-auto-create-accounts-domain + google-auth-client-id + google-auth-configured + google-auth-enabled + has-sample-database? + has-user-setup + help-link + help-link-custom-destination + instance-creation + is-hosted? + is-metabot-enabled + jdbc-data-warehouse-max-connection-pool-size + jdbc-data-warehouse-unreturned-connection-timeout-seconds + jwt-attribute-email + jwt-attribute-firstname + jwt-attribute-groups + jwt-attribute-lastname + jwt-configured + jwt-enabled + jwt-group-mappings + jwt-group-sync + jwt-identity-provider-uri + jwt-shared-secret + last-acknowledged-version + ldap-attribute-email + ldap-attribute-firstname + ldap-attribute-lastname + ldap-bind-dn + ldap-configured? + ldap-enabled + ldap-group-base + ldap-group-mappings + ldap-group-membership-filter + ldap-group-sync + ldap-host + ldap-password + ldap-port + ldap-security + ldap-sync-user-attributes + ldap-sync-user-attributes-blacklist + ldap-user-base + ldap-user-filter + load-analytics-content + map-tile-server-url + metabot-default-embedding-model + metabot-feedback-url + metabot-get-prompt-templates-url + metabot-prompt-generator-token-limit + multi-setting-read-only + notification-link-base-url + num-metabot-choices + openai-api-key + openai-available-models + openai-model + openai-organization + other-sso-enabled? + password-complexity + persist-models-enabled + persisted-model-refresh-cron-schedule + premium-embedding-token + prometheus-server-port + query-caching-max-kb + query-caching-max-ttl + query-caching-min-ttl + query-caching-ttl-ratio + redirect-all-requests-to-https + reset-token-ttl-hours + retired-setting + retry-initial-interval + retry-max-attempts + retry-max-interval-millis + retry-multiplier + retry-randomization-factor + saml-application-name + saml-attribute-email + saml-attribute-firstname + saml-attribute-group + saml-attribute-lastname + saml-configured + saml-enabled + saml-group-mappings + saml-group-sync + saml-identity-provider-certificate + saml-identity-provider-issuer + saml-identity-provider-uri + saml-keystore-alias + saml-keystore-password + saml-keystore-path + send-email-on-first-login-from-new-device + send-new-sso-user-admin-email? + session-cookie-samesite + session-cookies + session-timeout + setup-token + show-database-syncing-modal + show-metabase-links + site-url + site-uuid + site-uuid-for-premium-features-token-checks + site-uuid-for-unsubscribing-url + site-uuid-for-version-info-fetching + slack-app-token + slack-cached-channels-and-usernames + slack-channels-and-usernames-last-updated + slack-files-channel + slack-token + slack-token-valid? + snowplow-available + snowplow-enabled + snowplow-url + sql-jdbc-fetch-size + ssh-heartbeat-interval-sec + ssl-certificate-public-key + startup-time-millis + token-features + token-status + toucan-name + uncached-setting + uploads-table-prefix + user-visibility + version + version-info + version-info-last-checked}) + +(defn- defsetting-lint [node setting-name docstring options-list] + (let [anon-binding (common/with-macro-meta (hooks/token-node '_) node) + ;; (defn my-setting [] ...) + getter-node (-> (list + (hooks/token-node 'defn) + setting-name + (hooks/string-node "Docstring.") + (hooks/vector-node [])) + hooks/list-node + (with-meta (meta node))) + ;; (defn my-setting! [_x] ...) + setter-node (-> (list + (hooks/token-node 'defn) + (with-meta + (hooks/token-node (symbol (str (hooks/sexpr setting-name) \!))) + (meta setting-name)) + (hooks/string-node "Docstring.") + (hooks/vector-node [(hooks/token-node '_value-or-nil)])) + hooks/list-node + (with-meta (update (meta node) :clj-kondo/ignore #(hooks/vector-node (cons :clojure-lsp/unused-public-var (:children %))))))] + + (when (nil? (second (drop-while (comp not #{[:k :export?]} first) options-list))) + (when-not (contains? ignored-implicit-export? (:value setting-name)) + (hooks/reg-finding! (assoc (meta node) + :message "Setting definition must provide an explicit value for :export? indicating whether the setting should be exported or not with serialization." + :type :metabase/defsetting-must-specify-export)))) + + {:node (-> (list + (hooks/token-node 'let) + ;; include description and the options map so they can get validated as well. + (hooks/vector-node + [anon-binding docstring + anon-binding (hooks/map-node options-list)]) + getter-node + setter-node) + hooks/list-node + (with-meta (meta node)))})) + (defn defsetting "Rewrite a [[metabase.models.defsetting]] form like @@ -11,54 +210,45 @@ as (let [_ \"Description\" - _ :type - _ :boolean] + _ {:type :boolean}] (defn my-setting \"Docstring.\" []) (defn my-setting! \"Docstring.\" [_value-or-nil])) for linting purposes." [{:keys [node]}] - (let [[setting-name & args] (rest (:children node)) - ;; (defn my-setting [] ...) - getter-node (-> (list - (hooks/token-node 'defn) - setting-name - (hooks/string-node "Docstring.") - (hooks/vector-node [])) - hooks/list-node - (with-meta (meta node))) - ;; (defn my-setting! [_x] ...) - setter-node (-> (list - (hooks/token-node 'defn) - (with-meta - (hooks/token-node (symbol (str (hooks/sexpr setting-name) \!))) - (meta setting-name)) - (hooks/string-node "Docstring.") - (hooks/vector-node [(hooks/token-node '_value-or-nil)])) - hooks/list-node - (with-meta (update (meta node) :clj-kondo/ignore #(hooks/vector-node (cons :clojure-lsp/unused-public-var (:children %))))))] - {:node (-> (list - (hooks/token-node 'let) - ;; include description and the options map so they can get validated as well. - (hooks/vector-node (vec (interleave (repeat (common/with-macro-meta (hooks/token-node '_) node)) - args))) - getter-node - setter-node) - hooks/list-node - (with-meta (meta node)))})) + (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 - (clojure.pprint/pprint - form)))})))) - - (defn- x [] + (: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.") diff --git a/src/metabase/driver/util.clj b/src/metabase/driver/util.clj index d42e55775fa..aec5c429438 100644 --- a/src/metabase/driver/util.clj +++ b/src/metabase/driver/util.clj @@ -118,11 +118,12 @@ "Consider [[metabase.driver/can-connect?]] / [[can-connect-with-details?]] to have failed if they were not able to successfully connect after this many milliseconds. By default, this is 10 seconds." :visibility :internal + :export? false :type :integer ;; for TESTS use a timeout time of 3 seconds. This is because we have some tests that check whether ;; [[driver/can-connect?]] is failing when it should, and we don't want them waiting 10 seconds to fail. ;; - ;; Don't set the timeout too low -- I've have Circle fail when the timeout was 1000ms on *one* occasion. + ;; Don't set the timeout too low -- I've had Circle fail when the timeout was 1000ms on *one* occasion. :default (if config/is-test? 3000 10000)) diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj index 4abe4efc02a..cc8f8ebed4a 100644 --- a/src/metabase/models/setting.clj +++ b/src/metabase/models/setting.clj @@ -255,6 +255,7 @@ :tag (s/maybe Symbol) ; type annotation, e.g. ^String, to be applied. Defaults to tag based on :type :sensitive? s/Bool ; is this sensitive (never show in plaintext), like a password? (default: false) :visibility Visibility ; where this setting should be visible (default: :admin) + :export? s/Bool ; should this setting be serialized? :cache? s/Bool ; should the getter always fetch this value "fresh" from the DB? (default: false) :deprecated (s/maybe s/Str) ; if non-nil, contains the Metabase version in which this setting was deprecated @@ -280,11 +281,7 @@ ;; should be used for most non-sensitive settings, and will log the value returned by its getter, which may be a ;; the default getter or a custom one. ;; (default: `:no-value`) - :audit (s/maybe (s/enum :never :no-value :raw-value :getter)) - - ;; TODO: make this required and deprecate setting/exported-setting - (s/optional-key :export?) s/Bool ; should this setting be serialized? - }) + :audit (s/maybe (s/enum :never :no-value :raw-value :getter))}) (defonce ^{:doc "Map of loaded defsettings"} registered-settings @@ -890,6 +887,7 @@ :setter (partial (default-setter-for-type setting-type) setting-name) :tag (default-tag-for-type setting-type) :visibility :admin + :export? false :sensitive? false :cache? true :feature nil @@ -1056,6 +1054,10 @@ 'Settings Managers' are non-admin users with the 'settings' permission, which gives them access to the Settings page in the Admin Panel. + ###### `:export?` + + Whether this Setting is included when producing a serializing settings export. + ###### `:getter` A custom getter fn, which takes no arguments. Overrides the default implementation. (This can in turn call functions -- GitLab