aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorF. Jason Park2023-10-06 17:34:04 -0700
committerF. Jason Park2023-10-13 07:47:00 -0700
commitd46c016fbd09cbce9ef23fe2b49d4fb5fc3b2b16 (patch)
tree7f1acf7155396ca7324853051071b13447a8b6e6
parent9120d7a32ea4906d7c9460add31d37c3ca38931e (diff)
downloademacs-d46c016fbd09cbce9ef23fe2b49d4fb5fc3b2b16.tar.gz
emacs-d46c016fbd09cbce9ef23fe2b49d4fb5fc3b2b16.zip
Sort and dedupe when loading modules in erc-open
* doc/misc/erc.texi: Add new subheading "Module Loading" under the "Modules" chapter. * lisp/erc/erc.el (erc--sort-modules): New utility function to sort and dedupe modules. (erc-modules): In `custom-set' function, factor out collation into separate utility `erc--sort-modules'. (erc-update-modules): Call `erc--update-modules' with an argument, the current value of `erc-modules'. (erc--aberrant-modules): New variable, a list of symbols whose modules ERC suspects of being incorrectly defined. (erc--warn-about-aberrant-modules): New function to print an error message and emit a warning prior to connecting when `erc--aberrant-modules' is non-nil. (erc--find-mode): Make heuristic more robust by always checking for a mode activation command rather than just a state variable. This fixes a compatibility bug, new in 5.6, affecting third-party modules that autoload module definitions instead of their corresponding mode-activation commands. (erc--update-modules): Add new positional argument `modules'. (erc--setup-buffer-hook): Add new default member, `erc--warn-about-aberrant-modules'. (erc-open): Pass sorted `erc-modules' to `erc--update-modules'. * test/lisp/erc/erc-tests.el (erc--sort-modules): New test. (erc-tests--update-modules): New fixture. (erc--update-modules): Remove and rework as three separate tests dedicated to specific contexts. The existing one had poor coverage and was difficult, if not impossible, to follow. (erc--update-modules/unknown, erc--update-modules/local, erc--update-modules/realistic): New tests. (Bug#57955)
-rw-r--r--doc/misc/erc.texi35
-rw-r--r--lisp/erc/erc.el53
-rw-r--r--test/lisp/erc/erc-tests.el163
3 files changed, 184 insertions, 67 deletions
diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index 3297d8b17f0..3bfa240cacc 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -653,6 +653,41 @@ And unlike global toggles, none of these ever mutates
653@code{erc-modules}. 653@code{erc-modules}.
654 654
655 655
656@anchor{Module Loading}
657@subheading Module Loading
658@cindex module loading
659
660ERC loads internal modules in alphabetical order and third-party
661modules as they appear in @code{erc-modules}. When defining your own
662module, take care to ensure ERC can find it. An easy way to do that
663is by mimicking the example in the doc string for
664@code{define-erc-module}. For historical reasons, ERC also falls back
665to @code{require}ing features. For example, if some module
666@code{<mymod>} in @code{erc-modules} lacks a corresponding
667@code{erc-<mymod>-mode} command, ERC will attempt to load the library
668@code{erc-<mymod>} prior to connecting. If this fails, ERC signals an
669error. Users wanting to define modules in an init files should
670@code{(provide 'erc-<my-mod>)} somewhere to placate ERC. Dynamically
671generating modules on the fly is not supported.
672
673Sometimes, packages attempt to autoload a module's definition instead
674of its minor-mode command, which breaks the link between the library
675and the module. This means that enabling the mode by invoking its
676command toggle isn't enough to load its defining library. Such
677packages should instead only supply autoload cookies featuring an
678explicit @code{autoload} form for their module's minor-mode command.
679As mentioned above, packages can also usually avoid autoload cookies
680entirely so long as their module's prefixed name matches that of its
681defining library and the latter's provided feature.
682
683Packages have also been seen to specify unnecessary top-level
684@code{eval-after-load} forms, which end up being ineffective in most
685cases. Another unfortunate practice is mutating @code{erc-modules}
686itself in an autoloaded form. Doing this tricks Customize into
687displaying the widget for @code{erc-modules} incorrectly, with
688built-in modules moved from the predefined checklist to the
689user-provided free-form area.
690
656@c PRE5_4: Document every option of every module in its own subnode 691@c PRE5_4: Document every option of every module in its own subnode
657 692
658 693
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 16651b41eef..87abe2a133b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2004,6 +2004,14 @@ buffer rather than a server buffer.")
2004 ;; each item is in the format '(old . new) 2004 ;; each item is in the format '(old . new)
2005 (delete-dups (mapcar #'erc--normalize-module-symbol mods))) 2005 (delete-dups (mapcar #'erc--normalize-module-symbol mods)))
2006 2006
2007(defun erc--sort-modules (modules)
2008 "Return a copy of MODULES, deduped and led by sorted built-ins."
2009 (let (built-in third-party)
2010 (dolist (mod modules)
2011 (setq mod (erc--normalize-module-symbol mod))
2012 (cl-pushnew mod (if (get mod 'erc--module) built-in third-party)))
2013 `(,@(sort built-in #'string-lessp) ,@(nreverse third-party))))
2014
2007(defcustom erc-modules '( autojoin button completion fill imenu irccontrols 2015(defcustom erc-modules '( autojoin button completion fill imenu irccontrols
2008 list match menu move-to-prompt netsplit 2016 list match menu move-to-prompt netsplit
2009 networks noncommands readonly ring stamp track) 2017 networks noncommands readonly ring stamp track)
@@ -2039,16 +2047,10 @@ removed from the list will be disabled."
2039 (when (symbol-value f) 2047 (when (symbol-value f)
2040 (funcall f 0)) 2048 (funcall f 0))
2041 (kill-local-variable f))))))))) 2049 (kill-local-variable f)))))))))
2042 (let (built-in third-party) 2050 ;; Calling `set-default-toplevel-value' complicates testing.
2043 (dolist (v val) 2051 (set sym (erc--sort-modules val))
2044 (setq v (erc--normalize-module-symbol v))
2045 (if (get v 'erc--module)
2046 (push v built-in)
2047 (push v third-party)))
2048 ;; Calling `set-default-toplevel-value' complicates testing
2049 (set sym (append (sort built-in #'string-lessp)
2050 (nreverse third-party))))
2051 ;; this test is for the case where erc hasn't been loaded yet 2052 ;; this test is for the case where erc hasn't been loaded yet
2053 ;; FIXME explain how this ^ can occur or remove comment.
2052 (when (fboundp 'erc-update-modules) 2054 (when (fboundp 'erc-update-modules)
2053 (unless erc--inside-mode-toggle-p 2055 (unless erc--inside-mode-toggle-p
2054 (erc-update-modules)))) 2056 (erc-update-modules))))
@@ -2112,15 +2114,29 @@ removed from the list will be disabled."
2112(defun erc-update-modules () 2114(defun erc-update-modules ()
2113 "Enable minor mode for every module in `erc-modules'. 2115 "Enable minor mode for every module in `erc-modules'.
2114Except ignore all local modules, which were introduced in ERC 5.5." 2116Except ignore all local modules, which were introduced in ERC 5.5."
2115 (erc--update-modules) 2117 (erc--update-modules erc-modules)
2116 nil) 2118 nil)
2117 2119
2120(defvar erc--aberrant-modules nil
2121 "Modules suspected of being improperly loaded.")
2122
2123(defun erc--warn-about-aberrant-modules ()
2124 (when (and erc--aberrant-modules (not erc--target))
2125 (erc-button--display-error-notice-with-keys-and-warn
2126 "The following modules exhibited strange loading behavior: "
2127 (mapconcat (lambda (s) (format "`%s'" s)) erc--aberrant-modules ", ")
2128 ". Please contact ERC with \\[erc-bug] if you believe this to be untrue."
2129 " See Info:\"(erc) Module Loading\" for more.")
2130 (setq erc--aberrant-modules nil)))
2131
2118(defun erc--find-mode (sym) 2132(defun erc--find-mode (sym)
2119 (setq sym (erc--normalize-module-symbol sym)) 2133 (setq sym (erc--normalize-module-symbol sym))
2120 (if-let* ((mode (intern-soft (concat "erc-" (symbol-name sym) "-mode"))) 2134 (if-let ((mode (intern-soft (concat "erc-" (symbol-name sym) "-mode")))
2121 ((or (boundp mode) 2135 ((and (fboundp mode)
2122 (and (fboundp mode) 2136 (autoload-do-load (symbol-function mode) mode)))
2123 (autoload-do-load (symbol-function mode) mode))))) 2137 ((or (get sym 'erc--module)
2138 (symbol-file mode)
2139 (ignore (cl-pushnew sym erc--aberrant-modules)))))
2124 mode 2140 mode
2125 (and (require (or (get sym 'erc--feature) 2141 (and (require (or (get sym 'erc--feature)
2126 (intern (concat "erc-" (symbol-name sym)))) 2142 (intern (concat "erc-" (symbol-name sym))))
@@ -2129,9 +2145,9 @@ Except ignore all local modules, which were introduced in ERC 5.5."
2129 (fboundp mode) 2145 (fboundp mode)
2130 mode))) 2146 mode)))
2131 2147
2132(defun erc--update-modules () 2148(defun erc--update-modules (modules)
2133 (let (local-modes) 2149 (let (local-modes)
2134 (dolist (module erc-modules local-modes) 2150 (dolist (module modules local-modes)
2135 (if-let ((mode (erc--find-mode module))) 2151 (if-let ((mode (erc--find-mode module)))
2136 (if (custom-variable-p mode) 2152 (if (custom-variable-p mode)
2137 (funcall mode 1) 2153 (funcall mode 1)
@@ -2158,7 +2174,7 @@ realizes it's missing some required module \"foo\", it can
2158confidently call (erc-foo-mode 1) without having to learn 2174confidently call (erc-foo-mode 1) without having to learn
2159anything about the dependency's implementation.") 2175anything about the dependency's implementation.")
2160 2176
2161(defvar erc--setup-buffer-hook nil 2177(defvar erc--setup-buffer-hook '(erc--warn-about-aberrant-modules)
2162 "Internal hook for module setup involving windows and frames.") 2178 "Internal hook for module setup involving windows and frames.")
2163 2179
2164(defvar erc--display-context nil 2180(defvar erc--display-context nil
@@ -2315,7 +2331,8 @@ Returns the buffer for the given server or channel."
2315 (setq old-point (point)) 2331 (setq old-point (point))
2316 (setq delayed-modules 2332 (setq delayed-modules
2317 (erc--merge-local-modes (let ((erc--updating-modules-p t)) 2333 (erc--merge-local-modes (let ((erc--updating-modules-p t))
2318 (erc--update-modules)) 2334 (erc--update-modules
2335 (erc--sort-modules erc-modules)))
2319 (or erc--server-reconnecting 2336 (or erc--server-reconnecting
2320 erc--target-priors))) 2337 erc--target-priors)))
2321 2338
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 64b503832f3..0b88ad9cfa9 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2293,65 +2293,130 @@
2293 (should (eq (erc--find-group 'smiley nil) 'erc)) 2293 (should (eq (erc--find-group 'smiley nil) 'erc))
2294 (should (eq (erc--find-group 'unmorse nil) 'erc))) 2294 (should (eq (erc--find-group 'unmorse nil) 'erc)))
2295 2295
2296(ert-deftest erc--update-modules () 2296(ert-deftest erc--sort-modules ()
2297 (let (calls 2297 (should (equal (erc--sort-modules '(networks foo fill bar fill stamp bar))
2298 erc-modules 2298 ;; Third-party mods appear in original order.
2299 erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook) 2299 '(fill networks stamp foo bar))))
2300
2301(defun erc-tests--update-modules (fn)
2302 (let* ((calls nil)
2303 (custom-modes nil)
2304 (on-load nil)
2305
2306 (get-calls (lambda () (prog1 (nreverse calls) (setq calls nil))))
2307
2308 (add-onload (lambda (m k v)
2309 (put (intern m) 'erc--feature k)
2310 (push (cons k (lambda () (funcall v m))) on-load)))
2300 2311
2301 ;; This `lbaz' module is unknown, so ERC looks for it via the 2312 (mk-cmd (lambda (module)
2302 ;; symbol proerty `erc--feature' and, failing that, by 2313 (let ((mode (intern (format "erc-%s-mode" module))))
2303 ;; `require'ing its "erc-" prefixed symbol. 2314 (fset mode (lambda (n) (push (cons mode n) calls))))))
2304 (should-not (intern-soft "erc-lbaz-mode")) 2315
2316 (mk-builtin (lambda (module-string)
2317 (let ((s (intern module-string)))
2318 (put s 'erc--module s))))
2319
2320 (mk-global (lambda (module)
2321 (push (intern (format "erc-%s-mode" module))
2322 custom-modes))))
2305 2323
2306 (cl-letf (((symbol-function 'require) 2324 (cl-letf (((symbol-function 'require)
2307 (lambda (s &rest _) 2325 (lambda (s &rest _)
2308 (when (eq s 'erc--lbaz-feature) 2326 ;; Simulate library being loaded, things defined.
2309 (fset (intern "erc-lbaz-mode") ; local module 2327 (when-let ((h (alist-get s on-load))) (funcall h))
2310 (lambda (n) (push (cons 'lbaz n) calls)))) 2328 (push (cons 'req s) calls)))
2311 (push s calls))) 2329
2312 2330 ;; Spoof global module detection.
2313 ;; Local modules 2331 ((symbol-function 'custom-variable-p)
2314 ((symbol-function 'erc-lbar-mode) 2332 (lambda (v) (memq v custom-modes))))
2315 (lambda (n) (push (cons 'lbar n) calls))) 2333
2316 ((get 'lbaz 'erc--feature) 'erc--lbaz-feature) 2334 (funcall fn get-calls add-onload mk-cmd mk-builtin mk-global))
2317 2335 (should-not erc--aberrant-modules)))
2318 ;; Global modules 2336
2319 ((symbol-function 'erc-gfoo-mode) 2337(ert-deftest erc--update-modules/unknown ()
2320 (lambda (n) (push (cons 'gfoo n) calls))) 2338 (erc-tests--update-modules
2321 ((get 'erc-gfoo-mode 'standard-value) 'ignore) 2339
2340 (lambda (get-calls _ mk-cmd _ mk-global)
2341
2342 (ert-info ("Baseline")
2343 (let* ((erc-modules '(foo))
2344 (obarray (obarray-make))
2345 (err (should-error (erc--update-modules erc-modules))))
2346 (should (equal (cadr err) "`foo' is not a known ERC module"))
2347 (should (equal (funcall get-calls)
2348 `((req . ,(intern-soft "erc-foo")))))))
2349
2350 ;; Module's mode command exists but lacks an associated file.
2351 (ert-info ("Bad autoload flagged as suspect")
2352 (should-not erc--aberrant-modules)
2353 (let* ((erc--aberrant-modules nil)
2354 (obarray (obarray-make))
2355 (erc-modules (list (intern "foo"))))
2356
2357 ;; Create a mode activation command.
2358 (funcall mk-cmd "foo")
2359
2360 ;; Make the mode var global.
2361 (funcall mk-global "foo")
2362
2363 ;; No local modules to return.
2364 (should-not (erc--update-modules erc-modules))
2365 (should (equal (mapcar #'prin1-to-string erc--aberrant-modules)
2366 '("foo")))
2367 ;; ERC requires the library via prefixed module name.
2368 (should (equal (mapcar #'prin1-to-string (funcall get-calls))
2369 `("(req . erc-foo)" "(erc-foo-mode . 1)"))))))))
2370
2371;; A local module (here, `lo2') lacks a mode toggle, so ERC tries to
2372;; load its defining library, first via the symbol property
2373;; `erc--feature', and then via an "erc-" prefixed symbol.
2374(ert-deftest erc--update-modules/local ()
2375 (erc-tests--update-modules
2376
2377 (lambda (get-calls add-onload mk-cmd mk-builtin mk-global)
2378
2379 (let* ((obarray (obarray-make 20))
2380 (erc-modules (mapcar #'intern '("glo" "lo1" "lo2"))))
2381
2382 ;; Create a global and a local module.
2383 (mapc mk-cmd '("glo" "lo1"))
2384 (mapc mk-builtin '("glo" "lo1"))
2385 (funcall mk-global "glo")
2386 (funcall add-onload "lo2" 'explicit-feature-lib mk-cmd)
2387
2388 ;; Returns local modules.
2389 (should (equal (mapcar #'symbol-name (erc--update-modules erc-modules))
2390 '("erc-lo2-mode" "erc-lo1-mode")))
2391
2392 ;; Requiring `erc-lo2' defines `erc-lo2-mode'.
2393 (should (equal (mapcar #'prin1-to-string (funcall get-calls))
2394 `("(erc-glo-mode . 1)"
2395 "(req . explicit-feature-lib)")))))))
2396
2397(ert-deftest erc--update-modules/realistic ()
2398 (let ((calls nil)
2399 ;; Module `pcomplete' "resolves" to `completion'.
2400 (erc-modules '(pcomplete autojoin networks)))
2401 (cl-letf (((symbol-function 'require)
2402 (lambda (s &rest _) (push (cons 'req s) calls)))
2403
2404 ;; Spoof global module detection.
2405 ((symbol-function 'custom-variable-p)
2406 (lambda (v)
2407 (memq v '(erc-autojoin-mode erc-networks-mode
2408 erc-completion-mode))))
2409 ;; Mock and spy real builtins.
2322 ((symbol-function 'erc-autojoin-mode) 2410 ((symbol-function 'erc-autojoin-mode)
2323 (lambda (n) (push (cons 'autojoin n) calls))) 2411 (lambda (n) (push (cons 'autojoin n) calls)))
2324 ((get 'erc-autojoin-mode 'standard-value) 'ignore)
2325 ((symbol-function 'erc-networks-mode) 2412 ((symbol-function 'erc-networks-mode)
2326 (lambda (n) (push (cons 'networks n) calls))) 2413 (lambda (n) (push (cons 'networks n) calls)))
2327 ((get 'erc-networks-mode 'standard-value) 'ignore)
2328 ((symbol-function 'erc-completion-mode) 2414 ((symbol-function 'erc-completion-mode)
2329 (lambda (n) (push (cons 'completion n) calls))) 2415 (lambda (n) (push (cons 'completion n) calls))))
2330 ((get 'erc-completion-mode 'standard-value) 'ignore))
2331
2332 (ert-info ("Unknown module")
2333 (setq erc-modules '(lfoo))
2334 (should-error (erc--update-modules))
2335 (should (equal (pop calls) 'erc-lfoo))
2336 (should-not calls))
2337 2416
2338 (ert-info ("Local modules") 2417 (should-not (erc--update-modules erc-modules)) ; no locals
2339 (setq erc-modules '(gfoo lbar lbaz)) 2418 (should (equal (nreverse calls)
2340 ;; Don't expose the mode here 2419 '((completion . 1) (autojoin . 1) (networks . 1)))))))
2341 (should (equal (mapcar #'symbol-name (erc--update-modules))
2342 '("erc-lbaz-mode" "erc-lbar-mode")))
2343 ;; Lbaz required because unknown.
2344 (should (equal (nreverse calls) '((gfoo . 1) erc--lbaz-feature)))
2345 (fmakunbound (intern "erc-lbaz-mode"))
2346 (unintern (intern "erc-lbaz-mode") obarray)
2347 (setq calls nil))
2348
2349 (ert-info ("Global modules") ; `pcomplete' resolved to `completion'
2350 (setq erc-modules '(pcomplete autojoin networks))
2351 (should-not (erc--update-modules)) ; no locals
2352 (should (equal (nreverse calls)
2353 '((completion . 1) (autojoin . 1) (networks . 1))))
2354 (setq calls nil)))))
2355 2420
2356(ert-deftest erc--merge-local-modes () 2421(ert-deftest erc--merge-local-modes ()
2357 (cl-letf (((get 'erc-b-mode 'erc-module) 'b) 2422 (cl-letf (((get 'erc-b-mode 'erc-module) 'b)