From: Eshel Yaron Date: Tue, 16 Jul 2024 08:09:18 +0000 (+0200) Subject: New post about buglet-bindings X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=a525135afd508eeef36c11419106357ccf62d537;p=esy-publish.git New post about buglet-bindings --- 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 index 0000000..06cdf26 --- /dev/null +++ b/source/posts/2024-07-15-on-buglet-bindings.org @@ -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:
@@Created on [{{{date}}}], last updated [{{{modification-time(%Y-%m-%d, t)}}}]@@html:
@@ + +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!