aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBozhidar Batsov <bozhidar@toptal.com>2026-04-25 22:50:36 +0100
committerBozhidar Batsov <bozhidar@toptal.com>2026-04-25 22:50:36 +0100
commita8a90311044f240f9b1d4789a32d5144f8d955ec (patch)
tree30513d86b30bb72a405713fdb7c8969ab8568482
parent82fd4a5d3ab97cf5a8fdf65c38a68995ec1b3ef1 (diff)
Warn when a + keep entry contains glob metacharactersdirconfig-improvements
The parser silently turns every keep entry into a directory via file-name-as-directory, which means a user-typed +*.json or +/foo.txt becomes "*.json/" or "foo.txt/" and quietly never matches anything. Spot the obvious misuses (anything containing *, ?, or [) at parse time and emit a warning so the user can correct the file or move the pattern to an ignore/ensure rule.
-rw-r--r--CHANGELOG.md1
-rw-r--r--projectile.el27
-rw-r--r--test/projectile-test.el26
3 files changed, 47 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b1c821..40a5c62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
### 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.
+* Warn when a `+' keep entry in `.projectile' contains glob metacharacters. The `+' prefix is for subdirectory paths only and globs are silently coerced into a non-matching directory name; the warning surfaces the misuse rather than letting it fail silently.
* [#1964](https://github.com/bbatsov/projectile/issues/1964): Implement `project-name` and `project-buffers` methods for the `project.el` integration, so that code using `project.el` APIs returns correct results for Projectile-managed projects.
* [#1837](https://github.com/bbatsov/projectile/issues/1837): Add `eat` project terminal commands with keybindings `x x` and `x 4 x`.
* Add keybinding `A` (in the projectile command map) and a menu entry for `projectile-add-known-project`.
diff --git a/projectile.el b/projectile.el
index 7d7d745..7f2b754 100644
--- a/projectile.el
+++ b/projectile.el
@@ -2176,6 +2176,18 @@ 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)))
+(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
+each entry to a directory, so a glob pattern would never match."
+ (display-warning
+ 'projectile
+ (format "%s contains `+%s', but `+' entries are treated as \
+subdirectory paths and globs are not expanded. Use a plain directory \
+or move the pattern to a `-'/`!' rule."
+ dirconfig entry)
+ :warning))
+
(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."
@@ -2200,12 +2212,15 @@ Returns a list of (KEEP IGNORE ENSURE) or nil if the file doesn't exist."
(?! (push (buffer-substring (1+ (point)) (line-end-position)) ensure))
(_ (push (buffer-substring (point) (line-end-position)) ignore)))
(forward-line)))
- (list (mapcar (lambda (f) (file-name-as-directory (string-trim f)))
- (delete "" (reverse keep)))
- (mapcar #'string-trim
- (delete "" (reverse ignore)))
- (mapcar #'string-trim
- (delete "" (reverse ensure)))))))
+ (let ((trimmed-keep (mapcar #'string-trim (delete "" (reverse keep)))))
+ (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))))))))
(defun projectile-parse-dirconfig-file ()
"Parse project ignore file and return directories to ignore and keep.
diff --git a/test/projectile-test.el b/test/projectile-test.el
index 21e04f9..7e1e2a1 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -572,7 +572,31 @@ 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 '(nil ("keep-this") nil))))
+ (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
+ (lambda (_filename)
+ (save-excursion (insert "+/*.json\n+/src\n"))))
+ (spy-on 'display-warning)
+ (projectile-parse-dirconfig-file)
+ (expect 'display-warning :to-have-been-called-times 1))
+ (it "does not warn for plain + subdirectory entries"
+ (spy-on 'file-exists-p :and-return-value t)
+ (spy-on 'insert-file-contents :and-call-fake
+ (lambda (_filename)
+ (save-excursion (insert "+/src\n+/tests/foo\n"))))
+ (spy-on 'display-warning)
+ (projectile-parse-dirconfig-file)
+ (expect 'display-warning :not :to-have-been-called))
+ (it "does not warn for - ignore entries that contain globs"
+ (spy-on 'file-exists-p :and-return-value t)
+ (spy-on 'insert-file-contents :and-call-fake
+ (lambda (_filename)
+ (save-excursion (insert "-*.json\n-build/*.tmp\n"))))
+ (spy-on 'display-warning)
+ (projectile-parse-dirconfig-file)
+ (expect 'display-warning :not :to-have-been-called)))
(describe "projectile-parse-dirconfig-file with a real file"
(before-each