From 5bedbe6b1d5f4b801abf91b4d023d5c4e66418f0 Mon Sep 17 00:00:00 2001 From: Lars Ingebrigtsen Date: Mon, 10 May 2021 12:40:11 +0200 Subject: [PATCH] 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. --- doc/lispref/variables.texi | 8 + etc/NEWS | 7 + lisp/files.el | 314 +++++++++++++++++++------------------ test/lisp/files-tests.el | 13 ++ 4 files changed, 193 insertions(+), 149 deletions(-) diff --git a/doc/lispref/variables.texi b/doc/lispref/variables.texi index b25eea12a53..36abc316cbb 100644 --- a/doc/lispref/variables.texi +++ b/doc/lispref/variables.texi @@ -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 diff --git a/etc/NEWS b/etc/NEWS index 6efadfec6f9..c421073c96c 100644 --- a/etc/NEWS +++ b/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 diff --git a/lisp/files.el b/lisp/files.el index 93a0e07aba0..7fb13202696 100644 --- a/lisp/files.el +++ b/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. +Uses `hack-local-variables-apply' to apply the variables. -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. +See `hack-local-variables--find-variables' for the meaning of +HANDLE-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)))))) - - (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)))))))) - ;; 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))))) + (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)))))) + +(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. + +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)))))) + + (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'. diff --git a/test/lisp/files-tests.el b/test/lisp/files-tests.el index 921e2c80f3a..dc96dff6398 100644 --- a/test/lisp/files-tests.el +++ b/test/lisp/files-tests.el @@ -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.") -- 2.39.2