diff --git a/README.md b/README.md index 4bec6b8..5480232 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ next method. In vanilla Clojure multimethods, you'd have to do something like th If you're not sure whether a `next-method` exists, you can check whether it's `nil` before calling it. +Methodical exports custom [clj-kondo](https://github.com/clj-kondo/clj-kondo) configuration and hooks for `defmulti` +and `defmethod`; with the exported configuration it will even tell you if you call `next-method` with the wrong number +of args: + +![Kondo](assets/kondo.png) ## Auxiliary Methods: `:before`, `:after`, and `:around` @@ -485,12 +490,95 @@ following summarizes all component implementations that currently ship with Meth * `cached-multifn-impl` -- wraps another multifn impl and an instance of `Cache` to implement caching. +### Validation + +Methodical offers a few opportunities for validation above and beyond what normal Clojure multimethods offer. + +#### `:dispatch-value-spec` + +If you include a `:dispatch-value-spec` in the metadata of a `defmulti`, it will automatically be used to validate the +dispatch value form of any `defmethod` forms at macroexpansion time: + +```clj +(macros/defmulti mfx + {:arglists '([x y]), :dispatch-value-spec (s/cat :x keyword?, :y int?)} + (fn [x y] [x y])) + +(macros/defmethod mfx [:x 1] + [x y] + {:x x, :y y}) +;; => #'methodical.macros-test/mfx + +(macros/defmethod mfx [:x] + [x y] + {:x x, :y y}) +;; failed: Insufficient input in: [0] at: [:args-for-method-type :primary :dispatch-value :y] [:x] +``` + +This is a great way to make sure people use your multimethods correctly and catch errors right away. + ### Debugging Methodical offers debugging facilities so you can see what's going on under the hood, such as the `trace` utility: ![Trace](assets/tracing.png) +and the `describe` utility, which outputs Markdown-formatted documentation, for human-friendly viewing in tools like +[CIDER](https://github.com/clojure-emacs/cider): + +![Describe](assets/describe.png) + +Methodical multimethods also implement `datafy`: + +```clj +(clojure.datafy/datafy mf) + +=> + +{:ns 'methodical.datafy-test + :name 'methodical.datafy-test/mf + :file "methodical/datafy_test.clj" + :line 11 + :column 1 + :arglists '([x y]) + :class methodical.impl.standard.StandardMultiFn + :combo {:class methodical.impl.combo.threaded.ThreadingMethodCombination + :threading-type :thread-last} + :dispatcher {:class methodical.impl.dispatcher.multi_default.MultiDefaultDispatcher + :dispatch-fn methodical.datafy-test/dispatch-first + :default-value :default + :hierarchy #'clojure.core/global-hierarchy + :prefs {:x #{:y}}} + :method-table {:class methodical.impl.method_table.standard.StandardMethodTable + :primary {:default + {:ns 'methodical.datafy-test + :name 'methodical.datafy-test/mf-primary-method-default + :doc "Here is a docstring." + :file "methodical/datafy_test.clj" + :line 15 + :column 1 + :arglists '([next-method x y])}} + :aux {:before {[:x :default] [{:ns 'methodical.datafy-test + :name 'methodical.datafy-test/mf-before-method-x-default + :doc "Another docstring." + :file "methodical/datafy_test.clj" + :column 1 + :line 20 + :arglists '([_x y]) + :methodical/unique-key 'methodical.datafy-test}]} + :around {[:x :y] [{:ns 'methodical.datafy-test + :name 'methodical.datafy-test/mf-around-method-x-y + :file "methodical/datafy_test.clj" + :column 1 + :line 25 + :arglists '([next-method x y]) + :methodical/unique-key 'methodical.datafy-test}]}}} + :cache {:class methodical.impl.cache.watching.WatchingCache + :cache {:class methodical.impl.cache.simple.SimpleCache + :cache {}} + :refs #{#'clojure.core/global-hierarchy}}} +``` + ## Performance Methodical is built with performance in mind. Although it is written entirely in Clojure, and supports many more diff --git a/assets/describe.png b/assets/describe.png new file mode 100755 index 0000000..284d7c4 Binary files /dev/null and b/assets/describe.png differ diff --git a/assets/kondo.png b/assets/kondo.png new file mode 100755 index 0000000..e676c18 Binary files /dev/null and b/assets/kondo.png differ diff --git a/codecov.yml b/codecov.yml index 1f01e88..ac08075 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,11 +6,11 @@ coverage: status: project: default: - # Project must always have at least 80% coverage (by line) + # Project must always have at least 85% coverage (by line) target: 85% # Whole-project test coverage is allowed to drop up to 5%. (For situations where we delete code with full coverage) threshold: 5% patch: default: # Changes must have at least 80% test coverage (by line) - target: 90% + target: 80% diff --git a/resources/clj-kondo.exports/methodical/methodical/hooks/methodical/macros.clj b/resources/clj-kondo.exports/methodical/methodical/hooks/methodical/macros.clj index ab96675..22f7753 100644 --- a/resources/clj-kondo.exports/methodical/methodical/hooks/methodical/macros.clj +++ b/resources/clj-kondo.exports/methodical/methodical/hooks/methodical/macros.clj @@ -1,6 +1,7 @@ (ns hooks.methodical.macros (:refer-clojure :exclude [defmulti defmethod]) - (:require [clj-kondo.hooks-api :as hooks])) + (:require + [clj-kondo.hooks-api :as hooks])) ;;; The code below is basically simulating the spec for parsing defmethod args without using spec. It uses a basic ;;; backtracking algorithm to achieve a similar result. Parsing defmethod args is kinda complicated. @@ -116,7 +117,9 @@ [(hooks/list-node (list* (hooks/token-node 'fn) - (hooks/token-node 'next-method) + (hooks/token-node (if (contains? #{nil :around} (some-> (:qualifier parsed) hooks/sexpr)) + 'next-method + '__FN__NAME__THAT__YOU__CANNOT__REFER__TO__)) fn-tail))]))] #_(println "=>") #_(clojure.pprint/pprint (hooks/sexpr result)) diff --git a/src/methodical/impl/cache/simple.clj b/src/methodical/impl/cache/simple.clj index 05a92fb..7b5ec61 100644 --- a/src/methodical/impl/cache/simple.clj +++ b/src/methodical/impl/cache/simple.clj @@ -38,6 +38,6 @@ {:class (class this) :cache @atomm}) - describe/Describeable + describe/Describable (describe [this] - (format "It caches methods using a %s." (.getCanonicalName (class this))))) + (format "It caches methods using a [[%s]]." (.getCanonicalName (class this))))) diff --git a/src/methodical/impl/cache/watching.clj b/src/methodical/impl/cache/watching.clj index f89fc79..7768ee2 100644 --- a/src/methodical/impl/cache/watching.clj +++ b/src/methodical/impl/cache/watching.clj @@ -55,9 +55,9 @@ :cache (datafy/datafy cache) :refs refs}) - describe/Describeable + describe/Describable (describe [this] - (format "It caches methods using a %s." (.getCanonicalName (class this))))) + (format "It caches methods using a [[%s]]." (.getCanonicalName (class this))))) (defn- cache-watch-fn [cache] (let [cache-weak-ref (WeakReference. cache)] diff --git a/src/methodical/impl/combo/clojure.clj b/src/methodical/impl/combo/clojure.clj index d9955dc..08df3ed 100644 --- a/src/methodical/impl/combo/clojure.clj +++ b/src/methodical/impl/combo/clojure.clj @@ -38,6 +38,6 @@ (datafy [this] {:class (class this)}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the method combination %s." (.getCanonicalName (class this))))) + (format "It uses the method combination [[%s]]." (.getCanonicalName (class this))))) diff --git a/src/methodical/impl/combo/clos.clj b/src/methodical/impl/combo/clos.clj index 9b13e33..aa8c247 100644 --- a/src/methodical/impl/combo/clos.clj +++ b/src/methodical/impl/combo/clos.clj @@ -87,6 +87,6 @@ (datafy [this] {:class (class this)}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the method combination %s." (.getCanonicalName (class this))))) + (format "It uses the method combination [[%s]]." (.getCanonicalName (class this))))) diff --git a/src/methodical/impl/combo/operator.clj b/src/methodical/impl/combo/operator.clj index 533e095..6b5afa4 100644 --- a/src/methodical/impl/combo/operator.clj +++ b/src/methodical/impl/combo/operator.clj @@ -202,9 +202,9 @@ {:class (class this) :operator operator-name}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the method combination %s\nwith the operator %s." + (format "It uses the method combination [[%s]]\nwith the operator `%s`." (.getCanonicalName (class this)) (pr-str operator-name)))) diff --git a/src/methodical/impl/combo/threaded.clj b/src/methodical/impl/combo/threaded.clj index 9f9ba65..f92a298 100644 --- a/src/methodical/impl/combo/threaded.clj +++ b/src/methodical/impl/combo/threaded.clj @@ -105,9 +105,9 @@ {:class (class this) :threading-type threading-type}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the method combination %s\nwith the threading strategy %s." + (format "It uses the method combination [[%s]]\nwith the threading strategy `%s`." (.getCanonicalName (class this)) (pr-str threading-type)))) diff --git a/src/methodical/impl/dispatcher/everything.clj b/src/methodical/impl/dispatcher/everything.clj index 8df1f11..ca568ad 100644 --- a/src/methodical/impl/dispatcher/everything.clj +++ b/src/methodical/impl/dispatcher/everything.clj @@ -71,9 +71,9 @@ :hierarchy hierarchy-var :prefs prefs}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the dispatcher %s\nwith hierarchy %s\nand prefs %s." + (format "It uses the dispatcher [[%s]]\nwith hierarchy `%s`\nand prefs `%s`." (.getCanonicalName (class this)) (pr-str hierarchy-var) (pr-str prefs)))) diff --git a/src/methodical/impl/dispatcher/multi_default.clj b/src/methodical/impl/dispatcher/multi_default.clj index af20f55..ba814e2 100644 --- a/src/methodical/impl/dispatcher/multi_default.clj +++ b/src/methodical/impl/dispatcher/multi_default.clj @@ -198,9 +198,9 @@ :hierarchy hierarchy-var :prefs prefs}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the dispatcher %s\nwith hierarchy %s\nand prefs %s.\n\nThe default value is %s." + (format "It uses the dispatcher [[%s]]\nwith hierarchy `%s`\nand prefs `%s`.\n\nThe default value is `%s`." (.getCanonicalName (class this)) (pr-str hierarchy-var) (pr-str prefs) diff --git a/src/methodical/impl/dispatcher/standard.clj b/src/methodical/impl/dispatcher/standard.clj index c15a3c5..7c29a40 100644 --- a/src/methodical/impl/dispatcher/standard.clj +++ b/src/methodical/impl/dispatcher/standard.clj @@ -166,9 +166,9 @@ :hierarchy hierarchy-var :prefs prefs}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the dispatcher %s\nwith hierarchy %s\nand prefs %s.\n\nThe default value is %s." + (format "It uses the dispatcher [[%s]]\nwith hierarchy `%s`\nand prefs `%s`.\n\nThe default value is `%s`." (.getCanonicalName (class this)) (pr-str hierarchy-var) (pr-str prefs) diff --git a/src/methodical/impl/method_table/clojure.clj b/src/methodical/impl/method_table/clojure.clj index 798a2a1..10ef231 100644 --- a/src/methodical/impl/method_table/clojure.clj +++ b/src/methodical/impl/method_table/clojure.clj @@ -54,8 +54,8 @@ {:class (class this) :primary (method-table.common/datafy-primary-methods m)}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the method table %s. These primary methods are known:\n\n%s" + (format "It uses the method table [[%s]]. These primary methods are known:\n\n%s" (.getCanonicalName (class this)) (method-table.common/describe-primary-methods m)))) diff --git a/src/methodical/impl/method_table/common.clj b/src/methodical/impl/method_table/common.clj index 97ac48a..be04888 100644 --- a/src/methodical/impl/method_table/common.clj +++ b/src/methodical/impl/method_table/common.clj @@ -44,7 +44,7 @@ (str/join \space [(when method-ns - (format "defined in %s" (ns-name method-ns))) + (format "defined in [[%s]]" (ns-name method-ns))) (cond (and file line) (format "(%s:%d)" file line) @@ -55,9 +55,9 @@ (format "\n\nIt has the following documentation:\n\n%s" doc))]))) ([dispatch-value f] - (format "* %s, %s" (pr-str dispatch-value) (str/join - "\n " - (str/split-lines (describe-method f)))))) + (format "* `%s`, %s" (pr-str dispatch-value) (str/join + "\n " + (str/split-lines (describe-method f)))))) (defn describe-primary-methods "Helper for [[methodical.util.describe/describe]]ing the primary methods in a method table." @@ -80,7 +80,7 @@ "\n\n" (for [[qualifier dispatch-value->methods] (sort-by first qualifier->dispatch-value->methods)] (format - "%s methods:\n\n%s" + "`%s` methods:\n\n%s" (pr-str qualifier) (str/join "\n\n" diff --git a/src/methodical/impl/method_table/standard.clj b/src/methodical/impl/method_table/standard.clj index e127020..83cf4d0 100644 --- a/src/methodical/impl/method_table/standard.clj +++ b/src/methodical/impl/method_table/standard.clj @@ -95,8 +95,9 @@ :primary (method-table.common/datafy-primary-methods primary) :aux (method-table.common/datafy-aux-methods aux)}) - describe/Describeable + describe/Describable (describe [this] - (format "It uses the method table %s.%s%s" (.getCanonicalName (class this)) + (format "It uses the method table [[%s]].%s%s" + (.getCanonicalName (class this)) (method-table.common/describe-primary-methods primary) (method-table.common/describe-aux-methods aux)))) diff --git a/src/methodical/impl/multifn/cached.clj b/src/methodical/impl/multifn/cached.clj index 316fe4a..f473eda 100644 --- a/src/methodical/impl/multifn/cached.clj +++ b/src/methodical/impl/multifn/cached.clj @@ -81,7 +81,7 @@ :class (class this) :cache (datafy/datafy cache))) - describe/Describeable + describe/Describable (describe [_this] (str (describe/describe cache) \newline \newline diff --git a/src/methodical/impl/multifn/standard.clj b/src/methodical/impl/multifn/standard.clj index 4fe1f6b..f40104e 100644 --- a/src/methodical/impl/multifn/standard.clj +++ b/src/methodical/impl/multifn/standard.clj @@ -149,7 +149,7 @@ :dispatcher (datafy/datafy dispatcher) :method-table (datafy/datafy method-table)}) - describe/Describeable + describe/Describable (describe [_this] (str (describe/describe combo) \newline \newline diff --git a/src/methodical/impl/standard.clj b/src/methodical/impl/standard.clj index cc53db8..924efd8 100644 --- a/src/methodical/impl/standard.clj +++ b/src/methodical/impl/standard.clj @@ -243,13 +243,13 @@ {:class (class this)}) mta)) - describe/Describeable + describe/Describable (describe [_this] (let [{mf-name :name, mf-ns :ns, :keys [file line]} mta] (str (pr-str mf-name) (let [message (str (when mf-ns - (ns-name mf-ns)) + (format "[[%s]]" (ns-name mf-ns))) (cond (and file line) (format " (%s:%d)" file line) file (str \space file) diff --git a/src/methodical/macros.clj b/src/methodical/macros.clj index cede986..f1cd4ae 100644 --- a/src/methodical/macros.clj +++ b/src/methodical/macros.clj @@ -229,7 +229,8 @@ (let [allowed-qualifiers (i/allowed-qualifiers multifn) primary-methods-allowed? (contains? allowed-qualifiers nil) allowed-aux-qualifiers (disj allowed-qualifiers nil) - dispatch-value-spec (get (meta multifn) :dispatch-value-spec (default-dispatch-value-spec allowed-aux-qualifiers))] + dispatch-value-spec (or (some-> (get (meta multifn) :dispatch-value-spec) s/spec) + (default-dispatch-value-spec allowed-aux-qualifiers))] (s/cat :args-for-method-type (s/alt :primary (if primary-methods-allowed? (s/cat :dispatch-value dispatch-value-spec) (constantly false)) diff --git a/src/methodical/util/describe.clj b/src/methodical/util/describe.clj index 62d0e78..2ce29e5 100644 --- a/src/methodical/util/describe.clj +++ b/src/methodical/util/describe.clj @@ -2,11 +2,11 @@ (:require [clojure.datafy :as datafy] [potemkin.types :as p.types])) -(p.types/defprotocol+ Describeable +(p.types/defprotocol+ Describable (describe ^String [this] - "Return a string description of a Methodical object, such as a multifn.")) + "Return a Markdown-formatted string description of a Methodical object, such as a multifn.")) -(extend-protocol Describeable +(extend-protocol Describable nil (describe [_this] "nil") diff --git a/test/methodical/macros_test.clj b/test/methodical/macros_test.clj index c3233a1..efd1948 100644 --- a/test/methodical/macros_test.clj +++ b/test/methodical/macros_test.clj @@ -365,3 +365,22 @@ #_{:clj-kondo/ignore [:unresolved-symbol]} #'docstring-multifn-primary-method-docstring (first (u/aux-methods docstring-multifn :around :docstring)) #_{:clj-kondo/ignore [:unresolved-symbol]} #'docstring-multifn-around-method-docstring)) + + +(macros/defmulti mf-dispatch-value-spec-2 + {:arglists '([x y]), :dispatch-value-spec (s/cat :x keyword?, :y int?)} + (fn [x y] [x y])) + +(t/deftest dispatch-value-spec-test-2 + (t/testing "We should specize :dispatch-value-spec if needed" + (t/is (some? + (macroexpand + '(macros/defmethod mf-dispatch-value-spec-2 [:x 1] + [x y] + {:x x, :y y})))) + (t/is (thrown? + clojure.lang.Compiler$CompilerException + (macroexpand + '(macros/defmethod mf-dispatch-value-spec-2 [:x] + [x y] + {:x x, :y y})))))) diff --git a/test/methodical/util/describe_test.clj b/test/methodical/util/describe_test.clj index 70645bd..5bc2fd2 100644 --- a/test/methodical/util/describe_test.clj +++ b/test/methodical/util/describe_test.clj @@ -31,24 +31,24 @@ (m/prefer-method! #'mf :x :y) (def ^:private expected-description - ["mf is defined in methodical.util.describe-test (methodical/util/describe_test.clj:12)." + ["mf is defined in [[methodical.util.describe-test]] (methodical/util/describe_test.clj:12)." "" - "It caches methods using a methodical.impl.cache.watching.WatchingCache." + "It caches methods using a [[methodical.impl.cache.watching.WatchingCache]]." "" - "It uses the method combination methodical.impl.combo.threaded.ThreadingMethodCombination" - "with the threading strategy :thread-last." + "It uses the method combination [[methodical.impl.combo.threaded.ThreadingMethodCombination]]" + "with the threading strategy `:thread-last`." "" - "It uses the dispatcher methodical.impl.dispatcher.multi_default.MultiDefaultDispatcher" - "with hierarchy #'clojure.core/global-hierarchy" - "and prefs {:x #{:y}}." + "It uses the dispatcher [[methodical.impl.dispatcher.multi_default.MultiDefaultDispatcher]]" + "with hierarchy `#'clojure.core/global-hierarchy`" + "and prefs `{:x #{:y}}`." "" - "The default value is :default." + "The default value is `:default`." "" - "It uses the method table methodical.impl.method_table.standard.StandardMethodTable." + "It uses the method table [[methodical.impl.method_table.standard.StandardMethodTable]]." "" "These primary methods are known:" "" - "* :default, defined in methodical.util.describe-test (methodical/util/describe_test.clj:17) " + "* `:default`, defined in [[methodical.util.describe-test]] (methodical/util/describe_test.clj:17) " " " " It has the following documentation:" " " @@ -56,13 +56,13 @@ "" "These aux methods are known:" "" - ":around methods:" + "`:around` methods:" "" - "* [:x :y], defined in methodical.util.describe-test (methodical/util/describe_test.clj:27) " + "* `[:x :y]`, defined in [[methodical.util.describe-test]] (methodical/util/describe_test.clj:27) " "" - ":before methods:" + "`:before` methods:" "" - "* [:x :default], defined in methodical.util.describe-test (methodical/util/describe_test.clj:22) " + "* `[:x :default]`, defined in [[methodical.util.describe-test]] (methodical/util/describe_test.clj:22) " " " " It has the following documentation:" " "