Warn when loading .el files without lexical-binding declaration
authorMattias Engdegård <mattiase@acm.org>
Fri, 3 May 2024 15:58:44 +0000 (17:58 +0200)
committerMattias Engdegård <mattiase@acm.org>
Fri, 3 May 2024 17:27:26 +0000 (19:27 +0200)
This corresponds to the byte-compiler warning for the same issue,
here emitted for files that aren't compiled but loaded from source.
It should make the planned change to enable lexical binding by default
in Emacs 31 go smoother.

* src/lread.c (lexical_cookie_t): New type.
(lisp_file_lexically_bound_p): Renamed to...
(lisp_file_lexical_cookie): ...this, with the return value retyped.
* src/lread.c (warn_missing_cookie): New.
(Fload): Warn when loading source file and cookie missing.
(Feval_buffer): Add LOADING arg; warn when set and cookie missing.
* lisp/international/mule.el (load-with-code-conversion):
* lisp/startup.el (command-line--load-script):
Call eval-buffer with LOADING arg set.
* etc/NEWS: Announce.

etc/NEWS
lisp/international/mule.el
lisp/startup.el
src/lread.c

index e2588afeb4003195c5934688da034fb8e473d15f..d4177d759f3bac223689d5db63be543ffc38d359 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2422,6 +2422,13 @@ The warning name is 'docstrings-control-chars'.
 *** The warning about wide docstrings can now be disabled separately.
 Its warning name is 'docstrings-wide'.
 
+---
+** Warn about missing 'lexical-binding' directive when loading .el files.
+Emacs now emits a run-time warning if an Elisp source file being loaded
+lacks the '-*- lexical-binding: ... -*-' cookie on the first line.
+See the lexical-binding compiler warning described above for how to make
+the warning go away.
+
 ---
 ** New user option 'native-comp-async-warnings-errors-kind'.
 It allows control of what kinds of warnings and errors from asynchronous
index a17221e6d2165655889f06e4ba0a88221c675ec4..8875c4f06afc55c61a26475e6286a5f69671de9a 100644 (file)
@@ -367,7 +367,7 @@ Return t if file exists."
                 (eval-buffer buffer nil
                             ;; This is compatible with what `load' does.
                              (if dump-mode file fullname)
-                            nil t))))
+                            nil t t))))
        (let (kill-buffer-hook kill-buffer-query-functions)
          (kill-buffer buffer)))
       (do-after-load-evaluation fullname)
index 357a4154e4c870ab5cae4b99dd19ba6b09ffa71d..f2532f5254e3f0e4027aa9080c09cf790b786e4a 100644 (file)
@@ -2935,7 +2935,7 @@ nil default-directory" name)
        ;; buffer is empty.
        (when (looking-at "#!")
          (delete-line))
-       (eval-buffer buffer nil file nil t)))))
+       (eval-buffer buffer nil file nil t t)))))
 
 (defun command-line--eval-script (file)
   (load-with-code-conversion
index 983fdb883ffbbcdad20d11d899ea4dc0e26e7a44..f9a1a8562cb8a7f344a2f1df157d82561e0a945a 100644 (file)
@@ -1053,13 +1053,19 @@ DEFUN ("get-file-char", Fget_file_char, Sget_file_char, 0, 0, 0,
 
 \f
 
-/* Return true if the lisp code read using READCHARFUN defines a non-nil
-   `lexical-binding' file variable.  After returning, the stream is
-   positioned following the first line, if it is a comment or #! line,
-   otherwise nothing is read.  */
-
-static bool
-lisp_file_lexically_bound_p (Lisp_Object readcharfun)
+typedef enum {
+  Cookie_None,                 /* no cookie */
+  Cookie_Dyn,                  /* explicit dynamic binding */
+  Cookie_Lex                   /* explicit lexical binding */
+} lexical_cookie_t;
+
+/* Determine if the lisp code read using READCHARFUN defines a
+   `lexical-binding' file variable return its value.
+   After returning, the stream is positioned following the first line,
+   if it is a comment or #! line, otherwise nothing is read.  */
+
+static lexical_cookie_t
+lisp_file_lexical_cookie (Lisp_Object readcharfun)
 {
   int ch = READCHAR;
 
@@ -1070,7 +1076,7 @@ lisp_file_lexically_bound_p (Lisp_Object readcharfun)
         {
           UNREAD (ch);
           UNREAD ('#');
-          return 0;
+          return Cookie_None;
         }
       while (ch != '\n' && ch != EOF)
         ch = READCHAR;
@@ -1083,12 +1089,12 @@ lisp_file_lexically_bound_p (Lisp_Object readcharfun)
     /* The first line isn't a comment, just give up.  */
     {
       UNREAD (ch);
-      return 0;
+      return Cookie_None;
     }
   else
     /* Look for an appropriate file-variable in the first line.  */
     {
-      bool rv = 0;
+      lexical_cookie_t rv = Cookie_None;
       enum {
        NOMINAL, AFTER_FIRST_DASH, AFTER_ASTERIX
       } beg_end_state = NOMINAL;
@@ -1170,7 +1176,7 @@ lisp_file_lexically_bound_p (Lisp_Object readcharfun)
              if (strcmp (var, "lexical-binding") == 0)
                /* This is it...  */
                {
-                 rv = (strcmp (val, "nil") != 0);
+                 rv = strcmp (val, "nil") != 0 ? Cookie_Lex : Cookie_Dyn;
                  break;
                }
            }
@@ -1336,6 +1342,17 @@ close_file_unwind_android_fd (void *ptr)
 
 #endif
 
+static void
+warn_missing_cookie (Lisp_Object file)
+{
+  Lisp_Object msg = CALLN (Fformat,
+                          build_string ("File %s lacks `lexical-binding'"
+                                        " directive on its first line"),
+                          file);
+  Vdelayed_warnings_list = Fcons (list2 (Qlexical_binding, msg),
+                                 Vdelayed_warnings_list);
+}
+
 DEFUN ("load", Fload, Sload, 1, 5, 0,
        doc: /* Execute a file of Lisp code named FILE.
 First try FILE with `.elc' appended, then try with `.el', then try
@@ -1785,7 +1802,10 @@ Return t if the file exists and loads successfully.  */)
     }
   else
     {
-      if (lisp_file_lexically_bound_p (Qget_file_char))
+      lexical_cookie_t lc = lisp_file_lexical_cookie (Qget_file_char);
+      if (lc == Cookie_None && !compiled)
+       warn_missing_cookie (file);
+      if (lc == Cookie_Lex)
         Fset (Qlexical_binding, Qt);
 
       if (! version || version >= 22)
@@ -2618,7 +2638,7 @@ readevalloop (Lisp_Object readcharfun,
   unbind_to (count, Qnil);
 }
 
-DEFUN ("eval-buffer", Feval_buffer, Seval_buffer, 0, 5, "",
+DEFUN ("eval-buffer", Feval_buffer, Seval_buffer, 0,6, "",
        doc: /* Execute the accessible portion of current buffer as Lisp code.
 You can use \\[narrow-to-region] to limit the part of buffer to be evaluated.
 When called from a Lisp program (i.e., not interactively), this
@@ -2635,6 +2655,8 @@ UNIBYTE, if non-nil, specifies `load-convert-to-unibyte' for this
 DO-ALLOW-PRINT, if non-nil, specifies that output functions in the
  evaluated code should work normally even if PRINTFLAG is nil, in
  which case the output is displayed in the echo area.
+LOADING, if non-nil, indicates that this call is part of loading a
+Lisp source file.
 
 This function ignores the current value of the `lexical-binding'
 variable.  Instead it will heed any
@@ -2643,7 +2665,8 @@ settings in the buffer, and if there is no such setting, the buffer
 will be evaluated without lexical binding.
 
 This function preserves the position of point.  */)
-  (Lisp_Object buffer, Lisp_Object printflag, Lisp_Object filename, Lisp_Object unibyte, Lisp_Object do_allow_print)
+  (Lisp_Object buffer, Lisp_Object printflag, Lisp_Object filename,
+   Lisp_Object unibyte, Lisp_Object do_allow_print, Lisp_Object loading)
 {
   specpdl_ref count = SPECPDL_INDEX ();
   Lisp_Object tem, buf;
@@ -2667,7 +2690,10 @@ This function preserves the position of point.  */)
   specbind (Qstandard_output, tem);
   record_unwind_protect_excursion ();
   BUF_TEMP_SET_PT (XBUFFER (buf), BUF_BEGV (XBUFFER (buf)));
-  specbind (Qlexical_binding, lisp_file_lexically_bound_p (buf) ? Qt : Qnil);
+  lexical_cookie_t lc = lisp_file_lexical_cookie (buf);
+  if (!NILP (loading) && lc == Cookie_None)
+    warn_missing_cookie (filename);
+  specbind (Qlexical_binding, lc == Cookie_Lex ? Qt : Qnil);
   BUF_TEMP_SET_PT (XBUFFER (buf), BUF_BEGV (XBUFFER (buf)));
   readevalloop (buf, 0, filename,
                !NILP (printflag), unibyte, Qnil, Qnil, Qnil);