aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBozhidar Batsov <bozhidar@toptal.com>2026-04-25 23:01:11 +0100
committerBozhidar Batsov <bozhidar@toptal.com>2026-04-25 23:01:11 +0100
commit4928cbf3306b4699f6c4a7b63f934b647d545c00 (patch)
treece734f75451c345acfba471d4181b131138be75c
parente86fbda84ac2f4da82d64387209724ddcf1affd9 (diff)
Return a projectile-dirconfig struct from the parser
Replace the positional (KEEP IGNORE ENSURE) triple with a cl-defstruct. Every internal call site used car/cadr/caddr to pull out a slot, which is unreadable and error-prone — slot accessors make the intent explicit and let cl-defstruct grow a fourth field later without touching every consumer. Existing callers that compared against the raw triple (a couple of internal helper tests) are updated to construct the struct with make-projectile-dirconfig.
-rw-r--r--CHANGELOG.md4
-rw-r--r--projectile.el56
-rw-r--r--test/projectile-test.el62
3 files changed, 75 insertions, 47 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40a5c62..e4e33a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## master (unreleased)
+### Changes
+
+* `projectile-parse-dirconfig-file' now returns a `projectile-dirconfig' struct (with `keep', `ignore', and `ensure' slots) instead of a positional 3-tuple. External callers should use the accessors (`projectile-dirconfig-keep' etc.) rather than `car'/`cadr'/`caddr'.
+
### New features
* Warn once per session when `projectile-indexing-method' is `alien' but the project has a non-empty `.projectile' file, so users notice their dirconfig rules are being bypassed. Controlled by the new `projectile-warn-when-dirconfig-is-ignored' option.
diff --git a/projectile.el b/projectile.el
index 7f2b754..dd6fce0 100644
--- a/projectile.el
+++ b/projectile.el
@@ -1516,8 +1516,10 @@ If PROJECT is not specified acts on the current project."
;;; Project indexing
(defun projectile-get-project-directories (project-dir)
"Get the list of PROJECT-DIR directories that are of interest to the user."
- (mapcar (lambda (subdir) (concat project-dir subdir))
- (or (car (projectile-parse-dirconfig-file)) '(""))))
+ (let* ((cfg (projectile-parse-dirconfig-file))
+ (keep (and cfg (projectile-dirconfig-keep cfg))))
+ (mapcar (lambda (subdir) (concat project-dir subdir))
+ (or keep '("")))))
(defun projectile--directory-p (directory)
"Checks if DIRECTORY is a string designating a valid directory."
@@ -2101,13 +2103,23 @@ Unignored files are not included."
Unignored directories are not included."
(seq-filter 'file-directory-p (projectile-project-ignored)))
+(defun projectile--dirconfig-ignore ()
+ "Return the IGNORE entries from the project's dirconfig, or nil."
+ (when-let* ((cfg (projectile-parse-dirconfig-file)))
+ (projectile-dirconfig-ignore cfg)))
+
+(defun projectile--dirconfig-ensure ()
+ "Return the ENSURE entries from the project's dirconfig, or nil."
+ (when-let* ((cfg (projectile-parse-dirconfig-file)))
+ (projectile-dirconfig-ensure cfg)))
+
(defun projectile-paths-to-ignore ()
"Return a list of ignored project paths."
- (projectile-normalise-paths (cadr (projectile-parse-dirconfig-file))))
+ (projectile-normalise-paths (projectile--dirconfig-ignore)))
(defun projectile-patterns-to-ignore ()
"Return a list of relative file patterns."
- (projectile-normalise-patterns (cadr (projectile-parse-dirconfig-file))))
+ (projectile-normalise-patterns (projectile--dirconfig-ignore)))
(defun projectile-project-ignored ()
"Return list of project ignored files/directories.
@@ -2151,7 +2163,7 @@ Unignored files/directories are not included."
(defun projectile-paths-to-ensure ()
"Return a list of unignored project paths."
- (projectile-normalise-paths (caddr (projectile-parse-dirconfig-file))))
+ (projectile-normalise-paths (projectile--dirconfig-ensure)))
(defun projectile-files-to-ensure ()
(let ((default-directory (projectile-project-root)))
@@ -2160,7 +2172,7 @@ Unignored files/directories are not included."
(defun projectile-patterns-to-ensure ()
"Return a list of relative file patterns."
- (projectile-normalise-patterns (caddr (projectile-parse-dirconfig-file))))
+ (projectile-normalise-patterns (projectile--dirconfig-ensure)))
(defun projectile-filtering-patterns ()
(cons (projectile-patterns-to-ignore)
@@ -2176,6 +2188,15 @@ Unignored files/directories are not included."
"Return the absolute path to the project's dirconfig file."
(expand-file-name projectile-dirconfig-file (projectile-project-root)))
+(cl-defstruct projectile-dirconfig
+ "Parsed contents of a project's dirconfig file.
+KEEP is the list of subdirectories to restrict the project to (as
+returned with a trailing slash). IGNORE and ENSURE are the lists
+of files or directories to ignore and to forcibly include,
+respectively. All slots default to nil, which represents \"no
+file present or no entries of this kind\"."
+ (keep nil) (ignore nil) (ensure nil))
+
(defun projectile--warn-glob-in-keep-entry (entry dirconfig)
"Warn that ENTRY in DIRCONFIG looks like a glob pattern after a `+'.
The `+' prefix is for subdirectories only; the parser silently coerces
@@ -2190,7 +2211,7 @@ or move the pattern to a `-'/`!' rule."
(defun projectile--parse-dirconfig-file-uncached ()
"Parse the dirconfig file without caching.
-Returns a list of (KEEP IGNORE ENSURE) or nil if the file doesn't exist."
+Return a `projectile-dirconfig' or nil if the file doesn't exist."
(let (keep ignore ensure (dirconfig (projectile-dirconfig-file)))
(when (projectile-file-exists-p dirconfig)
(with-temp-buffer
@@ -2216,20 +2237,19 @@ Returns a list of (KEEP IGNORE ENSURE) or nil if the file doesn't exist."
(dolist (entry trimmed-keep)
(when (string-match-p "[*?[]" entry)
(projectile--warn-glob-in-keep-entry entry dirconfig)))
- (list (mapcar #'file-name-as-directory trimmed-keep)
- (mapcar #'string-trim
- (delete "" (reverse ignore)))
- (mapcar #'string-trim
- (delete "" (reverse ensure))))))))
+ (make-projectile-dirconfig
+ :keep (mapcar #'file-name-as-directory trimmed-keep)
+ :ignore (mapcar #'string-trim (delete "" (reverse ignore)))
+ :ensure (mapcar #'string-trim (delete "" (reverse ensure))))))))
(defun projectile-parse-dirconfig-file ()
- "Parse project ignore file and return directories to ignore and keep.
+ "Parse project ignore file and return its rules.
-The return value is a list of three elements: the car is the list
-of directories to keep, the cadr is the list of files or
-directories to ignore, and the caddr is the list of files or
-directories to ensure (i.e. forcibly include even when otherwise
-ignored).
+The return value is a `projectile-dirconfig' struct with three
+slots: KEEP (subdirectories to restrict the project to), IGNORE
+(files or directories to skip), and ENSURE (files or directories
+to forcibly include even when otherwise ignored). When the file
+does not exist, the return value is nil.
Lines are dispatched on their first non-whitespace character:
diff --git a/test/projectile-test.el b/test/projectile-test.el
index 7e1e2a1..30ddaa3 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -534,22 +534,23 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
(spy-on 'insert-file-contents :and-call-fake
(lambda (filename)
(save-excursion (insert "\n-exclude\n+include\n#may-be-a-comment\nno-prefix\n left-wspace\nright-wspace\t\n"))))
- (expect (projectile-parse-dirconfig-file) :to-equal '(("include/")
- ("exclude"
- "#may-be-a-comment"
- "no-prefix"
- "left-wspace"
- "right-wspace")
- nil))
+ (expect (projectile-parse-dirconfig-file)
+ :to-equal (make-projectile-dirconfig
+ :keep '("include/")
+ :ignore '("exclude"
+ "#may-be-a-comment"
+ "no-prefix"
+ "left-wspace"
+ "right-wspace")))
;; same test - but with comment lines enabled using prefix '#'
(let ((projectile-dirconfig-comment-prefix ?#))
- (expect (projectile-parse-dirconfig-file) :to-equal '(("include/")
- ("exclude"
- "no-prefix"
- "left-wspace"
- "right-wspace")
- nil)))
- )
+ (expect (projectile-parse-dirconfig-file)
+ :to-equal (make-projectile-dirconfig
+ :keep '("include/")
+ :ignore '("exclude"
+ "no-prefix"
+ "left-wspace"
+ "right-wspace")))))
(it "skips leading whitespace before dispatching on the prefix"
(spy-on 'file-exists-p :and-return-value t)
(spy-on 'insert-file-contents :and-call-fake
@@ -560,9 +561,10 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
" !indented-ensure\n"
" no-prefix-indented\n"))))
(expect (projectile-parse-dirconfig-file)
- :to-equal '(("indented-include/")
- ("indented-exclude" "no-prefix-indented")
- ("indented-ensure"))))
+ :to-equal (make-projectile-dirconfig
+ :keep '("indented-include/")
+ :ignore '("indented-exclude" "no-prefix-indented")
+ :ensure '("indented-ensure"))))
(it "treats indented comment-prefix lines as comments"
(spy-on 'file-exists-p :and-return-value t)
(spy-on 'insert-file-contents :and-call-fake
@@ -572,7 +574,7 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
"-keep-this\n"))))
(let ((projectile-dirconfig-comment-prefix ?#))
(expect (projectile-parse-dirconfig-file)
- :to-equal '(nil ("keep-this") nil))))
+ :to-equal (make-projectile-dirconfig :ignore '("keep-this")))))
(it "warns when a + keep entry contains glob metacharacters"
(spy-on 'file-exists-p :and-return-value t)
(spy-on 'insert-file-contents :and-call-fake
@@ -613,9 +615,10 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
"stale-pattern\n"))
(spy-on 'projectile-project-root :and-return-value root)
(expect (projectile-parse-dirconfig-file)
- :to-equal '(("/src/")
- ("/build" "stale-pattern")
- ("/build/keepme")))))))
+ :to-equal (make-projectile-dirconfig
+ :keep '("/src/")
+ :ignore '("/build" "stale-pattern")
+ :ensure '("/build/keepme")))))))
(it "round-trips non-ASCII paths through the parser"
(projectile-test-with-sandbox
(projectile-test-with-files
@@ -627,9 +630,9 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
"+/プロジェクト\n"))
(spy-on 'projectile-project-root :and-return-value root)
(expect (projectile-parse-dirconfig-file)
- :to-equal '(("/プロジェクト/")
- ("héllo/wörld")
- nil))))))
+ :to-equal (make-projectile-dirconfig
+ :keep '("/プロジェクト/")
+ :ignore '("héllo/wörld")))))))
(it "tolerates a trailing line without a final newline"
(projectile-test-with-sandbox
(projectile-test-with-files
@@ -638,7 +641,7 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
(with-temp-file (expand-file-name ".projectile" root)
(insert "-foo\n-bar"))
(spy-on 'projectile-project-root :and-return-value root)
- (expect (cadr (projectile-parse-dirconfig-file))
+ (expect (projectile-dirconfig-ignore (projectile-parse-dirconfig-file))
:to-equal '("foo" "bar")))))))
(describe "dirconfig cache"
@@ -667,14 +670,14 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
(dirconfig (expand-file-name ".projectile" root)))
(with-temp-file dirconfig (insert "-foo\n"))
(spy-on 'projectile-project-root :and-return-value root)
- (expect (cadr (projectile-parse-dirconfig-file))
+ (expect (projectile-dirconfig-ignore (projectile-parse-dirconfig-file))
:to-equal '("foo"))
;; Force a distinct mtime — file-attribute-modification-time has
;; second-level resolution on some filesystems.
(set-file-times dirconfig (time-add (current-time) 5))
(with-temp-file dirconfig (insert "-bar\n"))
(set-file-times dirconfig (time-add (current-time) 5))
- (expect (cadr (projectile-parse-dirconfig-file))
+ (expect (projectile-dirconfig-ignore (projectile-parse-dirconfig-file))
:to-equal '("bar"))))))
(it "returns nil and does not cache when the dirconfig file is absent"
(projectile-test-with-sandbox
@@ -766,11 +769,12 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
(describe "projectile-get-project-directories"
(it "gets the list of project directories"
(spy-on 'projectile-project-root :and-return-value "/my/root/")
- (spy-on 'projectile-parse-dirconfig-file :and-return-value '(nil))
+ (spy-on 'projectile-parse-dirconfig-file :and-return-value nil)
(expect (projectile-get-project-directories "/my/root") :to-equal '("/my/root")))
(it "gets the list of project directories with dirs to keep"
(spy-on 'projectile-project-root :and-return-value "/my/root/")
- (spy-on 'projectile-parse-dirconfig-file :and-return-value '(("foo" "bar/baz")))
+ (spy-on 'projectile-parse-dirconfig-file
+ :and-return-value (make-projectile-dirconfig :keep '("foo" "bar/baz")))
(expect (projectile-get-project-directories "/my/root/") :to-equal '("/my/root/foo" "/my/root/bar/baz"))))
(describe "projectile-dir-files"