]> git.eshelyaron.com Git - emacs.git/commitdiff
Always heed the `lexical-binding' local variable
authorLars Ingebrigtsen <larsi@gnus.org>
Mon, 10 May 2021 10:40:11 +0000 (12:40 +0200)
committerLars Ingebrigtsen <larsi@gnus.org>
Mon, 10 May 2021 10:40:11 +0000 (12:40 +0200)
* 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
etc/NEWS
lisp/files.el
test/lisp/files-tests.el

index b25eea12a53cc426dd84db1bcdb868d2939e7be2..36abc316cbb21d85c2f2ec19d40629daee114572 100644 (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
index 6efadfec6f9e489f4be8f4b776f3103cf0b36fe3..c421073c96c62c4331d1df7afaa3092e178aa464 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2484,6 +2484,13 @@ This is to keep the same behavior as Eshell.
 \f
 * 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
index 93a0e07aba0b774ea8bc3d350c25f4e6dd4c42e6..7fb13202696933365d432f6ea5f101e4ddbefa25 100644 (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.
+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'.
index 921e2c80f3a9ccdf90b95b3ca6e4ec30922f7f00..dc96dff63987942b9ccbcc4dcace445a85b0bcfe 100644 (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.")