Always heed the `lexical-binding' local variable

* doc/lispref/variables.texi (File Local Variables): Document
`permanently-enabled-local-variables'.

* lisp/files.el (enable-local-variables): Mention the new variable.
(set-auto-mode): Always call `hack-local-variables'.
(hack-local-variables): Factor out the variable gathering into its
own function, and respect the new variable (bug#47843).
(hack-local-variables--find-variables): Factored out from
`hack-local-variables'.
(permanently-enabled-local-variables): New variable.
This commit is contained in:
Lars Ingebrigtsen 2021-05-10 12:40:11 +02:00
parent aa354dd55b
commit 5bedbe6b1d
4 changed files with 190 additions and 146 deletions

View file

@ -1885,6 +1885,14 @@ any form of file-local variable. For examples of why you might want
to use this, @pxref{Auto Major Mode}.
@end defvar
@defvar permanently-enabled-local-variables
Some local variable settings will, by default, be heeded even if
@code{enable-local-variables} is @code{nil}. By default, this is only
the case for the @code{lexical-binding} local variable setting, but
this can be controlled by using this variable, which is a list of
symbols.
@end defvar
@defun hack-local-variables &optional handle-mode
This function parses, and binds or evaluates as appropriate, any local
variables specified by the contents of the current buffer. The variable

View file

@ -2484,6 +2484,13 @@ This is to keep the same behavior as Eshell.
* Incompatible Lisp Changes in Emacs 28.1
+++
** The 'lexical-binding' local variable is always enabled.
Previously, if 'enable-local-variables' was nil, a 'lexical-binding'
local variable would not be heeded. This has now changed, and a file
with a 'lexical-binding' cookie is always heeded. To revert to the
old behavior, set 'permanently-enabled-local-variables' to nil.
+++
** 'completing-read-default' sets completion variables buffer-locally.
'minibuffer-completion-table' and related variables are now set buffer-locally

View file

@ -577,7 +577,9 @@ a -*- line.
The command \\[normal-mode], when used interactively,
always obeys file local variable specifications and the -*- line,
and ignores this variable."
and ignores this variable.
Also see the `permanently-enabled-local-variables' variable."
:risky t
:type '(choice (const :tag "Query Unsafe" t)
(const :tag "Safe Only" :safe)
@ -3198,13 +3200,8 @@ we don't actually set it to the same mode the buffer already has."
(or (set-auto-mode-0 mode keep-mode-if-same)
;; continuing would call minor modes again, toggling them off
(throw 'nop nil))))))
;; hack-local-variables checks local-enable-local-variables etc, but
;; we might as well be explicit here for the sake of clarity.
(and (not done)
enable-local-variables
local-enable-local-variables
try-locals
(setq mode (hack-local-variables t))
(setq mode (hack-local-variables t (not try-locals)))
(not (memq mode modes)) ; already tried and failed
(if (not (functionp mode))
(message "Ignoring unknown mode `%s'" mode)
@ -3503,6 +3500,10 @@ function is allowed to change the contents of this alist.
This hook is called only if there is at least one file-local
variable to set.")
(defvar permanently-enabled-local-variables '(lexical-binding)
"A list of local variables that are always enabled.
This overrides any `enable-local-variables' setting.")
(defun hack-local-variables-confirm (all-vars unsafe-vars risky-vars dir-name)
"Get confirmation before setting up local variable values.
ALL-VARS is the list of all variables to be set up.
@ -3716,25 +3717,26 @@ DIR-NAME is the name of the associated directory. Otherwise it is nil."
;; TODO? Warn once per file rather than once per session?
(defvar hack-local-variables--warned-lexical nil)
(defun hack-local-variables (&optional handle-mode)
(defun hack-local-variables (&optional handle-mode inhibit-locals)
"Parse and put into effect this buffer's local variables spec.
For buffers visiting files, also puts into effect directory-local
variables.
Uses `hack-local-variables-apply' to apply the variables.
If HANDLE-MODE is nil, we apply all the specified local
variables. If HANDLE-MODE is neither nil nor t, we do the same,
except that any settings of `mode' are ignored.
See `hack-local-variables--find-variables' for the meaning of
HANDLE-MODE.
If HANDLE-MODE is t, all we do is check whether a \"mode:\"
is specified, and return the corresponding mode symbol, or nil.
In this case, we try to ignore minor-modes, and return only a
major-mode.
If `enable-local-variables' or `local-enable-local-variables' is nil,
this function does nothing. If `inhibit-local-variables-regexps'
If `enable-local-variables' or `local-enable-local-variables' is
nil, or INHIBIT-LOCALS is non-nil, this function disregards all
normal local variables. If `inhibit-local-variables-regexps'
applies to the file in question, the file is not scanned for
local variables, but directory-local variables may still be applied."
local variables, but directory-local variables may still be
applied.
Variables present in `permanently-enabled-local-variables' will
still be evaluated, even if local variables are otherwise
inhibited."
;; We don't let inhibit-local-variables-p influence the value of
;; enable-local-variables, because then it would affect dir-local
;; variables. We don't want to search eg tar files for file local
@ -3742,9 +3744,18 @@ local variables, but directory-local variables may still be applied."
;; to them. The real meaning of inhibit-local-variables-p is "do
;; not scan this file for local variables".
(let ((enable-local-variables
(and local-enable-local-variables enable-local-variables))
result)
(unless (eq handle-mode t)
(and (not inhibit-locals)
local-enable-local-variables enable-local-variables)))
(if (eq handle-mode t)
;; We're looking just for the major mode setting.
(and enable-local-variables
(not (inhibit-local-variables-p))
;; If HANDLE-MODE is t, and the prop line specifies a
;; mode, then we're done, and have no need to scan further.
(or (hack-local-variables-prop-line t)
;; Look for the mode elsewhere in the buffer.
(hack-local-variables--find-variables t)))
;; Normal handling of local variables.
(setq file-local-variables-alist nil)
(when (and (file-remote-p default-directory)
(fboundp 'hack-connection-local-variables)
@ -3755,133 +3766,138 @@ local variables, but directory-local variables may still be applied."
(connection-local-criteria-for-default-directory))))
(with-demoted-errors "Directory-local variables error: %s"
;; Note this is a no-op if enable-local-variables is nil.
(hack-dir-local-variables)))
;; This entire function is basically a no-op if enable-local-variables
;; is nil. All it does is set file-local-variables-alist to nil.
(when enable-local-variables
;; This part used to ignore enable-local-variables when handle-mode
;; was t. That was inappropriate, eg consider the
;; (artificial) example of:
;; (setq local-enable-local-variables nil)
;; Open a file foo.txt that contains "mode: sh".
;; It correctly opens in text-mode.
;; M-x set-visited-file name foo.c, and it incorrectly stays in text-mode.
(unless (or (inhibit-local-variables-p)
;; If HANDLE-MODE is t, and the prop line specifies a
;; mode, then we're done, and have no need to scan further.
(and (setq result (hack-local-variables-prop-line
handle-mode))
(eq handle-mode t)))
;; Look for "Local variables:" line in last page.
(save-excursion
(goto-char (point-max))
(search-backward "\n\^L" (max (- (point-max) 3000) (point-min))
'move)
(when (let ((case-fold-search t))
(search-forward "Local Variables:" nil t))
(skip-chars-forward " \t")
;; suffix is what comes after "local variables:" in its line.
;; prefix is what comes before "local variables:" in its line.
(let ((suffix
(concat
(regexp-quote (buffer-substring (point)
(line-end-position)))
"$"))
(prefix
(concat "^" (regexp-quote
(buffer-substring (line-beginning-position)
(match-beginning 0))))))
(hack-dir-local-variables))
(let ((result (append (hack-local-variables-prop-line)
(hack-local-variables--find-variables))))
(if (and enable-local-variables
(not (inhibit-local-variables-p)))
(progn
;; Set the variables.
(hack-local-variables-filter result nil)
(hack-local-variables-apply))
;; Handle `lexical-binding' and other special local
;; variables.
(dolist (variable permanently-enabled-local-variables)
(when-let ((elem (assq variable result)))
(push elem file-local-variables-alist)))
(hack-local-variables-apply))))))
(forward-line 1)
(let ((startpos (point))
endpos
(thisbuf (current-buffer)))
(save-excursion
(unless (let ((case-fold-search t))
(re-search-forward
(concat prefix "[ \t]*End:[ \t]*" suffix)
nil t))
;; This used to be an error, but really all it means is
;; that this may simply not be a local-variables section,
;; so just ignore it.
(message "Local variables list is not properly terminated"))
(beginning-of-line)
(setq endpos (point)))
(defun hack-local-variables--find-variables (&optional handle-mode)
"Return all local variables in the ucrrent buffer.
If HANDLE-MODE is nil, we gather all the specified local
variables. If HANDLE-MODE is neither nil nor t, we do the same,
except that any settings of `mode' are ignored.
(with-temp-buffer
(insert-buffer-substring thisbuf startpos endpos)
(goto-char (point-min))
(subst-char-in-region (point) (point-max) ?\^m ?\n)
(while (not (eobp))
;; Discard the prefix.
(if (looking-at prefix)
(delete-region (point) (match-end 0))
(error "Local variables entry is missing the prefix"))
(end-of-line)
;; Discard the suffix.
(if (looking-back suffix (line-beginning-position))
(delete-region (match-beginning 0) (point))
(error "Local variables entry is missing the suffix"))
(forward-line 1))
(goto-char (point-min))
If HANDLE-MODE is t, all we do is check whether a \"mode:\"
is specified, and return the corresponding mode symbol, or nil.
In this case, we try to ignore minor-modes, and return only a
major-mode."
(let ((result nil))
;; Look for "Local variables:" line in last page.
(save-excursion
(goto-char (point-max))
(search-backward "\n\^L" (max (- (point-max) 3000) (point-min))
'move)
(when (let ((case-fold-search t))
(search-forward "Local Variables:" nil t))
(skip-chars-forward " \t")
;; suffix is what comes after "local variables:" in its line.
;; prefix is what comes before "local variables:" in its line.
(let ((suffix
(concat
(regexp-quote (buffer-substring (point)
(line-end-position)))
"$"))
(prefix
(concat "^" (regexp-quote
(buffer-substring (line-beginning-position)
(match-beginning 0))))))
(while (not (or (eobp)
(and (eq handle-mode t) result)))
;; Find the variable name;
(unless (looking-at hack-local-variable-regexp)
(error "Malformed local variable line: %S"
(buffer-substring-no-properties
(point) (line-end-position))))
(goto-char (match-end 1))
(let* ((str (match-string 1))
(var (intern str))
val val2)
(and (equal (downcase (symbol-name var)) "mode")
(setq var 'mode))
;; Read the variable value.
(skip-chars-forward "^:")
(forward-char 1)
;; As a defensive measure, we do not allow
;; circular data in the file-local data.
(let ((read-circle nil))
(setq val (read (current-buffer))))
(if (eq handle-mode t)
(and (eq var 'mode)
;; Specifying minor-modes via mode: is
;; deprecated, but try to reject them anyway.
(not (string-match
"-minor\\'"
(setq val2 (downcase (symbol-name val)))))
(setq result (intern (concat val2 "-mode"))))
(cond ((eq var 'coding))
((eq var 'lexical-binding)
(unless hack-local-variables--warned-lexical
(setq hack-local-variables--warned-lexical t)
(display-warning
'files
(format-message
"%s: `lexical-binding' at end of file unreliable"
(file-name-nondirectory
;; We are called from
;; 'with-temp-buffer', so we need
;; to use 'thisbuf's name in the
;; warning message.
(or (buffer-file-name thisbuf) ""))))))
((and (eq var 'mode) handle-mode))
(t
(ignore-errors
(push (cons (if (eq var 'eval)
'eval
(indirect-variable var))
val)
result))))))
(forward-line 1))))))))
;; Now we've read all the local variables.
;; If HANDLE-MODE is t, return whether the mode was specified.
(if (eq handle-mode t) result
;; Otherwise, set the variables.
(hack-local-variables-filter result nil)
(hack-local-variables-apply)))))
(forward-line 1)
(let ((startpos (point))
endpos
(thisbuf (current-buffer)))
(save-excursion
(unless (let ((case-fold-search t))
(re-search-forward
(concat prefix "[ \t]*End:[ \t]*" suffix)
nil t))
;; This used to be an error, but really all it means is
;; that this may simply not be a local-variables section,
;; so just ignore it.
(message "Local variables list is not properly terminated"))
(beginning-of-line)
(setq endpos (point)))
(with-temp-buffer
(insert-buffer-substring thisbuf startpos endpos)
(goto-char (point-min))
(subst-char-in-region (point) (point-max) ?\^m ?\n)
(while (not (eobp))
;; Discard the prefix.
(if (looking-at prefix)
(delete-region (point) (match-end 0))
(error "Local variables entry is missing the prefix"))
(end-of-line)
;; Discard the suffix.
(if (looking-back suffix (line-beginning-position))
(delete-region (match-beginning 0) (point))
(error "Local variables entry is missing the suffix"))
(forward-line 1))
(goto-char (point-min))
(while (not (or (eobp)
(and (eq handle-mode t) result)))
;; Find the variable name;
(unless (looking-at hack-local-variable-regexp)
(error "Malformed local variable line: %S"
(buffer-substring-no-properties
(point) (line-end-position))))
(goto-char (match-end 1))
(let* ((str (match-string 1))
(var (intern str))
val val2)
(and (equal (downcase (symbol-name var)) "mode")
(setq var 'mode))
;; Read the variable value.
(skip-chars-forward "^:")
(forward-char 1)
;; As a defensive measure, we do not allow
;; circular data in the file-local data.
(let ((read-circle nil))
(setq val (read (current-buffer))))
(if (eq handle-mode t)
(and (eq var 'mode)
;; Specifying minor-modes via mode: is
;; deprecated, but try to reject them anyway.
(not (string-match
"-minor\\'"
(setq val2 (downcase (symbol-name val)))))
(setq result (intern (concat val2 "-mode"))))
(cond ((eq var 'coding))
((eq var 'lexical-binding)
(unless hack-local-variables--warned-lexical
(setq hack-local-variables--warned-lexical t)
(display-warning
'files
(format-message
"%s: `lexical-binding' at end of file unreliable"
(file-name-nondirectory
;; We are called from
;; 'with-temp-buffer', so we need
;; to use 'thisbuf's name in the
;; warning message.
(or (buffer-file-name thisbuf) ""))))))
((and (eq var 'mode) handle-mode))
(t
(ignore-errors
(push (cons (if (eq var 'eval)
'eval
(indirect-variable var))
val)
result))))))
(forward-line 1)))))))
result))
(defun hack-local-variables-apply ()
"Apply the elements of `file-local-variables-alist'.

View file

@ -151,6 +151,19 @@ form.")
(dolist (subtest (cdr test))
(should (file-test--do-local-variables-test str subtest)))))))
(ert-deftest files-tests-permanent-local-variables ()
(let ((enable-local-variables nil))
(with-temp-buffer
(insert ";;; test-test.el --- tests -*- lexical-binding: t; -*-\n\n")
(hack-local-variables)
(should (eq lexical-binding t))))
(let ((enable-local-variables nil)
(permanently-enabled-local-variables nil))
(with-temp-buffer
(insert ";;; test-test.el --- tests -*- lexical-binding: t; -*-\n\n")
(hack-local-variables)
(should (eq lexical-binding nil)))))
(defvar files-test-bug-18141-file
(ert-resource-file "files-bug18141.el.gz")
"Test file for bug#18141.")