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

View file

@ -151,6 +151,19 @@ form.")
(dolist (subtest (cdr test)) (dolist (subtest (cdr test))
(should (file-test--do-local-variables-test str subtest))))))) (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 (defvar files-test-bug-18141-file
(ert-resource-file "files-bug18141.el.gz") (ert-resource-file "files-bug18141.el.gz")
"Test file for bug#18141.") "Test file for bug#18141.")