Subject: 30.0.50; [PATCH] Add permanently-enabled-local-variable-dirs variable.

From: Antero Mejr <[email-masked]>
Date: Wed, 01 Mar 2023 22:20:33 +0000

This patch allows users to trust directories to load dir-local variables
from, so they don't have to do something lile this:
(defun risky-local-variable-p (sym &optional _ignored) nil)
as suggested here:
https://emacs.stackexchange.com/questions/10983/remember-permission-to-execute-risky-local-variables

It also works over TRAMP if enable-remote-dir-locals is true.

>From 93494f5beb4b51d989ea87755c077379458ffb04 Mon Sep 17 00:00:00 2001
From: Antero Mejr <[email-masked]>
Date: Wed, 1 Mar 2023 21:59:57 +0000
Subject: [PATCH] Add permanently-enabled-local-variable-dirs variable.

This variable can be set to automatically load risky dir-local variables from
a list of trusted directories.

* lisp/emacs-lisp/files.el (permanently-enabled-local-variable-dirs,
hack-local-variables-filter, hack-local-variables-confirm): New variable and
associated logic.
* test/lisp/files-tests.el
(files-tests-permanently-enabled-local-variable-dirs): Add tests for same.
* doc/lispref/variables.texi (File Local Variables): Add documentation for
same.
* etc/NEWS (Lisp Changes in Emacs 30.1): Add news entry for same.
---
 doc/lispref/variables.texi |  6 ++++++
 etc/NEWS                   |  5 +++++
 lisp/files.el              | 27 ++++++++++++++++++++++-----
 test/lisp/files-tests.el   | 22 ++++++++++++++++++++++
 4 files changed, 55 insertions(+), 5 deletions(-)

diff --git a/doc/lispref/variables.texi b/doc/lispref/variables.texi
index 5584cbce9a6..47cfb824dcb 100644
--- a/doc/lispref/variables.texi
+++ b/doc/lispref/variables.texi
@@ -1974,6 +1974,12 @@ File Local Variables
 symbols.
 @end defvar
 
+@defvar permanently-enabled-local-variable-dirs
+This is a list of trusted directories that contain local variables.
+Local variables in these directories will always be enabled, regardless
+of whether they are risky.
+@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 31fb22fc1e2..cc5198a903b 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -238,6 +238,11 @@ hooks named after the feature name, like 'esh-mode-unload-hook'.
 
 * Lisp Changes in Emacs 30.1
 
++++
+** New variable 'permanently-enabled-local-variable-dirs'.
+This variable is used to to permanently trust directories containing
+risky directory-local variables.
+
 ** Functions and variables to transpose sexps
 
 +++
diff --git a/lisp/files.el b/lisp/files.el
index 387a3b5dc66..bde126375ae 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -681,7 +681,8 @@ enable-local-variables
 always obeys file local variable specifications and the -*- line,
 and ignores this variable.
 
-Also see the `permanently-enabled-local-variables' variable."
+Also see the `permanently-enabled-local-variables' and
+'permanently-enabled-local-variable-dirs' variables."
   :risky t
   :type '(choice (const :tag "Query Unsafe" t)
 		 (const :tag "Safe Only" :safe)
@@ -3692,6 +3693,14 @@ permanently-enabled-local-variables
   "A list of file-local variables that are always enabled.
 This overrides any `enable-local-variables' setting.")
 
+(defcustom permanently-enabled-local-variable-dirs '()
+  "A list of directories that contain local variables that are always
+enabled, regardless of whether they are risky."
+  :version "30.1"
+  :type '(repeat string)
+  :risky t
+  :group 'find-file)
+
 (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.
@@ -3730,7 +3739,9 @@ hack-local-variables-confirm
 !  -- to apply the local variables list, and permanently mark these
       values (*) as safe (in the future, they will be set automatically.)
 i  -- to ignore the local variables list, and permanently mark these
-      values (*) as ignored\n\n")
+      values (*) as ignored
++  -- to apply the local variables list, and permanently trust "
+                    name "\n\n")
 	  (insert "\n\n"))
 	(dolist (elt all-vars)
 	  (cond ((member elt unsafe-vars)
@@ -3754,7 +3765,7 @@ hack-local-variables-confirm
 	(pop-to-buffer buf '(display-buffer--maybe-at-bottom))
 	(let* ((exit-chars '(?y ?n ?\s))
 	       (prompt (format "Please type %s%s: "
-			       (if offer-save "y, n, ! or i" "y or n")
+			       (if offer-save "y, n, !, i, or +" "y or n")
 			       (if (< (line-number-at-pos (point-max))
 				      (window-body-height))
 				   ""
@@ -3762,8 +3773,13 @@ hack-local-variables-confirm
 	       char)
 	  (when offer-save
             (push ?i exit-chars)
-            (push ?! exit-chars))
+            (push ?! exit-chars)
+            (push ?+ exit-chars))
 	  (setq char (read-char-choice prompt exit-chars))
+          (when (and offer-save (= char ?+))
+            (customize-push-and-save
+             'permanently-enabled-local-variable-dirs
+             (list dir-name)))
 	  (when (and offer-save
                      (or (= char ?!) (= char ?i))
                      unsafe-vars)
@@ -3772,7 +3788,7 @@ hack-local-variables-confirm
                  'safe-local-variable-values
                'ignored-local-variable-values)
              unsafe-vars))
-	  (prog1 (memq char '(?! ?\s ?y))
+	  (prog1 (memq char '(?! ?\s ?y ?+))
 	    (quit-window t)))))))
 
 (defconst hack-local-variable-regexp
@@ -3904,6 +3920,7 @@ hack-local-variables-filter
 		  (null unsafe-vars)
 		  (null risky-vars))
 	     (memq enable-local-variables '(:all :safe))
+             (member dir-name permanently-enabled-local-variable-dirs)
 	     (hack-local-variables-confirm all-vars unsafe-vars
 					   risky-vars dir-name))
 	 (dolist (elt all-vars)
diff --git a/test/lisp/files-tests.el b/test/lisp/files-tests.el
index aadb60e1de7..95eaf9a6bd0 100644
--- a/test/lisp/files-tests.el
+++ b/test/lisp/files-tests.el
@@ -166,6 +166,28 @@ files-tests-permanent-local-variables
       (hack-local-variables)
       (should (eq lexical-binding nil)))))
 
+(ert-deftest files-tests-permanently-enabled-local-variable-dirs ()
+  ;; permanently-enabled-local-variable-dirs should be risky,
+  ;; so use it as an arbitrary risky variable.
+  (let ((test-alist '((permanently-enabled-local-variable-dirs
+                       . "some_val")))
+        (fakedir "test1/test2")
+        (enable-local-eval t))
+    (with-temp-buffer
+      (setq permanently-enabled-local-variable-dirs (list fakedir))
+      (hack-local-variables-filter test-alist fakedir)
+      (should (equal file-local-variables-alist test-alist)))
+    (with-temp-buffer
+      (setq permanently-enabled-local-variable-dirs (list fakedir))
+      (setq noninteractive t)
+      (hack-local-variables-filter test-alist "wrong")
+      (should-not (equal file-local-variables-alist test-alist)))
+    (with-temp-buffer
+      (setq permanently-enabled-local-variable-dirs '())
+      (setq noninteractive t)
+      (hack-local-variables-filter test-alist fakedir)
+      (should-not (equal file-local-variables-alist test-alist)))))
+
 (defvar files-test-bug-18141-file
   (ert-resource-file "files-bug18141.el.gz")
   "Test file for bug#18141.")
-- 
2.38.1