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:
parent
aa354dd55b
commit
5bedbe6b1d
4 changed files with 190 additions and 146 deletions
|
@ -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
|
||||
|
|
7
etc/NEWS
7
etc/NEWS
|
@ -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
|
||||
|
|
308
lisp/files.el
308
lisp/files.el
|
@ -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'.
|
||||
|
|
|
@ -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.")
|
||||
|
|
Loading…
Add table
Reference in a new issue