]> git.eshelyaron.com Git - esy-publish.git/commitdiff
New post about buglet-bindings
authorEshel Yaron <me@eshelyaron.com>
Tue, 16 Jul 2024 08:09:18 +0000 (10:09 +0200)
committerEshel Yaron <me@eshelyaron.com>
Tue, 16 Jul 2024 08:09:18 +0000 (10:09 +0200)
source/posts/2024-07-15-on-buglet-bindings.org [new file with mode: 0644]

diff --git a/source/posts/2024-07-15-on-buglet-bindings.org b/source/posts/2024-07-15-on-buglet-bindings.org
new file mode 100644 (file)
index 0000000..06cdf26
--- /dev/null
@@ -0,0 +1,105 @@
+#+TITLE:       On Buglet-Bindings
+#+SUBTITLE:    Highlighting a pervasive Emacs Lisp bug
+#+DESCRIPTION: A post by Eshel Yaron highlighting a pervasive Emacs Lisp bug
+#+KEYWORDS:    emacs,lisp
+#+DATE:        2024-07-15
+
+@@html:<div class="metadata">@@Created on [{{{date}}}], last updated [{{{modification-time(%Y-%m-%d, t)}}}]@@html:</div>@@
+
+This post is about a class of bugs that you run into left and right in
+Emacs Lisp code, a sneaky pitfall that developers keep falling into,
+n00bs and greybeards alike: unsafe let-bindings of special variables.
+
+Say you're writing some code that lets you (or other users) input some
+text interactively.  Say you're using the minibuffer for reading that
+input.  Say you're going the extra mile to provide text completion.
+In short, say you're using ~completing-read~:
+
+#+begin_src emacs-lisp
+  (completing-read "Frobnicate: " '("Foo" "Bar" "Spam"))
+#+end_src
+
+So far so good, but when you try it out you realize that completion is
+case-sensitive by default, so type =s TAB= doesn't complete to =Spam=
+and you have to type =S TAB= with an uppercase =S= instead.  That's a
+nuance, so you venture to make completion case-insensitive.  You could
+just configure you're Emacs with ~completion-ignore-case~ set to ~t~,
+but that affects all kinds of completion, and you only want "this"
+completion to be case-insensitive.  That's when you fall head first
+into the /buglet-binding/ pitfall, and do this:
+
+#+begin_src emacs-lisp
+  ;; BAD!
+  (let ((completion-ignore-case t))
+    (completing-read "Frobnicate: " '("Foo" "Bar" "Spam")))
+#+end_src
+
+You've let-bound ~completion-ignore-case~ around the call to
+~completing-read~.  You do this because that's what you see others do.
+It makes sense.  But it's wrong.  The others you mimic are staring at
+you from the bottom of the pitfall, and you've jumped in to join them.
+
+It is wrong because this binding affects all recursive minibuffers,
+while you only wanted to influence the completion you're coding up.
+Reading input from the minibuffer puts Emacs in a recursive edit.
+Many things can happen before the minibuffer is terminated---you can
+switch away to another buffer and do some editing, get a coffee, and
+even enter more, recursive, minibuffers.  And your let-binding takes
+effect over all of that.
+
+To see this nasty effect, execute the above code, and while in the
+minibuffer type =C-h f= to invoke ~describe-function~.  Now, in the
+new minibuffer, try to complete some function name.  For example, type
+=info=, all lower case, followed by =?= to show the completions
+list---you get completion candidates that start with capitalized
+=Info=, because your let-binding of ~completion-ignore-case~ is still
+in effect.  Ouch!
+
+Instead, the correct way to implement case-insensitive minibuffer
+completion, and more generally, to change some minibuffer settings
+without affecting recursive minibuffers, is to use buffer-local
+bindings.  For example:
+
+#+begin_src emacs-lisp
+  ;; OK.
+  (minibuffer-with-setup-hook
+      (lambda () (setq-local completion-ignore-case t))
+    (completing-read "Frobnicate: " '("Foo" "Bar" "Spam")))
+#+end_src
+
+[ Actually, since Emacs version 29, ~completion-ignore-case~ in
+  particular gets a special treatment that requires some additional
+  work to handle correctly.  This is fine for other variables.  ]
+  
+While let-bindings are local to a certain piece of code, that piece
+may become boundless when recursive edits or other forms of arbitrary
+code execution are involved.  On the other hand, buffer-local variable
+values only affect the one minibuffer in which you set them.
+Recursive minibuffers are different buffers, so they have their own
+buffer-local values and thus maintain their usual behavior.
+
+A /bug-let/, or a /buglet-binding/, is a coding error in which a
+let-binding has unintended influences on some code in the scope of
+that let-binding, like in the example we saw earlier.  Let-binding
+special variables (those defined with ~defvar~, ~defcustom~, etc.)
+around functions that read input in the minibuffer can often yield
+buglet-bindings.  Variables that are often buglet-bound include
+~enable-recursive-minibuffers~, ~completion-extra-properties~,
+~completion-ignore-case~, ~minibuffer-allow-text-properties~,
+~completion-ignored-extensions~ and ~minibuffer-completing-file-name~.
+But it's not all minibuffers, buglet-bindings are a more general class
+of bugs that can occur even when no minibuffers are in play.  When you
+let-bind special variables, you need to ensure that everything in the
+binding's scope cooperates with it correctly.  That's easy to do for
+your own special variables that only your code uses: if all other code
+is indifferent to the value of your variable, then all you need to
+check is that your code is not called recursively, or that it's
+prepared for being called recursively with the let-binding in effect.
+For special variables defined elsewhere, keep the scope of
+let-bindings as small as possible and check that all functions in
+scope are known and that the let-binding doesn't change their behavior
+in unindented ways.  If you're invoking a user-supplied function in
+the let-binding scope, document that clearly so users can prepare
+their code accordingly.
+
+Other than that, let-bind away, and stay safe!