Subject: Re: bug#62947: 30.0.50; ERC 5.6: Improve partitioning of outgoing messages

From: "J.P." <[email-masked]>
Date: Wed, 06 Dec 2023 23:19:18 -0800

This fix introduced an innocuous though technically breaking change in
the definition of `erc-input' (the struct). Basically, it added a new
slot, `refoldp', to allow users access to something resembling the
pre-5.6 behavior, where protocol-oriented message splitting would take
place after `erc-pre-send-functions' ran. That is, setting the slot to t
is meant to buy you some of that old functionality in the form of a
second split. (The new behavior of only splitting beforehand favors
interactive client users over bot/module authors.)

However, reflecting back on this, I think it wouldn't kill us to account
for the unlikely possibility of someone "subclassing" `erc-input' for
use outside this hook. In most cases, I believe simply recompiling their
dependent libraries would solve the issue, but why chance it if we don't
have to? Hence the attached change, which removes the slot but "spoofs"
its would-be accessor, `erc-input-refoldp', while the hook runs. I
originally went with a name that differed from that of the would-be
accessor, but this feature has been on HEAD for a while, so keeping it
seemed the less disruptive option.

>From 6d6bfee8180c9ba37545c18aed46ddc8dc43732f Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Fri, 1 Dec 2023 13:54:12 -0800
Subject: [PATCH 06/11] [5.6] Make erc-input's refoldp slot conditionally
 available

* etc/ERC-NEWS: Fix entry regarding `erc-input-refoldp'.
* lisp/erc/erc-common.el (erc-input): Remove `refoldp' slot to reduce
churn in the unlikely event that third-party code subclasses
`erc-input' for use outside of `erc-pre-send-functions'.
(erc--input-split) Add `refoldp' slot here instead.
* lisp/erc/erc.el (erc-pre-send-functions): Amend doc string to stress
that `refoldp' is not a real slot.
(erc--input-ensure-hook-context, erc-input-refoldp): New function, an
impostor accessor for the nonexistent `refoldp' slot of `erc-input',
and a helper function for asserting a valid context at runtime.
(erc--run-send-hooks): Don't copy over `refoldp' from the
`erc--input-lines' object to the working `erc-insert' object.  Check
the insertion context's `erc--input-split' object instead of the
hook's `erc-insert' object when deciding whether to resplit.
* test/lisp/erc/erc-tests.el: Adjust test environment to satisfy
assertion.  (Bug#62947)
---
 etc/ERC-NEWS               |  5 +++--
 lisp/erc/erc-common.el     |  3 ++-
 lisp/erc/erc.el            | 31 +++++++++++++++++++++++++------
 test/lisp/erc/erc-tests.el |  7 ++++---
 4 files changed, 34 insertions(+), 12 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index f6a9d934e80..540d9e98751 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -460,8 +460,9 @@ ERC now adjusts input lines to fall within allowed length limits
 before showing hook members the result.  For compatibility,
 third-party code can request that the final input be adjusted again
 prior to being sent.  To facilitate this, the 'erc-input' object
-shared among hook members has gained a new 'refoldp' slot, making this
-a breaking change, if only in theory.  See doc string for details.
+shared among hook members has gained a "phony" 'refoldp' slot that's
+only accessible from 'erc-pre-send-functions'.  See doc string for
+details.
 
 *** ERC's prompt survives the insertion of user input and messages.
 Previously, ERC's prompt and its input marker disappeared while
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index e9e494720e5..cb820c812b3 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -49,7 +49,7 @@ erc-session-server
 (declare-function widget-type "wid-edit" (widget))
 
 (cl-defstruct erc-input
-  string insertp sendp refoldp)
+  string insertp sendp)
 
 (cl-defstruct (erc--input-split (:include erc-input
                                           (string :read-only)
@@ -57,6 +57,7 @@ erc-input
                                           (sendp (with-suppressed-warnings
                                                      ((obsolete erc-send-this))
                                                    erc-send-this))))
+  (refoldp nil :type boolean)
   (lines nil :type (list-of string))
   (abortp nil :type (list-of symbol))
   (cmdp nil :type boolean))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 9084b7ee042..bb05f17bee6 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1197,13 +1197,18 @@ erc-pre-send-functions
   `string': The current input string.
   `insertp': Whether the string should be inserted into the erc buffer.
   `sendp': Whether the string should be sent to the irc server.
+
+And one \"phony\" slot only accessible by hook members at runtime:
+
   `refoldp': Whether the string should be re-split per protocol limits.
 
 This hook runs after protocol line splitting has taken place, so
 the value of `string' is originally \"pre-filled\".  If you need
-ERC to refill the entire payload before sending it, set the
-`refoldp' slot to a non-nil value.  Preformatted text and encoded
-subprotocols should probably be handled manually."
+ERC to refill the entire payload before sending it, set the phony
+`refoldp' slot to a non-nil value.  Note that this refilling is
+only a convenience, and modules with special needs, such as
+preserving \"preformatted\" text or encoding for subprotocol
+\"tunneling\", should handle splitting manually."
   :group 'erc
   :type 'hook
   :version "27.1")
@@ -7424,6 +7429,22 @@ erc--split-lines
     (setf (erc--input-split-lines state)
           (mapcan #'erc--split-line (erc--input-split-lines state)))))
 
+(defun erc--input-ensure-hook-context ()
+  (unless (erc--input-split-p erc--current-line-input-split)
+    (error "Invoked outside of `erc-pre-send-functions'")))
+
+(defun erc-input-refoldp (_)
+  "Impersonate accessor for phony `erc-input' `refoldp' slot.
+This function only works inside `erc-pre-send-functions' members."
+  (declare (gv-setter (lambda (v)
+                        `(progn
+                           (erc--input-ensure-hook-context)
+                           (setf (erc--input-split-refoldp
+                                  erc--current-line-input-split)
+                                 ,v)))))
+  (erc--input-ensure-hook-context)
+  (erc--input-split-refoldp erc--current-line-input-split))
+
 (defun erc--run-send-hooks (lines-obj)
   "Run send-related hooks that operate on the entire prompt input.
 Sequester some of the back and forth involved in honoring old
@@ -7443,8 +7464,6 @@ erc--run-send-hooks
                       (run-hook-with-args 'erc-send-pre-hook str)
                       (make-erc-input :string str
                                       :insertp erc-insert-this
-                                      :refoldp (erc--input-split-refoldp
-                                                lines-obj)
                                       :sendp erc-send-this))))
         (run-hook-with-args 'erc-pre-send-functions state)
         (setf (erc--input-split-sendp lines-obj) (erc-input-sendp state)
@@ -7456,7 +7475,7 @@ erc--run-send-hooks
                 (if erc--allow-empty-outgoing-lines-p
                     lines
                   (cl-nsubst " " "" lines :test #'equal))))
-        (when (erc-input-refoldp state)
+        (when (erc--input-split-refoldp lines-obj)
           (erc--split-lines lines-obj)))))
   (when (and (erc--input-split-cmdp lines-obj)
              (cdr (erc--input-split-lines lines-obj)))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 2c70f100c3f..28c1e403e41 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -2256,10 +2256,11 @@ erc--run-send-hooks
               erc-pre-send-functions
               (lambda (o) (setf (erc-input-string o) "foo bar baz"
                                 (erc-input-refoldp o) t)))
-        (let ((erc-split-line-length 8))
+        (let* ((split (make-erc--input-split :string "foo" :lines '("foo")))
+               (erc--current-line-input-split split)
+               (erc-split-line-length 8))
           (should
-           (pcase (erc--run-send-hooks (make-erc--input-split
-                                        :string "foo" :lines '("foo")))
+           (pcase (erc--run-send-hooks split)
              ((cl-struct erc--input-split
                          (string "foo") (sendp 't) (insertp 't)
                          (lines '("foo bar " "baz")) (cmdp 'nil))
-- 
2.42.0